Summary#
A class compares by identity; a record by its members:
class Money { public decimal Amount; public string Currency = ""; }
record Price(decimal Amount, string Currency);
string Compare() {
var a = new Money { Amount = 9.50m, Currency = "USD" };
var b = new Money { Amount = 9.50m, Currency = "USD" };
var p = new Price(9.50m, "USD");
var q = new Price(9.50m, "USD");
return $"{a == b} {a == a} {p == q}"; // "False True True"
}a and b hold the same fields and are still two instances. p and q are records, so they are equal because
every member is.
Signature#
a == b // class: the same instance · record / record struct: every member equal
a != b // the negation
a.Equals(b) // the same question — and the only one a plain struct answers (it has no ==)
a == null // a class or a record is a reference type, so this is a legitimate checkDescription#
Why identity#
It is C#'s answer, and a class is how C# spells "a thing with an identity": the order being edited, the column the grid is sorted by, the step a wizard is on. Two of them with the same fields are still two of them.
The runtime keeps that answer honest where it could otherwise slip. A class value that crosses from the browser to
the server and back — an argument to a function that runs on the server and returns it — is rebuilt on each side, so each
instance carries a hidden identity that travels with it, and the rebuilt object is the instance it was made from:
Echo(m) == m is true, as in C#, and a twin built with the same fields is not.
What it does mean: a new inside a component's render makes a new instance every render, exactly as a new
inside a C# method makes one every call. When you need two separately built values to compare equal — a sort column
rebuilt each render, a filter in a list — make the type a record.
What a record compares#
A record (and a record struct, and Equals on a struct) compares every member with ==, so the
rule composes:
| a member holding | compares by |
|---|---|
a scalar (int, string, decimal, DateTime, an enum) | its value |
| a record or a struct | its members, recursively |
| a plain class | its identity |
a list or an array (string[], List<T>) | its elements, in order |
| an entity | that entity's identity, exactly as == on an entity does |
a Func<…> / Action<…> | the lambda it came from — see below |
| nothing (never assigned) | null, which is what an unset member is |
Two records of different types are never equal, however identical their members. Distinct(), Contains,
IndexOf and a dictionary key follow the same rule as ==: two equal records are one element and one key, and two
instances of a plain class are two.
A member holding a function#
A selector member compares by which lambda it is, not by the object wrapping it:
record Column<T> {
public string Label = "";
public Func<T, string> Value;
}Two Column records written from the same r => r.Title are equal; two written from different lambdas are not —
C#'s "same method" question. That is what lets a shape that carries behaviour, such as a table column, be found
again on the next render.
Entities compare by identity too#
An entity is a row, so two entity values are equal when they are the same row — its id, not its current field values. Comparing entity rows has that rule in full.
Examples#
record Column<T> {
public string Name = "";
public Func<T, string> Value;
}
[Composable] component SortHeader<T>(Column<T>[] columns) {
Column<T> sortBy = columns[0];
bool descending = false;
// `columns` is rebuilt every render, so `c` is a different INSTANCE from `sortBy` — and, being a record, an equal
// VALUE. Clicking the sorted column toggles direction; clicking another one selects it.
action Choose(Column<T> c) {
if (sortBy == c) { descending = !descending; } else { sortBy = c; descending = false; }
}
render {
Row {
foreach (var c in columns) {
Pressable(onClick: () => Choose(c)) { Text(c.Name); }
}
}
}
}Errors#
| Message | Cause | Fix |
|---|---|---|
cannot compare 'X' with 'Y' | The two operands are unrelated types. | Compare values of the same type, or compare a member of each. |
`==` is not defined on the struct 'X' | A plain struct has no == in C#. | Write a.Equals(b), or declare it record struct X. |
See also#
- Records — a type that compares by its members, with
with, deconstruction and a readableToString - Structs — a value type: copied where it is stored
- Classes — what a class is, and when to reach for one instead of an entity
- Copying a class with changes — copying a value with some members replaced
- Comparing entity rows — why an entity compares by its row