On this page

18. Lazy values

18.1 Declaration and typing

CS-LAZY-001. let value = lazy expression defers the complete initializer. Typed declarations and let customer as lazy new Customer are supported. lazy let and new lazy are not accepted forms.

CS-LAZY-002. The source value has the initializer's value type, not a publicly exposed Lazy <of T> wrapper. Ordinary value use forces it. No extra source .Value access is required.

public action LoadLookup(path as String, showContents as Boolean)
    let contents = lazy Global.System.IO.File.ReadAllText(path)
    if showContents then
        Global.System.Console.WriteLine(contents)
    end if
end action

CS-LAZY-003. The binding is assign-once and cannot be reassigned, passed through a writable by-reference parameter, or exposed with a writable setter. Mutability of the computed object is independent of binding immutability.

18.2 First use and publication

CS-LAZY-004. An unused lazy initializer never runs. On first force, its complete expression executes, including calls and captured mutable-variable reads. These reads occur at force time rather than declaration time.

CS-LAZY-005. Concurrent first uses execute the initializer exactly once. Other consumers wait for that execution and observe its safely published outcome. This does not make the resulting object's later operations thread-safe.

CS-LAZY-006. A successful result is cached, including nothing. An initialization exception is cached and rethrown on later forces; there is no automatic retry.

18.3 State inspection

CS-LAZY-007. State predicates inspect the wrapper state without forcing the initializer.

State is initialized is not initialized is working is faulted
Not started false true false false
Initializing false true true false
Completed successfully, including nothing true false false false
Failed false true false true

These observations do not freeze another thread's progress or constitute synchronization for later application operations.

CS-LAZY-008. Direct or indirect recursive forcing of the same still-initializing value must fail clearly rather than deadlock or recurse indefinitely. Statically apparent cycles may also be diagnosed. The failure must identify recursive forcing; no specific recursion exception class is required by this rule.

18.4 Examples and diagnostics

let factor = 2
let total = lazy factor * 10
factor = 3
Global.System.Console.WriteLine(total) // first force computes 30

Reject assignment to total, writable by-reference uses and invalid lazy declaration forms. An initializer exception propagates to the forcing operation and is retained for subsequent forces. A successful nothing does not count as uninitialized.

18.5 Reference C# lowering

For the preceding value-only example:

int factor = 2;
var total = new Lazy<int>(() => factor * 10,
    System.Threading.LazyThreadSafetyMode.ExecutionAndPublication);
factor = 3;
Console.WriteLine(total.Value);

System.Lazy supplies once-only initialization and exception caching for this factory-based shape. The full language state-predicate surface, notably working/faulted inspection, requires additional compiler/runtime support; this snippet does not claim to implement it. Generated wrapper names and storage are implementation choices.

The complete lazy reference supplies a report-index application and private C# state/publication pattern, including nonforcing observations, original failure retention and recursive-force dependencies across value types and threads. These are reference implementation strategies, not public wrapper APIs.

The construct supplies no async-lazy, background-start or cancellation policy. Ordinary .NET lazy APIs remain available directly when an application needs their explicit API.


Specification contents · Previous: 17. Async, iteration and concurrency · Next: 19. Events