A struct holds one or more named values (fields), each with its own type. The struct's fields are located together in memory. A field's value can be individually accessed using the field's name.
Let's illustrate by declaring a new struct type:
struct Complex: real f32 imag f32
This defines a new struct type called Complex that holds two fields of data. The fields named real and imag are both 32-bit floating point numbers.
Variables can be declared and initialized using this new type, each with its own values in these two fields:
imm comp1 = Complex[real: 3.2, imag: 0.0] mut comp2 = Complex[real: 0.0, imag: 1.0]
Before we can make use of a specific data structure, we need to declare it, giving it a name and specifying its fields:
struct Point: x f32 = 0. y f32 = 0.
The struct's declared name is effectively new, custom-defined type. This type name can be used to declare any number of variables containing values of this type (e.g., imm pt1 Point).
Field declarations look like variable declarations, specifying the field's:
- Permission. The actual permission that governs field access is derived from both the field's and struct's permissions using a mechanism called viewpoint adaptation. If unspecified, mut is assumed, which effectively means the struct value's permission governs access.
Each field must be given a unique name.
However, names used in one struct will not be confused with same-named
fields in other types, nor with variable or function names.
Field names that begin with an underscore are considered pr\ivate. Private fields can only be accessed by the struct's methods. Fields should be private if we don't want logic outside the struct's methods to depend on this implementation detail or be able to change its value directly. This is particularly important when the field's value should be subject to additional invariant constraints that are properly enforced by the struct's methods.
The anonymous name _ may be used for any data segment of the struct that we don't want or need access to.
- Type. This can be any type: numeric, Bool, an array, some other struct, etc. The name Self may be used to refer to the struct type currently being defined. There is one important restriction: A field's type may not be the same as the struct type we are declaring (or any recursive variation on this). A field's type, however, may be a reference to the struct type.
- Default value. The default value must be a constant. Specifying default values makes initialization of a newly created struct more convenient.
The initial value for any struct value is always built (explicitly or implicitly) using a struct constructor: the struct name followed by comma-separated field values in square brackets. Values may be marked with field names or just listed in declared order:
imm pt = Point [x: 3., y: 4.] // Using field names imm pt2 = Point [3., 4.] // When field names are omitted, order matters. imm pt3 = Point // Uses x,y's declared default values of 0.
Several constraints apply to struct constructors:
- The constructor must specify a value for every field lacking a default value.
- Only the type's methods may use the struct constructor, if the struct has any private field lacking a default value.
It is also possible to initialize a new struct value by using one of the struct's defined initializer methods instead. Under the covers, initializer methods also use the struct constructor to initialize the struct's value.
Like numbers, struct values may be passed around between variables and functions:
fn main(): mut pt1 = Point pt1 = change(pt1) fn change(mut pt Point) Point: pt = Point[1., 2.] pt
Copies of the data structures are made as they are passed around (e.g. into and returned by change). Any changes made to a copy won't change the original struct value. This may not always be the behavior that you want, though. If a struct is too big to pass around efficiently, or you want changes to apply to the source structure, make use of references to a struct instead.
Use the . operator to access a struct field's value, specifying the struct value on the left and the field name on the right:
imm dist = (pt.x*pt.x + pt.y*pt.y).sqrt
A field's value may also be altered in this way:
pt.y = pt.x
The compiler applies field privacy and permission constraints to prevent invalid field accesses. A private field can only be accessed by the type's methods. Changing of a field's value is only allowed if the struct/field permissions allow mutability.
It is also possible to use the & operator to borrow a reference to a specific field within a struct value.
It is not possible to check whether two structs have the same value if the struct defines only fields. However, two struct values may be compared if the struct implements the comparison methods. Also, it is possible to perform partial matching of a struct against some pattern.
It is perfectly legal to define a struct with no fields. Nearly always, such types may be used like any struct type for variables, fields, parameters, etc.:
- Although its values take up no space, empty struct values can nonetheless be passed around.
- Methods may be defined for an empty struct and used by its values.
The most significant restriction is that it is not possible to allocate a zero-sized value on the heap, which means you cannot create an owning reference to a value of an empty struct.
The language's pre-defined void type is defined internally as an empty struct.
Sometimes it is valuable to work with types where we have no idea about the size or layout of any values of that type. This is useful when interfacing with some external system which knows the layout and size of the type's data, but is reluctant to share (or we don't really care).
We call such types opaque, because we cannot directly view their contents. Declaring an opaque type is easy:
struct @opaque SocketHandle // don't specify any fields as part of the type // Methods may be defined.
As one might expect, it is illegal for a variable or field to specify an opaque type. An opaque value cannot be created or passed around. We cannot access its fields. We cannot even determine its size.
Where opaque types are used is when pointed-at by references or pointers. Such references can be obtained, passed around, and stored, like any reference. We can call methods on a pointed-at value of an opaque type (typically implemented by the external system that knows the data layout and can work with such values). However, we may not use pointer de-referencing to read or mutate the pointed-at value.