Properties

Properties provide abstraction of a types data by combining the concepts of Fields (to store data in a Class or Record) and Methods (to perform actions on that data) into a single entity.

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:

  1. Properties can be read/write or read-only (and in rare cases even write-only)
  2. Setter code can validate new values
  3. 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 forth benefit of properties is that their getters can generste 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 (write) statements:

property method 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;

The setter can be any Writable Expression (such as a field, another property or even a Discardable), or the name of a method that takes a single parameter of the right type:

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 begin/end block of statements can also be provided for either the getter or the setter. In this case, the value Expression 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
    SetIntrnalValue(value / 8 - 5);
  end;

Stored Properties

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 thye can be "upgraded" to use custom getters or setter later, without breaking binary compatibility of a type. Also, they still will 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.

Initializers

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.

Indexer Properties

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 matycxh 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 structurel 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[42];

Indexer properties can have more than one parameter (i.e. be multi-dimensional), and – different that Arrays – they can be indexed on any arbitrate type, ot just Integers.

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.

Default Indexers

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[0];  // same as myObject.Items[0];

Property Notifications

Non-indexed properties can optionally be markled 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.

How these notifications work and how they can be retrieved depends ion the underlying platform. Notifications are used heavily in WPF on .NET or with Key-Value-Observation (KVO) on Cocoa.

Please refer to the Property Notifications topic for more details on this.

Storage Modifiers (Cocoa)

On the Cocoa platform, the type of a stored property declaration can be amended with one of the weak, unretained or strong Storage Modifier keywords, with strong being the implied default.

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 strong.

Cocoa Only

Storage Modifiers are necessary on Cocoa only, to assist with Automatic Reference Counting (ARC). Thye can be specified on all platfroms, but have no effect when using GC.

Static Properties

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

Visibility

The visibility of properties is governed by the Visibility Section of the containing type the property is declared in, or the Visibility Modifiers applied to the property.

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

Virtuality

The Virtuality of properties can be controlled by applying one of the Virtuality Member Modifiers.

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 have optionally specify the read and/or 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

Other Modifiers

A number of other Member Modifiers can be applied to properties.

See Also