Initializers and finalizers are specialized methods a type may optionally define. Their use bookends the life of a type's objects. Initializers help create new objects. Finalizers help destroy them.
Many types don't need initializer or finalizer methods. For those that do, here's how to define them.
Initializer methods offer an alternative to type constructors for creating new values of some type. Type constructors have the limitation that they can only specify initial values for public fields of a new object. Initializers offer more flexibility, as they can use any sort of logic for initializing a new object's fields, even private fields.
Initializers are useful when the use of a type constructor would be:
- Insufficient, because initialization needs to perform some logic that is more than simply assigning value(s).
- Impossible, because parts of the type are private and can only be accessed by methods of the type.
- Inconvenient, because the literal would be unnecessarily verbose in comparison to using an initializer.
Here is a simple initializer:
fn init(self &new) *self = Self[x: 1., y: 1., z: 1.]
An initializer is a method, but it has a few differences from typical methods:
- The default name is init. Initializers can have any name, but the benefit of the default name is that you need not specify the initializer's name when using it to create a new value. As with any method, multiple initializers may be defined (even with the same name).
- self is a borrowed reference using the new permission. Here's why: An initializer is called when memory has been allocated for the new object, but it does not yet hold a valid value. The borrowed reference effectively points to this location. It uses the new permission to signal that the reference does not yet point to a valid value. This prevents the initializer from accessing any of the fields until after they have been initialized.
- Initializers don't return a value. After the initializer has finished running, the code that invoked the initializer will automatically be given an owning reference pointing to the same location as self. Thus, having the initializer return a value would be superfluous and confusing.
- The initializer's logic uses a type constructor.
Essentially, an initializer's logic performs these steps in order:
- Figure out what the values should be for every field (if needed). The fields of self are inaccessible at this point, as self is not pointing to a valid value. This means it is also impossible to call any other methods on self.
- Use a type constructor to initialize the entire value of self at one time (as the above example does).
- Perform any additional work on the value that the logic requires, if needed. At this point, the initializer may access the value's fields or call any other method on the value.
The following code invokes the initializer defined above:
imm gcref = &gc mut Point() // x,y,z of the new Point value are 1.0
As discussed earlier, it is not always obvious how to create a faithful copy of some types' values. When this difficulty exists (e.g., a value that contains references to other values), a type may define a clone method whose logic is capable of creating a faithful copy.
A clone method is essentially an initializer whose second parameter is a value or another borrowed reference to the value we want to copy:
fn clone(self &new, from &) *self = Self[x: from.x, y: from.y, z: from.z]
Whenever copies are automatically made (e.g., assignment or function calls), the clone method will be used, if specified, to make these copies.
When a object is destroyed at the end of its life, its finalizer logic, if defined, is automatically invoked. Only types that acquire a dependent relationship with other system object(s) need to define a finalizer. The finalizer releases, gives back, or safely breaks all such dependencies (e.g., closing file/network handles, de-subscribing to services, etc.) before the value's memory is reclaimed.
Here's a finalizer:
fn final(self &uni) file.close
Key to a finalizer definition:
- The method's name must be final. Only one may be defined.
- self is a borrowed reference using the uni permission. No other parameters should be specified.
- Finalizers don't return a value.
- The logic may access the value and call methods. However, a finalizer's logic should run quickly without blocking, while still breaking all dependencies. After the finalizer finishes, the object will cease to exist.