Osy#betaRun it on Osyrin: osyrin.com

Reference / Class

Comparing classes

a == b — the same instance for a class; equal members for a record or a struct

`==` on two `class` values asks whether they are the SAME INSTANCE, exactly as C# does: two objects built separately are two objects, however equal their fields. A `record` (and a `record struct`) compares by its members instead, and so does `Equals` on a `struct`. The answer is the same on the server and in the browser, and an instance handed to a server function and back is still the same instance.

preview3 examples compiled by CIclasstypesauthoring

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 check

Description#

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 holdingcompares by
a scalar (int, string, decimal, DateTime, an enum)its value
a record or a structits members, recursively
a plain classits identity
a list or an array (string[], List<T>)its elements, in order
an entitythat 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#

MessageCauseFix
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 readable ToString
  • 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

Related

Classes

A class is an in-memory shape — data plus the behaviour that belongs to it — and it never touches the database. That is…

Records

A `record` is a class whose values compare by their MEMBERS rather than by identity, exactly as a C# record does. The…

Structs

A `struct` is a VALUE type, as in C#: assigning one, passing one, returning one or putting one in a list stores a COPY…

Copying a class with changes

`with` makes a COPY of a class value and replaces the fields you name. Everything you do not name comes from the value…

Comparing entity rows

`==` on two entity references compares them by ROW IDENTITY — not by object reference, and not field by field. Two…

class properties

A class member that reads and writes like a field but runs a body on access — a computed value, a validating setter, or…