# Comparing classes

> `==` on two class values compares them BY VALUE, field by field, rather than by reference. A class is a value you build rather than a row you hold, and the runtime rebuilds it freely — so two separately constructed values with the same contents are equal, and a class rebuilt on the next render still equals the one you kept.

<!-- id: class-equality · area: class · stability: preview · html: https://osysharp.com/reference/class/equality/ -->

## Summary        {#summary}
Two class values are equal when **their fields are equal**:

```osy title="value equality, not reference equality" test app=class-equality
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      {#signature}
```osy syntax
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 check
```

## Description    {#description}

### Why value equality   {#why}
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}
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   {#function-fields}
A selector field compares by **which lambda it is**, not by the object wrapping it:

```osy title="a shape carrying a selector" test app=class-equality-func
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   {#entities}
An [entity](https://osysharp.com/reference/entity/declaration/) 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](https://osysharp.com/reference/entity/equality/) has that rule in full, and what it
means for `Contains`/`IndexOf`/`Remove` over a list of rows.

## Examples       {#examples}

```osy title="a rebuilt value still matches the one you kept" test app=class-equality-render
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         {#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       {#see-also}
- [Classes](https://osysharp.com/reference/class/index/) — what a class is, and when to reach for one instead of an entity
- [class properties](https://osysharp.com/reference/class/properties/) — declaring the fields this rule compares
- [Copying a class with changes](https://osysharp.com/reference/class/with/) — copying a value with some fields replaced, which this rule is what makes coherent
- [Generic classes](https://osysharp.com/reference/class/generics/) — the `Column<T>` shape used above
- [Comparing entity rows](https://osysharp.com/reference/entity/equality/) — the other half of this rule: why an entity compares by identity instead
- [entity](https://osysharp.com/reference/entity/declaration/) — what a row is, and how you read one back
