Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Class

Comparing classes

a == b — two class values are equal when their fields are equal

`==` 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.

preview3 examples compiled by CIclasstypesauthoring

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 check

Description#

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 holdingcompares by
a scalar (int, string, decimal, DateTime, an enum)its value
another classits fields, recursively
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 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#

MessageCauseFix
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#

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…

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…

Generic classes

A class can declare type parameters, so one shape serves every type it is used with instead of being copied per entity…

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…

entity

Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit…

Func<T, R>

A parameter or class field typed `Func<T, R>` takes a lambda and can be invoked for a result, so reusable code can be…