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