Initialization
Every binding holds a value before anything reads it. A let gets its value where it is declared. A var may be declared without one, and what happens then depends on its type:
var-declaration = "var" Name ":" type ";"
| Declaration | When T … | The variable starts as |
|---|---|---|
var value: T; | has an accessible constructor T() | the value T() returns |
var value: T; | has no such constructor | nothing — it must be assigned before it is read |
let value: T; | — | an error: immutable variable requires an initializer |
There is no hidden default. A number declared without a value is not zero; it holds nothing until the program says what it is.
A default constructor runs
When the type has a constructor that takes no arguments, var value: T; calls it, and the variable is initialized from the start:
struct Tally {
count: int32;
}
extend Tally {
func Tally() -> Tally {
return Tally { count: 10 };
}
}
var tally: Tally;
tally.count += 1;
PrintLine("{}", tally.count);
This prints 11. The += reads count before changing it, and that read is fine: Tally() has already run.
Tracked storage
Without such a constructor, the variable holds nothing, and the compiler tracks it. Every path from the declaration to a read must assign it first:
func Grade(score: int32) -> char8[..] {
var grade: char8[..];
if score >= 90 {
grade = "excellent";
} else if score >= 50 {
grade = "pass";
} else {
grade = "retry";
}
return grade;
}
Each branch assigns grade, so the return is accepted. Remove the final else, and a score below 50 reaches the return with nothing assigned:
error: value 'grade' may be uninitialized on some control-flow paths
help: initialize or preserve 'grade' on every path before this use
A read with no assignment before it at all is reported directly:
var total: int32;
PrintLine("{}", total);
error: variable 'total' is used before it is initialized
help: assign a value to 'total' before this use
One part at a time
A value of a plain type — one with nothing to destroy, such as a structure of numbers — may be filled in one part at a time: field by field, tuple element by tuple element. A part that has been written may be read at once. The whole value counts as initialized once every part has been written on every path that reaches the use:
var p: Point;
p.x = 1;
p.y = 2;
PrintLine("{} {}", p.x, p.y);
Before p.y is written, reading it — or using p whole — is refused, with a note that names what has been written:
error: variable 'p' is used before it is initialized
note: only 'p.x' has been written, not 'p.y'
A fixed-size array of plain elements counts as initialized from its declaration, so a loop can fill it:
var values: int32[3];
for i in 0..3 {
values[i] = (i as int32) * 10;
}
A value with a destructor is written whole
A type that needs destroying — one with a destructor, or with a field that has one — cannot be built a part at a time. A part written into a variable that holds no value would belong to no whole value, and nothing would ever destroy it:
var holder: Holder;
holder.handle <- Handle { code: 1 };
error: cannot write field 'handle' of 'holder', which holds no value
help: initialize 'holder' whole, as in 'holder = Holder { ... }'
Assign such a variable whole — holder = Holder { … }; or holder <- other; — and from then on its parts can be replaced like any other place. Taking the address of a part, @holder.handle, is refused for the same reason.
Taking the address initializes
Taking the writable address of a whole uninitialized variable with @ counts as initializing it, because the address exists to be filled through. The compiler trusts the function that receives it to write a complete value:
func Fill(target: *var Point) {
*target = Point { x: 3, y: 4 };
}
var q: Point;
Fill(@q);
PrintLine("{} {}", q.x, q.y);
This is how a variable is handed to a C function that fills it in — see FFI.
After a move
A variable that has been moved from holds nothing again, as if it had just been declared. Reading it is an error — for a variable named moved, error: value 'moved' is used after it was moved — until it is assigned whole once more.
See also
- Bindings —
let,varand their types - Constructors — the
T()thatvar value: T;calls - Destructors — why some values are written whole
- Copy and move — what a move leaves behind
- Learn: Initialization
Constants
const declarations: values the compiler computes once, what their initializer may contain, where they may appear, and their use as array sizes.
Destructuring
Binding several names at once with tuple and structure patterns in let and var, discarding with _, and why such patterns must be irrefutable.