Summary#
Two class values are equal when their fields are equal:
class Money {
public decimal Amount;
public string Currency;
}
bool SamePrice() {
var a = new Money { Amount = 9.50m, Currency = "USD" };
var b = new Money { Amount = 9.50m, Currency = "USD" };
return a == b; // true — two values, one content
}a and b were built separately and are still equal. That is the rule, and it is the one place Osy# deliberately
answers differently from C#'s class.
Signature#
a == b // true when both are the SAME class and every field is equal
a != b // the negation
a == null // a class is a reference type, so this is a legitimate checkDescription#
Why value equality#
A class is an in-memory value, not a row: it has no table and no id, and the platform rebuilds it wherever it
needs to. A new written inside a component's render is rebuilt on every render. A class handed between the
browser and the server is serialised and reconstructed on the far side. Nothing preserves object identity across any
of that.
So reference equality would not merely be a different answer — it would be an answer that can never be true for two
values you did not personally hold onto within a single expression. That is why the rule is value equality: it is the
only question the runtime can answer honestly, and it is the one authors are asking.
If you are coming from C#: an Osy# class behaves like a C# record, not like a C# class.
What "equal fields" means#
Field comparison is the ordinary == applied to each field, so the rule composes:
| a field holding | compares by |
|---|---|
a scalar (int, string, decimal, DateTime, an enum) | its value |
| another class | its fields, recursively |
| 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 field is |
Two values of different classes are never equal, however identical their fields.
A field holding a function#
A selector field compares by which lambda it is, not by the object wrapping it:
class Column<T> {
public string Label;
public Func<T, string> Value;
}Two Column values written from the same r => r.Title are equal; two written from different lambdas are not. This
is the same question C# asks of a method group, and it is what lets a shape that carries behaviour — a table column,
a formatting rule — be compared at all. Without it a single function field would make every such value unequal to
every other, including to itself one render later.
Entities compare differently, and should#
An entity is a row, so two entity values are equal when they are the same row — its
identity, not its current field values. Two rows with identical columns are still two rows. A class has no identity
to compare, which is exactly why its contents are the answer. Comparing entity rows has that rule in full, and what it
means for Contains/IndexOf/Remove over a list of rows.
Examples#
class 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 not the same OBJECT as `sortBy` — it is the same 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 class, or compare a field of each. |
See also#
- Classes — what a class is, and when to reach for one instead of an entity
- class properties — declaring the fields this rule compares
- Copying a class with changes — copying a value with some fields replaced, which this rule is what makes coherent
- Generic classes — the
Column<T>shape used above - Comparing entity rows — the other half of this rule: why an entity compares by identity instead
- entity — what a row is, and how you read one back