When accessed, a property behaves much like a field – it has a value that can be read and (optionally) updated. But unlike fields, accessing a property does not directly give unfettered access to the stored data in the class or record. Instead, access to a property goes through custom method-like getter and setter code.
This provides three main benefits:
- Properties can be read/write or read-only (and in rare cases even write-only)
- Setter code can validate new values
- Setter code can perform additional actions (such as updating related values)
Combined, these aspects allow classes (and records) to take control of the data by not allowing outside access to their fields, which any external code could modify in an uncontrolled manner.
In fact, it is considered good practise to have all fields of a class marked
private, so only the class's code itself can access them, and funnel all external modifications through properties (or regular Methods, of course).
A further benefit of properties is that their getters can generate or modify the returned value dynamically – so not every property necessarily maps directly to a value stored in a field.
Property Declaration Syntax
A simple property declaration consists of the
property keyword, followed by a property name and a (result) type, separated with a colon and optional getter (
read) and setter (
property Name: String read fName write SetNameAndUpdateView;
If only a getter or or only a setter is provided, the property will be read-only or write-only, respectively. If neither getter or setter is provided, the compiler will automatically provide a field for storage, and a simple getter and setter that uses that field. Such a property works much like a regular Field then, from a usage level.
The getter can be any Expression that returns the correct type. This could be a simple field access (as in the example above), a method, or a more complex expression:
property Name: String read fName; property Name: String read GetName; property Name: String read FirstName+" "+LastName; method GetName: String;
property Name: String read fName write fName; property Name: String read fName write SetName; property Name: String read fname write nil; method SetName(aNewName: String);
Alternatively, a full
end block of statements can also be provided for either the getter or the setter. In this case, the can be used to access the incoming value, within the setter code:
property Value: Double read begin result := GetInternalValue; result := (result + 5) * 8; end write begin SetInternalValue(value / 8 - 5); end;
As mentioned above, if neither a getter or setter are provided, the property will be read/write, and the compiler will automatically generate getters and setters that store and obtain the value from a (hidden) backing variable. In this case, the property behaves very much like a plain field:
property Name: String; // internally stored in a hidden String var
Different that an actual field, stored properties still are exposed via getter and setters, so they can be "upgraded" to use custom getters or setter later, without breaking binary compatibility of a type. Also, they will still support the
notify Modifier, and other property-specific features.
Stored properties can be marked with the
readonly Member Modifier to become read-only. Read-only properties can still be written to from an Initializer or from the class's Constructors – but they cannot be modified once construction of an instance has completed.
Like Fields, Stored Properties can be assigned an initial value right in their declaration by having the property declaration closed off with the
:= operator followed by an expression. Optionally, they can be marked with the
lazy Member Modifier to defer execution of the initializer until the first time the property is accessed.
property Name: String := 'Paul';
Please also refer to the Constructors: Initializers topic for more detail on when and how fields get initialized.
While regular properties represent a single value (of an arbitrary type, of course), indexer properties can provide access to a range of values of the same type. This is similar in concept to an Array, but each access – read or write – goes through the proper getter or setter code.
An indexer property is declared by providing one or more parameters after the property name, enclosed in square brackets. Indexer properties cannot be stored properties, so either a
read or a
write statement is required.
property Items[aIndex: Integer]: String read ... write ...;
The rules for read and write statements are similar to regular properties: The name of the indexer parameter(s) may be used in the statements, and if the name of a getter of setter method is provided, the signature of this method must include parameters that match the property's parameters:
property Items[aIndex: Integer]: String read fMyArray[aIndex]; property Items[aIndex: Integer]: String read GetItems wrire SetItems; method GetItems(aIndex: Integer): String; method SetItems(aIndex: Integer; aValue: String);
Of course, an indexer property does not necessarily have to be backed to an array-like structure, it can also generate (or store) values more dynamically. In the example below, the
IntsAsStrings could be accessed with any arbitrary index, and would return the approriate string.
property IntsAsStrings[aIndex: Integer]: String read aIndex.ToString; ... var s := myObject.IntsAsString;
Note that, also unlike arrays, indexer properties themselves have no concept of a count, or a valid range of parameters. It is up to the type implementing the property to provide clear semantics as to how an indexer can be accessed. For example, a List class indexed with integer indices might expose a separate
Count property, while a dictionary would allow arbitrary indexes – and might decide to raise an exception, or return
nil for values not in the dictionary.
One indexer property per class (optionally overloaded on type) can be marked with the
default Member Modifier. The default property can then be accessed by omitting the property name accessing the indexer off an instance (or type) itself;
type MyClass = public class public property Items[aIndex: Integer]: String read ...; default; ... var s := myObject; // same as myObject.Items;
Non-indexed properties can optionally be marked with the
notify Member Modifier. Properties marked with
notify will emit special platform-specific events whenever they are changed – allowing other parts of code to be notified about and react to these changes.
Storage Modifiers (Cocoa)
property Value: weak String;
To specify a Storage Modifier, the type cannot be inferred, but must be explicitly specified. Inferred types will always be considered
Like most type members, properties are by default defined on the instance – that means the property can be called on and will execute in the context of an instance of the class. A property can be marked as static by prefixing the property declaration with the
class keyword, or by applying the
static Member Modifier:
class property Name: String; // static property on the class itself property Name2: String; static; // also a static property on the class itself
By default, both getter and setter of the property are accessible on that visibility level, but visibility can be overridden by prefixing either the getter or the setter with a separate visibility keyword:
property Name: String read private write; // readonly for external access
property Name: String read; abstract;
Properties can be marked as abstract, if a descendant class must provide the implementation. Abstract properties (and properties in Interfaces may not define a getter or setter, but they can optionally specify the
write keywords to indicate whether a property can be read, written or both:
property A: String; abstract; // read/write property B: String read; abstract; // read-only property C: String write; abstract; // write-only property D: String read write; abstract; // also read/write
A number of other Member Modifiers can be applied to properties.
copyObjc only: Creates a copy of the value when setting.
default(See Default Indexers, above).
deprecatedMakes an event deprecated.
ISomeInterface.SomeMember(See Explicit Interface Implementations).
ISomeInterface(See Explicit Interface Implementations).
inlineMakes the accessors inline in the caller.
lazy(See Lazy Initializers, above).
lockedLike locked on, with self as an expression.
Expressionputs a lock around the accessors body, locking on Expression.
notify(See Property Notifications, above).
optional(Interface members only).
readonly(See Read-only Stored Properties, above).
unsafeAllows the use of unsafe types in the property signature and body.