What are the two safe ways to write a generic class that has an optional 'T?' field?
← Generics · Ref: Q1348
A generic 'T?' field is uninitialised at declaration, so EK9 forces you into one of exactly two safe shapes - both eliminate the null at compile time.
SHAPE 1: PRESENT-BUT-UNSET (public no-arg, give the field a T())
Use when T is guaranteed to have a public no-argument constructor (String, Integer, your own value types). Empty construction is allowed; the field is an unset T, never null:
Box of type T content as T? Box() as pure content :=? T() Box() as pure -> initial as T content :=? initial
empty <- Box() of String // allowed; content is unset, '?' is false
SHAPE 2: NO EMPTY CONSTRUCTION (default private)
Use when T() is not available (T may be abstract, hold a function, or lack a public no-arg constructor). The no-arg default is private, so no empty instance can be created; callers must pass a value:
Box of type T content as T? default private Box() as pure Box() as pure -> initial as T content :=? initial
filled <- Box(someValue) // only way to construct - always has a value
WHAT IS NOT ALLOWED
A public 'default Box()' with a 'T?' field is E07175 - it would leave the field null. Pick shape 1 or shape 2.
The difference is a deliberate design choice: shape 1 says 'an empty one is meaningful (and unset)', shape 2 says 'there is no meaningful empty one'.
See Q1346 for the E07175 error. See Q1347 for the private escape hatch. See Q643 for the two-constructor requirement.
Example
defines module qa.genericsdeep.twosafeshapes defines class //Shape 1: present-but-unset - an empty one is meaningful Box of type T content as T? Box() as pure content :=? T() Box() as pure -> initial as T content :=? initial default operator ? //Shape 2: no empty construction - there is no meaningful empty one Required of type T content as T? default private Required() as pure Required() as pure -> initial as T content :=? initial default operator ? defines program TwoSafeShapesDemo() stdout <- Stdout() emptyBox <- Box() of String stdout.println(`empty box set: ${emptyBox?}`) filledBox <- Box("value") stdout.println(`filled box set: ${filledBox?}`) onlyWithValue <- Required("must have") stdout.println(`required set: ${onlyWithValue?}`)
Common mistakes
E07175 — A public default constructor with a 'T?' field leaves it null (E07175). Choose shape 1 (content :=? T()) or shape 2 (default private). See ek9 -h E07175 for details.
Incorrect:
default Box() as pure
Correct:
Box() as pure content :=? T()
Other ways to ask this
- How should a generic optional field be initialised in EK9?
- Default-value generic field versus no-empty-construction generic field
- Generic class with a nullable field done safely
Coming from another language?
Java/Kotlin: nothing forces a choice - the generic field is null until set and the risk is silent. Rust: Option<T> is always explicit. EK9: the compiler forces shape 1 (present-but-unset via T()) or shape 2 (no empty construction via default private), so a generic optional field is never a hidden null.
Keywords: default, T?, field, two, optional, null-safety, shapes, private, design, generic, present-but-unset