Structs, enums and unions
Reference: Structs, enums, and unions.
Structs#
pub struct Style:
pub let width: f64 = 1.0
pub var visible: bool = true
var cache: Layout*?- Fields are
let(set when the value is made, then fixed) orvar, and private to the module unlesspub. - Layout follows declaration order, as in C; see Types.
- A struct is a value. Assigning or passing it copies all its fields, as with a C struct or a Swift struct, and unlike a Python object, which is shared by reference. To share one struct between several places, pass a pointer.
- A struct cannot contain itself, directly or through an array or optional; a pointer to itself is fine (
next: Node*?). ==andhashcome automatically when every field supports them.- There is no syntax for copying a struct with some fields changed; copy it into a
varand assign the fields.
Construction#
Without an init of its own, a struct gets a constructor taking its fields in order or by name: Style(width = 0.5), Point(3.0, 4.0). A field may be left out when it has a default, or when its type has a zero value (numbers, bool, optionals, spans, ...), in which case it starts at zero. Every other field must be given. The constructor can be used outside the module only if the struct and all its required fields are pub.
Custom initialisers#
A method named init replaces the generated constructor. It must assign every field exactly once, and cannot read a field or call a method before all are assigned. It may fail, which makes construction fallible:
let out_of_range = ErrorCode.package(1)
struct Range:
let low: i64
let high: i64
func init(low: i64, high: i64) -> !:
if low > high:
error(out_of_range, "low is above high")
self.low = low
self.high = high
func contains(value: i64) -> bool:
return self.low <= value and value < self.high
pub func main(arguments: str[]) -> i32!:
let range = try Range(1, 10)
print(f"{range.contains(5)} {range.contains(10)}")
let fallback = Range(5, 1) catch failure:
print(f"refused: {failure.message}")
recover try Range(1, 5)
print(f"{fallback.low}..<{fallback.high}")
return 0true false refused: low is above high 1..<5
Another common pattern is a function of the type that builds a value, Point.origin() or Image.open(path), alongside or instead of init: a static func in the struct, called through the type.
Enums#
An enum's value is one of its cases. A case may carry values, as in Rust and Swift:
enum Token:
number(value: f64)
name(text: str)
end
func describe() -> str:
return match self:
.number(_) => "a number"
.name(_) => "a name"
.end => "the end"
pub func main(arguments: str[]) -> i32:
let tokens = [Token.number(value = 2.5), .name(text = "x"), .end]
for t in tokens: print(t.describe())
return 0a number a name the end
- A case is written
Token.end, or.endwhere the type is known from context. - A case's values are reached only by
match, which must handle every case. Adding a case makes everymatchthat does not handle it fail to compile, which shows you each place to update. - Enums can have methods, as above.
- The numeric value of a case is not visible; cases are not integers (use an integer-backed enum, below, for that).
nonecannot be a case name, because.nonemeans an empty optional.- An enum cannot contain itself by value through a case; use a pointer, as a tree's nodes do.
A var of an enum type with no value given starts as its first case, if that case carries no values.
Integer-backed enums#
An enum declared as an integer type, with a value for each case, is stored as that integer, as a C enum is. It is used for flags and for constants shared with C:
enum Access as u32:
empty = 0
read = 1
write = 2
execute = 4
pub func main(arguments: str[]) -> i32:
let mode = Access.read | Access.write
let allowed = (mode & Access.write) != Access.empty
print(f"{(u32)mode} {allowed}")
return 03 true
|,&,^and~combine cases into a value of the same enum type, like Python'senum.Flag.(u32)modegives the integer.Access(n)converts back and traps ifnis not one of the declared values;(Access)nconverts without checking.- Because a combination is not itself a declared case, a
matchover an integer-backed enum always needs a_arm.
Unions#
A union stores one of its members in the same memory, as a C union does, with nothing recording which one. It exists to match C's unions and to reinterpret bytes:
union Bits:
real: f64
integer: u64
pub func main(arguments: str[]) -> i32:
var bits: Bits
bits.real = 1.0
print(f"{bits.integer:x}")
return 03ff0000000000000
- Members are written
name: Type, withoutletorvar, and are private unlesspub. - A union starts zeroed. It is created by declaring it and assigning one member; there is no constructor.
- Reading a member other than the last one written reinterprets the bytes. That is undefined when the member's type has rules about its bytes: reading a
bool, an enum, a pointer, astror an optional that was not the last member written. - A union may have methods, but cannot implement an interface.
To store "one of several things" safely, use an enum: it records which case it holds.