Initializers are class and struct members that provide the actions for initializing instances. They are declared using initializer declarations.
Initializers may be either named or unnamed. An unnamed initializer is identified only by the type it initializes and the types of its parameters. A named initializer also has a name, similar to a method name. Just like methods, named initializers may be overloaded based on the number and type of parameters.
initializer_declaration
: attributes? initializer_signature initializer_body
;
An empty, unnamed initializer definition is equivalent to the default initializer.
public class Example
{
public init(mut self)
{
}
}
Type instances are created and their initializers called using the type name as if it were a function.
let x = Example();
A named initializer is declared by following the init keyword with an initializer name. When
initializing an instance, a named initializer can be called with a syntax like calling an associated
function.
public class Example
{
public init named(mut self)
{
}
}
let x = Example.named();
By default initializers return the type mut Self for non-const types and const Self for const
types.
Initializers can optionally be given a return type. This type must be a base type or must have at least one type parameter specified. That is, the return type may not be the type of the class or struct the initializer is declared in, nor a subtype or unrelated type.
public class Example inherits Base_Example
{
public init(mut self) -> mut Base_Example
{
}
}
It is illegal to simply repeat the class or struct name or to use a subtype or unrelated type.
TODO: currently, the return type can be iso Self if the parameters allow it.
Initializers, like methods, have a self parameter. For classes, this is typically mut self
regardless of the return reference capability or whether the class is a const class. This is
because typically mutation is needed during the initialization of the object. However, for types
that are used as both mutable and constant, it is sometimes necessary to have initializers that take
a read only reference to self. The reason for this is that when initializing from constant data, it
must be possible to store references to constant data into fields of the instance that would be
mutable if the initializer were mut self.
TODO: if the self reference is read, that wouldn't technically mean that there couldn't be a
mutable reference to it elsewhere that would make assigning read only reference into it unsafe.
Perhaps the parameter actually needs to be const self? That would also enforce that one actually
needed to initialize the fields to const values. However, that would also seem to imply there can
be zero mutation after the call to the base initializer. Would there also be cases where one wanted
to construct and assign fields with mutable values that would only be frozen when the constructor
was complete?
TODO: can a mut self constructor of a const class use self after calling the base
initializer to pass a mutable reference to such a class to something? That seems very strange and
like it violates the spirit of every instance being read only.
The initializer self parameter has special reference capabilities. As described in the sections
below, it doesn't allow passing a non-fully initialized self to another method or function.
Additionally, a read only self parameter still treats var fields as assignable during the first
phase of construction (the not fully initialized section).
The reference capability of the return type of an initializer is constrained by the reference capability of the self parameter. Thus a read only self initializer cannot return a mutable reference.
If a class or struct has no initializer declarations, then a default initializer is created for it.
This initializer is a published unnamed initializer that takes no parameters. If the class has a
base class, the default initializer will call the base class initializer that is unnamed with no
parameters. If no such base class initializer exists, it is an error. No further field
initialization is done beyond running the field initializers. If this would leave a field
unassigned, that is an compile error.
In a class with a base class, an initializer must either call a base class initializer or another
initializer on the class. Base class initializers are called using the init keyword as if it were
a method of the base class, i.e. base.init(arguments...). Named initializers are called by
following the init keyword with the initializer name, i.e. base.init.name(arguments...). Other
initializers on the class are called similarly using self, i.e. self.init(arguments...) or
self.init.name(arguments...). Using the implicit self reference when calling initializers is not
legal. The self keyword must explicitly be use. For example .init(arguments...) would be an
error. It is an error for self initializer calls to form a cycle.
Fields can be directly initialized from initializer arguments. This is done by prefixing the
parameter name with . and omitting the type. The type is determined by the type of the field.
public init(mut self, .field)
{
}
Just as any variable in a function must be definitely assigned before being used, any field must be
definitely assigned in an initializer before being used. All fields must be definitely assigned
before a reference to self can be shared outside of the initializer. This ensures that all
instances will be fully initialized before use. It also avoids the need to initialize all fields to
some safe default value before initialization begins. Instead, it is safe to run initialization on
uninitialized memory because it is guaranteed to be initialized before use. This is guaranteed by a
definite assignment analysis that divides the initializer body into two parts. The initial section
in which some fields may be uninitialized and the final section in which all fields, including
fields of subclasses are guaranteed to be initialized. The transition between these sections is the
point at which a base class initializer is called. If a base class initializer is explicitly called,
that call determines the transition point. If the base class initializer is implicitly called, or
there is no base class, the transition point occurs as the first point where every field is
definitely assigned. The compiler inserts the implicit base class initializer call there.
For the purpose of definite field assignment there are two kinds of initializers. Initializers that call another initializer of the current type are termed secondary initializers. Initializers that do not call another initializer of the current type are termed primary initializers. If this class subclasses another class, then its primary initializers will call a base class initializer either explicitly or implicitly.
When a primary initializer is called, it first executes all field initializers in textual order. Then any fields initialized from an argument using the initialization shorthand are initialized in argument order, left to right. The body of the initializer is then executed. The compiler divides the body of the initializer into two sections. In the first section, the self instance is considered to be not fully initialized. In the second section, the instance has been fully initialized. If there is an explicit call to a base class initializer, every statement after this call is the second section. If there is no explicit base class initializer call, the compiler determines the first statement at which every field has been definitely assigned. If this class is a subclass, it inserts an implicit call to the base class no-arg initializer at the point where all fields have been definitely assigned.
TODO: For classes without a base class (and structs), we may need a clear way to delineate the separation between these parts of the initializer.
In the not fully initialized section, the self reference may only be used to assign into fields or
access fields that have definitely been assigned. A reference to self may not be used or passed
to another function or method. At the end of this section, if any fields with optional types remain
uninitialized, they are implicitly initialized to none.
In the fully initialized section, it is guaranteed that every field of this class, its base class and any subclasses has been definitely assigned. Thus this section acts as a normal method with no special rules which may perform any additional initialization steps. The intention is that by this point, the instance should be initialized to a valid state. However, if it was not possible to put the object into the desired state, the rest of the initializer can do so.
When a secondary initializer is called, no field initializers are run. Instead, execution begins in
the initializer body. Before the call to the primary initializer, the self reference may not be
used. Thus no fields can be assigned or read. This is because the primary initializer will
initialize every field, so any initializations performed would be overwritten. There can, however,
be other statements before the call to the primary initializer for the purpose of preparing values
and state to call the primary initializer with. After the call to the primary initializer, all
fields have been definitely assigned and the initializer is treated as a normal method.
TODO: How does field initialization shorthand work with secondary initializers? Is it even allowed? The initialization shouldn't be run before the call to the primary initializer, instead it is like they should be passed along to the primary initializer. That may require special syntax or something.
Exceptions in initializers can be complex and easily lead to issues. For this reason initializers
are implicitly throws never instead of the default inferred throws. However, initializers can
throw exceptions if they are declared to throw them.
If an exception is thrown in a initializer, the drop method will not be run on the instance except as part of stack unwinding as described next. Exceptions thrown in or before a base or self initializer call simply unwind the stack, dropping any initialized field values as if they were local variables. Exceptions thrown after the completion of a base or self initializer call invoke the base class's drop method on this instance at the point of unwinding past the base class call (since it is now a fully constructed instance of the base class).
Note that the current class's drop method is never called so it is imperative that any resources be properly released by the initializer.
If you catch an exception from a self or base initializer call. You must re-throw it, because the current instance is not in a valid state.
The definite assignment rules lead to some real problems. For example, imagine a tree node class with left and right child node fields and a parent node reference. We must make the parent reference optional and mutable, and initialize it after the child. Otherwise, we have a problem initializing the tree even from in the parent initializer. The parent node needs to instantiate its two child nodes so that it can initialize its fields. However, to instantiate them it must provide a reference to itself. Yet, it is not completely initialized yet, so this is not safe. It might be possible for the reference capabilities to provide a way out of this. There could be a reference capability which does not allow for any methods or fields to be accessed but can only be stored with the promise that it would be valid in the future. However, the rules for this would probably be very complicated.