Osy#betaRun it on Osyrin: osyrin.com

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
}

On a record, and on a class#

with is C#'s copy for a record and a struct. Osy# also allows it on a plain class, where it makes a new INSTANCE with the same members. What the copy equals follows from what it is: a record compares by its members, so a plain copy equals the value it came from, while a class copy is a second instance and does not.

record Point(decimal X, decimal Y);
class Marker { public decimal X; }

string Copies() {
  var p = new Point(1m, 2m);
  var m = new Marker { X = 1m };
  return $"{p == p with { }} {m == m with { }}";   // "True False"
}

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
  • Records — the type with was made for, compared by its members
  • Comparing classes — what a copy is equal to
  • 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 asks whether they are the SAME INSTANCE, exactly as C# does: two objects built separately…

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…

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 from the…