Properties
A property is a struct member that is read and written like a field
(account.name, account.name = "Savings") but whose get and set
accessors can run code. A property that needs somewhere to keep its value
gets it from the compiler: its implied storage, reachable as field
inside its accessors. You declare a separate private field only when you
want one.
Declaring properties
A property is a field declaration followed by an accessor block instead of
;:
# A property is read and written like a field, but its `get` and `set`
# accessors can run code.
struct Account {
# An auto property: implied storage, read and written by anyone.
var owner: String { get; set; }
# Read-only outside the struct: only Account's own code assigns it.
var balance: long { get; }
# A custom `set` over implied storage: `field` is the storage and
# `value` is the new value.
var name: String {
get;
set {
assert(!value.is_empty, "an account name cannot be empty");
field = value;
}
} = "unnamed";
# Computed: no accessor is omitted and none uses `field`, so it takes
# no space.
var is_empty: bool {
get {
return balance == 0;
}
}
# Explicit storage, when you want it.
var _deposits: ulong;
var deposits: ulong {
get {
return _deposits;
}
}
# An initializer initializes the implied storage.
var limit: long { get; set; } = 100;
construct(this.owner = "") {}
deposit(amount: long) {
balance += amount; # Account's own code may set a get-only property
_deposits += 1;
}
}
main(): int {
var account = Account("Ann");
assert(account.owner == "Ann", "a constructor parameter can set a property");
assert(account.is_empty, "a computed property runs its get");
account.owner = "Bob";
account.name = "Savings";
assert(account.name == "Savings", "the set stored the name");
account.deposit(30);
account.deposit(12);
assert(account.balance == 42 && account.deposits == 2, "methods update the storage");
assert(!account.is_empty, "the computed property follows the balance");
# `account.balance = 0;` would not compile: 'balance' has no 'set'.
account.limit += 50; # a get, then a set
assert(account.limit == 150, "compound assignment reads and writes");
return 0;
}
get;andset;with no body are the defaults:getreturns the storage, andsetstores the new value in it. Either may have a body instead.- A property has implied storage when an accessor is omitted (
get;orset;) or an accessor body usesfield. Otherwise it is computed: it takes no space, and itsgetruns every time it is read. - An initializer after the accessors (
var limit: long { get; set; } = 100;) initializes the storage, so it needs a property that has storage. It does not runset. - Inside an accessor,
fieldis the storage, and insideset,valueis the new value (a by-value parameter). Both are contextual: outside accessors they are ordinary names, so a local or a member calledfieldorvalueelsewhere keeps working. Inside an accessor, a barefieldalways means the storage; reach a member of that name asthis.field. - A property must have a
get:{ set; }alone is an error. - A property is declared with
varand an explicit type; it cannot beconst. There are nostaticproperties.
Reading and writing
p.countcallsget, andp.count = xcallsset. A default accessor is not a call at all: an auto property ({ get; set; }) compiles to the same loads and stores as a field.p.count += 1,p.count++and the other compound assignments are agetand then aset.pis evaluated once.- A constructor parameter
this.nameassigns the property, through itssetwhen it has one. A field initializer and the struct’s own field initialization do not. - Inside the struct’s methods, a property is reached by its bare name, like a
field:
balance += amount;isthis.balance += amount;. - A property’s accessors are ordinary method calls for
reference counting:
getreturns a temporary that owns one reference, andvalueis a by-value parameter, acquired on entry and released on exit.
Read-only properties
{ get; } alone makes a property read-only outside the struct: assigning
it from other code is an error at the property’s name.
main.pep:6:7: error: cannot assign to 'balance': it has no 'set' accessor
6 | a.balance = 3;
| ^~~~~~~
main.pep:2:9: note: 'balance' declared here
2 | var balance: long { get; }
| ^~~~~~~
The struct’s own constructors and methods may assign a get-only property with implied storage; that writes the storage directly. An extension’s methods are not the struct’s own code: they cannot assign it either.
Properties in traits and extensions
A trait can require a property, var area: double { get; } or { get; set; }, with no accessor bodies. A struct meets
the requirement with a property of the same name and type (auto or
computed), or with a plain field. Reading or writing the property through a
trait value calls the struct’s accessors.
An extension can add computed properties only, since it cannot add storage to the struct:
# A trait can require a property; a struct meets it with a property of the
# same name and type.
trait Shape {
var area: double { get; }
var label: String { get; set; }
}
struct Square {
var side: double { get; set; } = 1.0;
var area: double {
get {
return side * side;
}
}
var label: String { get; set; } = "square";
}
extend Square with Shape;
# An extension can add computed properties, which need no storage.
extend Square {
var perimeter: double {
get {
return side * 4.0;
}
}
}
describe(shape: Shape): String {
shape.label = "seen";
return $"{shape.label}: {shape.area}";
}
main(): int {
var square = Square();
square.side = 3.0;
assert(square.area == 9.0, "the computed area follows the side");
assert(square.perimeter == 12.0, "an extension's property reads like any other");
assert(describe(square) == "seen: 9.0", "a trait value reaches the struct's accessors");
assert(square.label == "seen", "the set through the trait reached the struct");
return 0;
}
An extension property that would need storage is an error:
main.pep:5:9: error: an extension cannot add storage: property 'y' must be computed, with accessor bodies that do not use 'field'
An enum can declare computed properties too, under
the same rule, and no set, since its values never change.
Properties of [Shared] structs
The fields of a [Shared] struct are const, so the storage
of its properties is too:
- A property with implied storage must be
{ get; }, and only the struct’s constructors assign it. Aset, with or without a body, is an error atset, with a note at[Shared]. - A computed
getover the struct’sconstfields is allowed, as any method is.
A [Shared] struct’s properties are get only, by design: an assignment
x.p = v would read as a write to a struct whose fields cannot change. To
change what a thread-safe member holds (an Atomic<int>, or a value behind a
Mutex), declare a method that says what it does, such as
store_hits(value) or add_name(name).
How properties compile
The compiler lowers a property to plain members: a field for its storage
(named after the property when its get is the default, __field_name
otherwise), a getter method __get_name() and, with a set, a setter
__set_name(value). Every read and write of the property becomes a storage
access or an accessor call once the receiver’s type is known. The accessor
methods appear in debuggers and in profiles under those names.