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 beforewith 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
withneeds norecordkeyword readonlyfields — 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