Properties and indexers

Properties and indexers present field-like and indexed syntax while routing reads and writes through accessor bodies.

Properties

A property declares a type and a name followed by a get accessor, a set accessor, or both. The setter receives the assigned value through the implicit name value.

struct Player
{
    private int health;

    public Player(int initialHealth)
    {
        health = initialHealth;
    }

    public int Health
    {
        get { return health; }
        set { health = value; }
    }
}

Player player = Player(100);
player.Health = 80;
int health = player.Health;

A property is an accessor abstraction, not an implicit field. It occupies no additional space in the struct layout; storage is explicit in fields such as health.

Getter-only properties

Omit set when callers should be able to read a computed value without assigning to it.

public int readonly Health
{
    get { return health; }
}

The readonly modifier also permits the getter to be called through a readonly struct receiver. The getter obeys the readonly effect contract, including the restrictions on hidden state and calls to non-readonly members.

The canonical form places the property modifier after the complete property type, immediately before the property name. The older leading form, such as readonly int Health, remains accepted for by-value properties where it is unambiguous.

A readonly function may use a getter or setter without this modifier on a mutable local object or an object supplied through a mutable pointer/reference parameter. The compiler checks the accessor body with that object's origin: writes to its local or explicitly supplied fields are allowed, but hidden static/global writes are not. The same rule applies to indexers and compound assignments. Accessors may call non-readonly instance methods on permitted receivers when those bodies also pass contextual checking. Setters through readonly receivers and non-readonly free/static calls remain forbidden. See contextual member checks.

Readonly result types and readonly getters

Readonly access through the returned type and readonly access to the property receiver are independent. This is the same rule used by methods: readonly byte* readonly GetCString() and readonly byte* readonly CString both place the result-type qualifier before byte and the callable/member modifier immediately before the name.

DeclarationResultProperty/getter
readonly byte* CStringReadonly bytes through a mutable pointer bindingMutable
byte* readonly CStringMutable bytes through a mutable pointerReadonly
readonly byte* readonly CStringReadonly bytes through a mutable pointer bindingReadonly
public readonly byte* readonly CString
{
    get
    {
        readonly byte* pointer = &buffer[0];
        return pointer;
    }
}

Use the same placement for indexers, for example public readonly byte* readonly this[int index]. Interfaces and structural template requirements use the same property and indexer syntax.

Indexers

An indexer uses this[parameters] and the same getter/setter model. Reading invokes get; assignment invokes set with the assigned value available as value.

struct Buffer
{
    private int* data;

    public int this[int index]
    {
        get { return data[index]; }
        set { data[index] = value; }
    }
}

buffer[5] = 10;
int item = buffer[5];

Multiple parameters and overloads

An indexer may accept multiple parameters, with no language-defined maximum parameter count. A struct may declare several indexers when their parameter type lists differ; normal overload resolution selects the matching declaration. Receiver mutability is also part of an indexer's overload signature, so a mutable and a readonly indexer may use the same parameter types.

public float this[int x, int y]
{
    get { return GetValue(x, y); }
    set { SetValue(x, y, value); }
}

float sample = matrix[4, 7];
matrix[4, 7] = 1.0f;

User indexers are distinct from built-in rectangular array indexing, which requires one integer index for each dimension of the array.

Mutable and readonly indexer overloads

A mutable receiver prefers the mutable overload when both declarations accept the arguments. A readonly receiver cannot use a mutable overload, so only an eligible readonly indexer can be selected. The same rule applies to structs, interfaces, inherited indexers and structural template requirements.

struct Vector<T>
{
    private T* items;

    public T* this[int index]
    {
        get { return &items[index]; }
    }

    public readonly T* readonly this[int index]
    {
        get { return &items[index]; }
    }
}

Vector<int> mutable = ...;
readonly Vector<int>& immutable = mutable;

int* a = mutable[0];
readonly int* b = immutable[0];

The destination type does not drive overload resolution. For example, readonly int* value = mutable[0]; still selects the mutable indexer first; its int* result then widens to readonly int* through the normal pointer conversion.

Return type is not an overload discriminator

Two mutable indexers with the same parameter types are duplicates even if one returns T* and the other returns readonly T*. The same is true for two readonly indexers. The distinguishing signature is receiver mutability plus the index parameter types, not the result type.

Compound assignments

Compound assignments preserve accessor behavior. An expression such as player.Health += 5 performs one property read and one property write; index arguments are evaluated once for an indexed compound assignment.

player.Health += 5;
buffer[index] -= 1;