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

Reference / Class

Copying a class with changes

p with { Field = value } — a copy of p, with those fields replaced

`with` makes a COPY of a class value and replaces the fields you name. Everything you do not name comes from the value you copied, and the original is left untouched — so it is how you derive one value from another without rebuilding it field by field, and without mutating something another part of the program is still holding.

preview6 examples compiled by CIclasstypesauthoring

Summary#

with copies a class value and replaces the fields you name:

class Money {
  public decimal Amount;
  public string Currency = "USD";
  public string Note = "";
}

string Rename() {
  var fee = new Money { Amount = 25m, Currency = "SEK", Note = "late fee" };
  var refund = fee with { Amount = 0m };
  return refund.Currency;        // "SEK" — carried over, not re-defaulted
}

refund is a new value. It took its Amount from the initializer and everything else — Currency, Note — from fee, which is unchanged.

Signature#

value with { Field = expr, … }   // a copy of `value`, with those fields replaced
value with { }                   // a plain copy
a with { X = 1m } with { Y = 2m }  // chains; each copy is made from the one before

with is available on any class. It is not available on an entity, or on a scalar — see [[#errors|Errors]].

Description#

The fields you do not name come from the value you copied#

This is the whole point, and it is the part worth testing when something looks wrong. A with does not start from the class's declared defaults and it does not re-run field initializers:

class Settings {
  public string Theme = "light";
  public decimal Zoom = 1m;
}

string Keep() {
  var dark = new Settings { Theme = "dark", Zoom = 2m };
  var zoomed = dark with { Zoom = 3m };
  return zoomed.Theme;           // "dark" — the copy's value, NOT the declared "light"
}

The original is never modified#

with produces a value; it does not write through to the one it copied. Anything else still holding the original sees exactly what it saw before:

class Money { public decimal Amount; public string Note = ""; }

decimal Both() {
  var original = new Money { Amount = 10m, Note = "invoice" };
  var adjusted = original with { Amount = 25m };
  return original.Amount + adjusted.Amount;    // 35 — 10 and 25, two values
}

A copy keeps the type it actually is#

If you copy a value held in a base-typed variable, the copy is the type the value is, not the type the variable is declared as:

class Animal { public string Name = "?"; }
class Dog : Animal { public decimal Legs = 4m; }

bool StillADog() {
  Animal held = new Dog { Name = "rex", Legs = 3m };
  var renamed = held with { Name = "fido" };
  return renamed is Dog;         // true, and its Legs is still 3
}

Why there is no record keyword#

In C#, with works on a record and not on a plain class, because the two differ in how they compare. In Osy# they do not: a class already compares by value, which is what a C# record is. A second keyword would therefore separate nothing, so with is simply available on every class.

A consequence worth knowing: a plain copy equals the value it came from.

class Point { public decimal X; public decimal Y; }

bool SameValue() {
  var p = new Point { X = 1m, Y = 2m };
  return p == p with { };        // true — same class, same fields
}

readonly fields still cannot be set#

A readonly field is one only the constructor may assign, and a with is not a constructor — it copies an already-built value and then writes over it. To vary a readonly field, construct the value instead.

Examples#

class Quote {
  public string Customer;
  public decimal Net;
  public decimal Vat = 0m;
  public string Status = "draft";
}

decimal Finalize() {
  var draft = new Quote { Customer = "Acme", Net = 100m };
  var taxed = draft with { Vat = 25m };
  var sent  = taxed with { Status = "sent" };
  // Customer and Net rode through both copies; nobody had to restate them.
  return sent.Net + sent.Vat;    // 125
}

Errors#

| Message | Cause | Fix | |---|---|---| | with` copies an object, and a `decimal` is not one` | The value on the left is a scalar, which has no fields to replace. | Use ordinary arithmetic or assignment; `with` applies to a class. | | withcopies an in-memoryclass, and 'X' is an entity`` | The value on the left is a stored row. Copying one would silently mean either a second row or a duplicate of this one. | Create the row you want (new X { … }), or change the fields on the row you have. | | entity 'X' has no property 'Y' | The initializer names a field the class does not declare. | Check the field name against the declaration. | | 'X.Y' is readonly | A readonly field cannot be set by a copy. | Pass the value to the constructor instead. |

See also#

  • Classes — what a class is, and when to reach for one instead of an entity
  • Comparing classes — why a class compares by value, which is why with needs no record keyword
  • readonly fields — the one kind of field a copy may not replace
  • constructor — building a value from scratch rather than from another one
  • entity — why a stored row is copied differently

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…

Comparing classes

`==` on two class values compares them BY VALUE, field by field, rather than by reference. A class is a value you build…

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…

`readonly` fields

A `readonly` field can be assigned only where it is declared or in a constructor of the class that declares it…

constructor

A class declares one constructor — its name is the class name, it takes no return type, and it runs when you write new…

entity

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