There are two related issues here: one is memory safety, and the other is domain invariants. We want to guarantee fields are initialised, because an uninitialised field with whatever bit pattern was previously left in that field's location might not be valid for the field's type (e.g. it may be a number out of range, a pointer to an object of the wrong type, or a stale pointer to an object which no longer exists). Additionally, we may also want programmers to be able to ensure their own invariants (e.g. for a Range
type, that this.start <= this.end
).
Definite assignment
By static analysis, a compiler can determine that a field is or isn't "definitely assigned" at each point in the constructor. The goal is to check that all fields are definitely assigned before a reference to the object can escape (by any means).
This may be done via syntax-directed rules, as Java does (albeit not for fields): for example, a field x
is definitely assigned after a statement this.x = ...;
, a field is definitely assigned after if(...) A else B
if it is definitely assigned after A
and definitely assigned after B
, and so on. Alternatively, a control-flow graph can be derived, then a variable is "definitely assigned" if every exit node is dominated by an assignment to that variable.
Such rules are conservative, so it is possible to write programs which in reality always assign to a variable, but which nonetheless don't satisfy the rules. The compiler will reject such programs; this could upset programmers who "know better than the compiler". Typescript offers an escape-hatch via definite assignment assertions, allowing the programmer to override the compiler's judgement, although (like other escape hatches) this means opting out of having the compiler check the correctness of your code.
Definite assignment checks are based on the principle of proving that the fields will be assigned after some statement or set of statements complete normally. This means that separate guarantees need to be made for when the constructor does not complete normally (e.g. if it throws an exception, yields, or falls into an infinite loop). In those cases we want to ensure that a reference to the uninitialised object cannot be acquired. The rules to ensure this can often be more onerous than the more straightforward rules requiring definite assignment before the constructor terminates.
Normally, a reference to a partially initialised object can't escape from the code which invokes the constructor, since it receives no such reference in case the constructor fails (or, if the constructor is merely paused, the invoking code doesn't receive a reference to the new object until the constructor resumes and completes). However, the constructor itself must also not provide other code with a reference to this
before all fields are initialised; in particular, instance methods should not be called, nor should a superclass constructor. (Alternatively, if a superclass constructor may be called earlier, then instance methods must not be called from anywhere in the superclass constructor, even after all superclass fields are definitely assigned.) If you allow constructors to yield, then likewise a constructor must not yield this
(or similar) before all fields are initialised.
Default field values
In Java, fields have default initial values based on their types, such that a field has a well-defined value even if is not assigned in the constructor (or an initialiser). This is a semantic guarantee rather than a runtime requirement, so a compiler is permitted to optimise away the assignment of the default value if it is a dead store.
This means that objects in Java always have their fields initialised, guaranteeing memory safety and preventing implementation-specific behaviour, though it is not guaranteed that a default initial value for a field will satisfy any invariant intended by the programmer.
Note that this is only possible in Java because null
is a valid value for all reference types. In general, non-nullable reference types do not have sensible default values, nor do function types, file handles, or so on.
Allow fallible constructors
In languages with exceptions, constructors can be permitted to throw them. This allows programmers to throw exceptions rather than allow objects to be created which violate their invariants. However, it doesn't help with memory safety since it depends on the programmer writing code to verify the object's state themselves.
Ban fallible constructors
A different approach is taken by languages like Rust, in which there are no non-trivial constructors or initialisers at all. Objects (really, structs and tuples) are created by directly providing values for all of their fields, and so it is trivially not possible to "see" an object before those values are assigned.
This guarantees memory safety, but means invariants (and other considerations such as encapsulation) must be addressed in a different way. Rather than writing a constructor which checks whether the object is being constructed correctly and throws an exception otherwise, programmers are expected to write factory functions which perform those checks before creating the object.