Initialisation in Plinth is more complicated than most other languages. Plinth enforces some strong constraints on initialisation to ensure that you never accidentally try to use something before it has been initialised. The main reason for these constraints is that Plinth has not-null types by default, and the only way of having a not-null variable is to initialise it.
There are two types of initialisation in Plinth: static and instance. Static initialisation happens before the main method is run, and instance initialisation happens when an object is created.
The most versatile form of static initialisation is a simple block that is executed as part of a type definition’s static initialiser. A static block can be defined as follows:
static {
// Do anything you want here.
}
This is sometimes useful if you need to populate a static data structure, but shouldn’t be used for anything too complex, as you can’t rely on static variables from other classes having been initialised yet.
When a type has multiple static blocks (and static variable assignments), they are run in the order that they are written in the type definition. Note that the order in which types are statically initialised is arbitrary.
A static variable assignment is treated exactly as if it were a normal assignment to a static variable inside a static block. They can be written as follows:
static string str = "hi";
static property double x = 12.345;
In Plinth, variables must always have a value before they can be used, whether it is a default (e.g. 0 or null) or an explicit assignment. For variables whose types do not have default values (e.g. not-null object types), this means an explicit value must always be provided. One of the most annoying consequences of this is that static variables (with a few exceptions) must always be nullable.
The reason for this is that static variables can be read at any time, including before they have been initialised (there are various things which make it impossible for the compiler to ensure that static variables aren’t read before they are initialised). If a static variable is read before it is initialised, it must always have some value, which in Plinth is the default value of the variable’s type. For most not-null types, this default value does not exist, and so the type cannot be used for a static variable.
Instance initialisation blocks are very similar to static initialisation blocks, but they run during object initialisation rather than static initialisation. They are also written similarly, but without the static keyword:
{
// Do some object initialisation here.
}
When a type has multiple instance blocks (and instance variable assignments), they are run in the order that they are written in the type definition, and together make up the instance initialiser. The point at which the instance initialiser is run is discussed in the Constructors section.
Similarly to static variable assignments, these are treated exactly as if they were normal assignments inside instance blocks. They can be written as follows:
string str = "hi";
property uint random = 4;
Constructors initialise objects before they can be used anywhere else in the program. They are specified as follows:
create(string name, uint age) {
// Do some object initialisation here.
}
Inside a constructor, another constructor may be called using the following “delegate constructor” syntax (note that recursive constructors are not allowed):
create(string name) {
this(name, 20);
}
If a super-class has a no-argument constructor and no delegate constructor is called, then the no-argument constructor from the super-class will be implicitly called at the beginning of the sub-class’s constructor. However, if the super-class does not have a no-argument constructor, then one must be called explicitly during the sub-class’s constructor, as follows (this is another kind of delegate constructor):
create(string first, string last) {
super(first + " " + last);
}
Delegate constructors can always be called during a constructor, as long as there is another constructor to delegate to. Delegate constructor calls do not have to be the first statement in a constructor, they can do some pre-processing and then call a delegate constructor, or they can decide which delegate constructor to call:
create(string name, boolean foo) {
if foo {
super("Foo");
} else {
this(name);
}
}
The restrictions on delegate constructors are:
The reason for these restrictions is to enforce the semantics of both super-class creation and initialisers. A super-class constructor must be run exactly once as part of a constructor call, as must the current class’s initialiser. The super-class’s constructor is run when the delegate constructor is called, and the initialiser is run immediately afterwards as part of the same statement. If no delegate constructor is called, then both are run at the very beginning of the constructor. If a this() delegate constructor is called, then the super-class constructor and the initialiser are run somewhere during its execution, as required by these rules.
In order to prevent undefined behaviour, nothing outside its own constructor may have access to an object until that object is fully initialised. Here, fully initialised means that all fields with no default value have been set, all final fields have been set, all properties with constructors have been set, all constructors have been run, and all initialisers have been run, at every level in the inheritance hierarchy.
There are two main things that the language uses to enforce these properties: prohibiting access to this, and the concept of a selfish constructor.
Prohibiting access to this is a fairly obvious necessity, since unrestrained access would allow this to be assigned to a static variable which could be accessed by another thread, or after throwing an exception. One of the most common ways that this is used is to assign to or access specific fields inside the object; since this usage does not allow the object to escape the constructor, the syntax this.foo is allowed. One way of accessing this without stating it explicitly is to call one of its methods, which could in turn do anything with the this pointer; for this reason, calling methods which have access to this is not allowed until the object is fully initialised. The most annoying incarnation of this problem is that property getters and setters actually have full access to the object pointer; the workaround is to use a property constructor instead, which does not have the same capability.
Selfish constructors exist to combat an initialisation problem with inheritance: even if a constructor has finished all of its initialisation, the object will not be fully initialised until all of the subclasses (if any) have run their constructors. A selfish constructor is a constructor that cannot be called by subclasses’ constructors, and so has full access to the object pointer as soon as all of its own initialisation has finished. The only way that it can be called is if there are no subclass constructors left to run.
Properties have three methods: getter, setter, and create. create is the property constructor, which (if it exists) is called the first time the property is assigned to. Making the constructor separate from the setter allows the setter use the old value if it needs to.
While the static initialisers are running, a static property could be accessed by another class’s static initialiser before the property has been initialised. This means that all static properties must cope with the possibility of being read before they are assigned to. Similarly, another class’s static initialiser could try to set a property before it has been initialised by the property’s class. For this reason, non-final properties do not have constructors, only setters.
For final properties, we still have the problem that another class can read it before it has been initialised, but we no longer have to worry about other things setting it. The definition of final in Plinth is “Once it has been set, it cannot be set again”. This definition allows us some flexibility regarding default values, but has the drawback that you can’t rely on them having been set before static initialisation is finished. final properties do have constructors, but they do not have setters. Each static final property must be set exactly once during the static initialiser in order to call the property’s constructor.
If a property constructor exists, that property must be set either during its class’s non-static initialiser or during each of its class’s constructors. This ensures that the constructor is called exactly once, and allows the language to determine at compile time which assignments are actually constructor calls. It is usually possible to set a property again immediately after setting it for the first time, in which case the second assignment will generate a call to setter instead of create. However, it is an error to have an assignment which can be either a constructor call or a setter call depending on the control flow, so for example the following is disallowed:
property string str;
create(boolean foo) {
if foo {
str = "foo"; // Calls the property's create method.
}
str = "hello"; // Error: might be a call to either the create method or the setter.
}
One of the most important features of instance properties is that they can be overridden by subclasses, which makes the semantics of the property constructor more difficult to define. In the previous paragraph, the meaning of the property’s class is the topmost class in the inheritance heirarchy that has that property [1]. This means that only the topmost class will call the property’s constructor, and subclasses may only call the setter (and only if the property is not final), which ensures that the property constructor is called exactly once.
These rules mean that a property’s create method may be called during a super-class’s initialisation, before any of the fields in the current class have been initialised. Similarly, it could be called during initialisation of this class, after most of the fields in the current class have been initialised. For this reason, the property constructor runs in a context where it cannot assume anything about the current state of any of the variables in this class or any super-classes.
[1] | The property could also be defined in an interface, and only inherited by classes, in which case some classes could have that property without defining it explicitly. |