Skip to Content

Frozen

frozen<T> turns a value into a deeply immutable form: it is deep-copied at construction, cut off from its source, and from then on every write path (indexer, mutation method, field assignment) is intercepted at compile time. The paired ToMutable() is the only way to thaw.

Construction Deep-Copies

freeze(expr) evaluates the expression, then deep-copies it into a frozen value; from that point on the frozen value and the source are unrelated:

val orig = new list<int>(); orig.Add(3); orig.Add(1); orig.Add(2); val frozenList = freeze(orig); orig[0] = 99; print(frozenList); print(orig);
[3, 1, 2] [99, 1, 2]

Mutating the source does not touch the frozen value, and the reverse holds too. The deep copy is recursive: freeze([[1], [2]]) freezes the inner arrays along with the outer one, inner levels of nested structures are frozen just the same, there is no partially-mutable exception, and cyclic self-referencing structures copy safely.

Every Write Path Is Intercepted

Each write path on a frozen value is rejected at compile time with the same diagnostic, MS3267, which names the offending form:

error MS3267: cannot mutate a frozen value via 'indexer'; call ToMutable() for a mutable copy. error MS3267: cannot mutate a frozen value via 'Add'; call ToMutable() for a mutable copy. error MS3267: cannot mutate a frozen value via 'x'; call ToMutable() for a mutable copy.

The three lines correspond to an indexer write (f[0] = 9), a container mutation method (Add / Insert / Clear / Set), and a field assignment (f.x = 9). Nesting is intercepted the same way: in f[0][0] = 9 the f[0] is itself a frozen<int[]>; each level deeper, another write, another interception.

The read surface is untouched: the Length / Count properties, index reads, and iteration all work as usual. freeze(freeze(x)) is legal nesting; member calls unwrap level by level and behave exactly like the single-layer case.

ToMutable: Deriving a Non-Frozen Copy

When you need to write again, ToMutable() derives a brand-new mutable copy from the frozen value; when the source is an array, the type is pinned back to T[]:

val thawed = frozenList.ToMutable(); thawed.Add(9); print(thawed); print(frozenList.Length);
[3, 1, 2, 9] 3

thawed grows to 9 elements; frozenList keeps its length.

Equality: Reference Identity and ContentEquals

== on frozen values is reference identity: a frozen object equals itself, but two values frozen independently from the same contents are not equal. To compare by contents, use ContentEquals:

val a = freeze([1, 2, 3]); val b = freeze([1, 2, 3]); print(a == b, a.ContentEquals(b)); print(a == a);
False True True

ContentEquals compares structurally, element by element, and terminates safely on cyclic self-references. For the uniform rule across the five container families that == compares identity and ContentEquals compares contents, see Equality and Hashing.

Notes

  • Treat frozen values as read-only data: to write, go through ToMutable() for a copy; if the result needs to be frozen again, wrap it in another freeze(...).
  • Deep copies cost: on large containers both freeze(...) and ToMutable() copy the whole thing; avoid freezing and thawing repeatedly on hot paths.
Last updated on October 11, 2026