References
A reference lets code use a value that belongs to someone else, without copying it and without taking it over. &T is a shared reference: it can read the value. &var T is an exclusive reference: it can read and write it. A reference never owns its referent, is never null, and lives no longer than the call or the block that made it.
reference-type = "&" type
| "&" "var" type
struct Account {
owner: char8[..];
balance: int;
}
func Report(account: &Account) {
PrintLine("{}: {}", account.owner, account.balance);
}
func Deposit(account: &var Account, amount: int) {
account.balance += amount;
}
func Main() -> int {
var alice = Account { owner: "alice", balance: 10 };
Deposit(alice, 5); // borrowed for writing: alice.balance is now 15
Report(alice); // borrowed for reading
return 0;
}
References are the safe way to share. A raw pointer is an address the program vouches for itself; a reference is one the compiler checks, under the rules on Exclusivity.
Borrowing is implicit
There is no borrow operator. A reference is made — the value is borrowed — when a place is given where a reference is expected:
- an argument for a parameter of type
&Tor&var T; - a receiver: calling a method declared with
self: &Torself: &var Tborrows the value it is called on; - a binding annotated with a reference type:
let shared: &Account = alice;.
The call site looks the same as passing by value; the parameter's type decides. & is never an operator, and writing it before a value is an error that points to @, which takes a raw address:
error: '&' does not take an address; write '@' to take the address of a value
What is borrowed must be a place — a variable, a field, an element, or the referent of another reference. A literal or a freshly built value has nowhere to be borrowed from, so Report(Account { owner: "x", balance: 1 }) and Bump(5) — Bump is defined below — are rejected:
error: argument 1 to 'Report' has type 'Account', but parameter 'account' requires '&Account'
error: argument 1 to 'Bump' has type 'int', but parameter 'count' requires '&var int'
An exclusive borrow also needs a place that may be written. With let savings = Account { … };, Deposit(savings, 10) is rejected:
error: argument 1 to 'Deposit' cannot borrow immutable 'savings' as '&var Account'
help: declare 'savings' with 'var' to make it mutable
A &var T is accepted wherever a &T is expected: an exclusive reference can always lend reading.
Using a reference
A reference is used as if it were the value. Fields, methods, indexing and .length all reach through it with no * and no arrow:
func First(values: &int[3]) -> int {
return values[0] + values.length as int;
}
A reference to a scalar — an integer, a float, a bool, a character — supplies the scalar's value wherever a value is expected: in arithmetic and comparisons, in a condition, in a cast, in an assignment or a typed binding, as a by-value argument:
func Twice(value: &int) -> int {
return value * 2;
}
To copy a scalar out of a reference, give the binding its type: let saved: int = value;. * is a raw-pointer operator and never applies to a reference:
error: operator '*' requires a pointer operand, but found '&var int'
note: a reference reads and writes its referent without '*'
help: write 'r' in place of '*r', as in 'r += 1'
Writing through &var
Through a &var T the referent can be written in three ways.
Fields and elements are written as usual: account.balance += 5, values[i] = 0.
A scalar referent is written by assigning to the reference itself. =, <-, the compound assignments and ++/-- all store into the caller's value:
func Bump(count: &var int) {
count += 1;
}
func Main() -> int {
var total = 4;
Bump(total); // total is now 5
let alias: &var int = total;
alias = 20;
alias++; // total is now 21
return 0;
}
Any other referent — a struct, a tuple, an array, a value of a type parameter — is replaced whole with = or <-. This is how a method replaces the value it was called on:
extend Account {
func Reset(self: &var Account) {
self = Account { owner: self.owner, balance: 0 };
}
}
The write follows the assignment rules for T exactly as an assignment to a var local would: the new value is produced first, the old one is destroyed, and the new one is installed; a move-only value needs <-. Compound assignment and ++/-- apply to scalar referents only.
Through a shared reference every write is an error:
error: cannot modify data through immutable reference '&Account'
Rebinding a reference
A reference binding declared with let always writes through. One declared with var is different for plain =, which points it at other storage instead:
var first = 1;
var second = 2;
var current: &int = first;
current = second; // current now refers to second; first is unchanged
Assigning a value of the referent's type to such a binding is therefore an error. With var counter: &var int = total;, counter = 5; fails:
error: cannot assign 'int' to '&var int'
note: '=' points the 'var' reference 'counter' at other storage
help: declare 'counter' with 'let' to write through it
Where a reference may appear
A reference is a non-owning alias for the length of a call or a block, so it may appear in exactly three places: a parameter, a receiver, and a local binding. It cannot be stored in a field, returned, or moved out of — a field r: &int in a struct Holder, a function returning &Account, and let mine <- account; on a reference parameter are rejected in turn:
error: field 'r' in struct 'Holder' cannot store reference type '&int'
note: references are non-owning aliases and cannot escape into aggregate storage
help: store the owned value or a raw pointer when an address must outlive the borrow
error: function return type cannot store reference type '&Account'
error: cannot move a non-owning reference
note: references borrow storage but do not own the value they address
A reference cannot destroy its referent either: replacing a value through &var T installs a new one in its place. When an address must be stored or must outlive the call, use a raw pointer.
References and pointers
&T, &var T | *T, *var T | |
|---|---|---|
| Made by | an implicit borrow | @place |
| Null | never | null is a value |
| Read and write | as the value itself | *p; p.field and p[i] reach through |
| Arithmetic | none | p + n, p - n, p[i] |
| Stored in a field, returned | no | yes |
| Checked | exclusivity, writability | writability only |
A reference is 8 bytes, the size of the address it holds.
See also
- Exclusivity — how many references may exist at once
- Parameters —
&Tand&var Tparameters - Methods — receivers declared as
self: &Tandself: &var T - Pointers — raw addresses, for what a reference cannot do
- Learn: Reference, Mutable reference, Mutating method