Fields
Overview
Fields are the mechanism used to manage state in classes. A single field encapsulates three concepts:
- Storage: a memory location to store a reference to the field's value
- Getter: a method to get the field
- Setter: a method to set the field
The key concept in Fantom is that all fields automatically use a getter and setter method (there are a couple exceptions to this rule we will discuss below). In Java fields are simply a storage location. Most Java programmers then wrap the field in a getter and setter method resulting in a huge amount of bloated, boiler plate code. Then all access is done through a method call which is not exactly the prettiest syntax for field access. C# is better in that properties are first class language constructs. However management of storage is still done via a field and all the same boiler plate code is required. These design patterns used in Java and C# don't capture programmer intent very cleanly. The vast majority of these accessors are boiler plate code that don't actually do anything other than get and set the field. Plus it requires multiple language constructs to model one logical construct - this makes documentation a pain.
Simple Fields
In the simplest case, field declarations look just like Java or C#:
class Thing { Int id := 0 Str? name }
In the example above id
and name
are instance fields on the class Thing
. We use the :=
operator to provide an initial value for the field which is automatically assigned before the constructor is executed. If an initial value is not provided, then all fields default to null
with the exception of value-types.
We access fields using the .
operator:
thing := Thing() thing.name = "bob" echo(thing.id) echo(thing.name)
In the code above, a getter or setter is not explicitly specified, so they are automatically generated by the compiler. All access to the field is through the getter and setter method. So the code thing.name
is really a method call to the name
getter method. This enables you to later add an explicit accessor without requiring a recompile of all the client code to your API.
You can also the ?.
operator to safely handle a null target. See safe invokes.
Note: if you don't provide a getter or setter on a non-virtual field, then the compiler will optimize field access internal to your pod. So inside your pod, accessor methods are only used if you declare explicit accessors. Outside your pod, accessor methods are always used for non-const fields. Abstract and virtual fields always use accessors.
Accessor Methods
The syntax for declaring a getter or setter is similar to C# properties:
class Thing { Int id := 0 { get { echo("get id"); return &id } set { echo("set id"); &id = it } } }
The accessor section is denoted by a {}
block after the initial value assignment (or after the field identifier if no initial value). You can declare both get
and set
- or if you just declare one, then the other is automatically generated.
Inside the setter, the it
keyword references the new value being assigned to the field. If you need to use it
inside a closure, then assign to a local variable:
set { newVal := it; 3.times { echo(newVal) }; this.&f = newVal }
The &
storage operator is used to access the raw storage of the field (think about like C dereference operator). The &
operator is used when you wish to access the storage location of the field without going through the accessor method. You can use the &
operator in any of your class's methods, but only the declaring class is permitted direct access using the &
operator (it is like storage is always private
scoped). Inside the accessor itself you are required to use the &
operator (otherwise you would likely end up with infinite recursion).
Note that :=
field initialization always sets storage directly without calling the setter method (see below for an exception to this rule).
Calculated Fields
In most cases, the compiler automatically declares storage memory for your field. However if the compiler detects that a field's storage is never accessed then no memory is allocated. We call these calculated fields. Calculated fields must declare both an explicit getter and setter which does not use the &
operator to access the field. Note that assigning an initial value using :=
implicitly uses the storage operator and will allocate storage.
Const Fields
The keyword const
can be applied to your fields to indicate that the field is immutable:
class Thing { new make(Int id) { this.id = id } const Int id }
In the example above, id
is an const instance field. Const instance fields can only be set in a constructor. They work a little different than Java final fields - you can set them as many times as you like (or let them default to null), but you can't assign to them outside of the construction process.
Fantom also allows const instance fields to be set during the construction process via an it-block:
class Thing { new make(|This f|? f := null) { if (f != null) f(this) } const Str name } t := Thing { name = "Brian" } // ok t { name = "Andy" } // throws ConstErr
Inside of it-blocks, the check for setting a const field is moved from compile-time to run-time. The compiler will allow any it-block to set a const field on it
. However, if an attempt is made to call the it-block outside of the constructor then ConstErr
is thrown. This gives you flexibility to build APIs which capture a snippet of code used to modify an immutable class.
Const instance fields may also configured during deserialization via the InStream.readObj
method.
The value of all const
fields must be immutable. This guarantees that all const fields reference an instance which will not change state.
Const fields do not use accessor methods. It is a compile time error to declare accessors on a const field. All field access is performed directly against storage.
Const fields cannot be declared abstract
or virtual
. However a const field can override a virtual method with no parameters. But const fields cannot override fields since that would imply the field could be set. Example of using this technique to use a pre-calcualted string for Obj.toStr
:
const class Something { new make(...) { toStr = "..." } override const Str toStr }
Static Fields
The keyword static
is used to create a static class level field:
class Thing { const static Int nullId := 0xffff }
The field nullId
is a const static field - there is only one global instance on the class itself. Const static fields can only be set in static initializers. Static fields must be const - this ensures thread safety. Static fields are accessed using the .
operator on the class itself:
Thing.nullId
Protection Scope
You can use the normal slot protection scope keywords with a field: public
, protected
, internal
, and private
. The default is public
if a keyword is omitted.
You can narrow the scope of the setter as follows:
class Thing { Int id { protected set } Str name { internal set { &name = it } } }
In the example above the fields id
and name
are publicly scoped. We've narrowed the scope of the id
setter to be protected. Because there is no method body for the setter of id
, then the compiler auto generates one. The declaration for name
shows narrowing the scope and declaring an explicit setter.
Only a protection scope keyword may be used with a setter. The getter is always the protection scope of the field itself by definition. It is a compile time error to use keywords like const
, virtual
, or abstract
on an individual getter or setter.
Readonly Fields
It is quite common to publically expose a field's getter, but to hide the setter. This can be done by narrowing the field's setter to be private
as the following code illustrates:
class Thing { Int id { private set } internal Int count { private set } }
Virtual Fields
Instance fields may be declared virtual which allows subclasses to override the getter and setter methods:
class Thing { virtual Int id := 0 } class NewThing : Thing { override Int id { get { echo("NewThing.id get"); return super.id } set { echo("NewThing.id set"); super.id = it } } } class AnotherThing : Thing { override Int id := 100 }
In this example Thing.id
is virtual which allows subclasses to override its getter and setter method (in this case Thing.id
accessors are auto generated by the compiler). The class NewThing
overrides the accessors for id
to add some tracing, then calls the superclass accessors. Note that the overridden field must specify the override
keyword and must be use the same type (covariance is not supported).
Overridden fields are not given their own storage, rather they should delegate to their superclass accessors using the super
keyword. In the case that an override requires its own storage, you should declare another field to use as storage.
In the example above, the class AnotherThing
overrides Thing.id
by providing a different default value. Since no explicit getter or setter is specified, the compiler automatically generates accessors. However the accessors for AnotherThing.id
call super.id
rather than access storage directly. Also note that when an overridden field specifies an initial value, the field is initialized via its virtual setter rather than direct storage.
Abstract Fields
A field may be declared abstract:
abstract class Thing { abstract Int id } class NewThing : Thing { override Int id := 0 }
An abstract field is basically just an abstract getter and setter that a subclass must override. Abstract fields have no storage and cannot have an initial value. Overrides of abstract fields are given storage accessed via the &
operator (this is different than overriding a normal virtual field).
Definite Assignment
Non-nullable fields must be assigned in all code pathes. Each constructor which doesn't chain to this
must set each non-nullable field which doesn't meet one of the following criteria:
- value type fields don't need to be set (Bool, Int, Float)
- fields with an initializer don't need to be set
- abstract or override fields don't need to be set
- native fields don't need to be set
- calcualted fields don't need to be set
Matching rules apply to static fields which must be set by all code paths in the static initializer.
As a special case, a constructor which takes an it-block as the last parameter allows definite assignment to be postponed to runtime. The it-block is required to set every non-nullable field or a FieldNotSetErr
is raised at runtime. Consider this example:
class Foo { new make(|This| f) { f(this) } Str name } Foo {} // throws FieldNotSetErr Foo { name = "Brian" } // ok
Overriding a Method
A field's getter may be used to override a virtual method which has no parameters:
abstract class Named { abstract Str name() } class Person : Named { override Str name }
In the example above Named.name
is a virtual method that returns a Str
. The class Person
overrides the Named.name
method using a field. The override of Named.name
maps to the getter of Person.name
. Note that the field must specify the override
keyword and must have a matching type. Field overrides of a method may use covariance - the field may be declared using a more specific type than the method return type.
This design pattern is a handy technique to use in your Fantom code.
Mixins
A mixin can only declare const static fields and abstract fields. Mixins with abstract fields are basically declaring a getter/setter signature. When implementing a mixin with abstract fields, it is the subclass's responsibility to provide an implementation of the field. Field access of mixins uses the standard .
dot operator too.
Native Fields
Native fields are implemented in an alternate language which is "native" for each target platform. Native fields are typically written in Java for the Java VM and C# for the .NET CLR. Native fields use the native
keyword and follow similiar rules to absract fields - no storage or accessors. The infrastructure for supporting native fields is discussed in the Natives chapter.