Structs
A struct is a user-defined data type used to group
variables into a single type. Additionally, structs may define member
functions, that can be called on instances of the struct. Inside member
functions, this is a non-null reference
(T&) to the instance that the function was called on.
Use &this when a storable T* is required.
Member functions must be defined inside the struct or enum body.
struct Person {
string name;
int age;
Person*? friend;
void introduce() {
// Members may be accessed via 'this' or directly.
println(this.name, " is ", age, " years old");
}
}
void main() {
var alice = Person(name = "Alice", age = 30, friend = null);
var bob = Person(name = "Bob", age = 25, friend = &alice);
alice.friend = &bob;
alice.introduce(); // prints "Alice is 30 years old"
bob.introduce(); // prints "Bob is 25 years old"
}Static constants
A const member with an initializer is a constant scoped
under the type name, accessed as Type.constant. Enums
support them the same way. Unlike fields, static constants take no
storage in instances and are not parameters of the autogenerated
constructor.
struct Circle {
const pi = 3.14;
float radius;
float area() {
return pi * radius * radius;
}
}
void main() {
println(Circle.pi); // prints 3.14
println(Circle(radius = 1.0).area()); // prints 3.14
}Constructors
A struct without an explicit constructor gets an autogenerated one that takes each field as a parameter, positionally or by name:
struct Point: Copyable {
int x;
int y;
}
void main() {
var a = Point(1, 2);
var b = Point(x = 3, y = 4);
println(a.x + b.x); // prints 4
}Named arguments can be given in any order and can skip fields with default values. Argument expressions always evaluate in the order written.
To customize construction, declare a constructor: a member function
with the same name as the struct. It must initialize every field, using
this. to refer to fields shadowed by parameters:
struct Counter: Copyable {
int count;
Counter(int count) {
this.count = count;
}
}
void main() {
var counter = Counter(10);
println(counter.count); // prints 10
}A single-parameter constructor marked implicit also
converts its argument type, so a bare 5 works wherever a
Counter is expected:
struct Counter: Copyable {
int count;
implicit Counter(int count) {
this.count = count;
}
}
void greet(Counter counter) {
println(counter.count);
}
void main() {
greet(5); // prints 5
}Without implicit, constructors never convert: pass
Counter(10) explicitly instead of a bare
10.
The other direction works with an implicit member
function that takes no parameters and returns the target type:
struct Label: Copyable {
string text;
implicit string view() {
return text;
}
}
void main() {
var label = Label("hi");
string s = label;
println(s); // prints "hi"
}Implicit conversions apply when passing arguments, returning values, and initializing or assigning variables. Only one user-declared conversion applies per step, and overloads that match without converting win. If conversions exist in both directions at once, the use is ambiguous and fails to compile.
Destructors
A struct can declare a destructor, written ~ followed by
the struct name. It runs automatically when an instance goes out of
scope, which makes it a good place to release resources:
struct Logger {
string prefix;
~Logger() {
println(prefix, " closed");
}
}
void main() {
var _log = Logger(prefix = "app");
println("working"); // prints "working", then "app closed" at scope exit
}A destructor can also be invoked explicitly with
.deinit(). This doesn't replace the automatic call: the
destructor still runs again at scope exit, so never call it on owned
values such as locals.
Explicit destruction is for values the compiler won't destroy, such as elements in manually allocated memory. This is how containers destroy their elements:
void main() {
var buffer = allocateArray<StringBuf>(1);
var slot = &buffer[0];
slot.init(StringBuf("hi"));
println(buffer[0]); // prints "hi"
buffer[0].deinit(); // destroys the element; nothing will destroy it again
deallocate(buffer);
}