References¶
A reference is a temporary, non-owning handle to an object that already exists elsewhere. It allows a function to read an object without copying it, or to modify an object belonging to its caller, as seen in the chapter on functions. References have appeared in passing in earlier examples; this chapter sets out the rules that govern them. Those rules are unusually strict: by confining references to a single role, Termina keeps the checking of memory safety simple enough to be carried out without the elaborate machinery that more permissive languages require.
The two kinds of reference¶
A reference is either immutable or mutable. An immutable reference, written
&T, grants read-only access to an object of type T. A mutable reference,
written &mut T, additionally allows the referenced object to be modified. A
reference is created by applying & or &mut to an object, and the object it
refers to is reached through the dereference operator *:
bump(&mut counter); // pass a mutable reference to counter
inspect(&reading); // pass an immutable reference to reading
When a reference refers to a structure, its fields are reached with the arrow
operator ->, as described in the chapter on types, rather than with the dot
operator used for direct values.
References are second-class¶
The central rule is that a reference may appear in exactly one position: as an argument to a call, whether to a function, a procedure, a method, or an action. A reference cannot be produced, stored, or named anywhere else.
Several consequences follow directly. A reference cannot be the type of a binding, so the following is rejected:
A reference cannot be the type of a structure field, nor the element type of an array:
And a function cannot return a reference: the only ways a value leaves a function are by return, which yields an owned value, or through a mutable-reference parameter.
The reason for confining references this way is that it makes their meaning
trivial to track. A reference exists only for the duration of the call it is
passed to, and it can only have come from the & or &mut expression written
at that call site. There is never any question of where a reference came from or
how long it remains valid, and so the analysis that guarantees the absence of
dangling references and aliasing errors needs none of the lifetime annotations,
regions, or reborrowing rules found in languages where references are
first-class values.
Slicing arrays¶
It is often necessary to pass not a whole array but a contiguous portion of it.
A slice, written array[lower .. upper], denotes the run of elements from
lower up to but not including upper, so that buf[1 .. 3] denotes the two
elements at indices 1 and 2. The bounds are of type usize, and literals
used as bounds may need the explicit annotation described in the chapter on
expressions.
Like a reference, a slice is not a value in its own right. It may appear only as
the operand of & or &mut, which together produce a reference to the selected
sub-array, and that reference is then passed to a function. The size of the
resulting reference is not computed from the difference of the bounds; it is
fixed by the type expected at the call site, and the transpiler checks that the
width of the slice matches that type. The following function expects a reference
to an array of two bytes, and the call supplies it by slicing a larger buffer:
In the generated code the slice becomes a pointer to the first selected element,
&buf[1U], while the number of elements is carried by the parameter type. Should
the width of the slice fail to match the size the context requires, the program
is rejected: supplying &buf[0 .. 3], a slice of three elements, where a
reference to two is expected, is reported as an invalid slice range. When the
bounds are compile-time constants, this check is performed during transpilation,
so a mismatched slice is caught before the program ever runs.