Structs & methods
A struct defines data; an impl block defines the functions and methods that operate on it.
struct Point {
x: i32,
y: i32,
}
impl Point {
// Associated function — no receiver. Called via `Point::new(...)`.
fn new(x: i32, y: i32) -> Point {
return Point { x: x, y: y };
}
// Instance method — receiver is `this`. Called via `p.translate(...)`.
fn translate(ref this, dx: i32, dy: i32) {
this.x = this.x +% dx;
this.y = this.y +% dy;
}
fn magnitude_squared(this) -> i32 {
return this.x *% this.x +% this.y *% this.y;
}
}
fn main() -> i32 {
var p: Point = Point::new(1, 2);
p.translate(3, 4);
return p.magnitude_squared();
}
Note the strict separation: :: reaches a type's associated items (Point::new), and . reaches an instance's methods (p.translate).
Struct literals
let x: i32 = 1;
let y: i32 = 2;
let p: Point = Point { x: x, y: y };
There is no field shorthand today; write every name: value pair explicitly.
Field visibility
Fields are public by default. To keep one private to the file, give it a leading underscore — the name itself is the marker, so privacy is always intentional:
struct Public {
value: i32, // visible to other modules
_internal: i32, // file-private
}
The three receiver forms
Methods take one of three receivers, which mirror the parameter markers. The name is always this; ref/take are the modifier:
impl Buf {
fn read(this) { ... } // read-only borrow
fn write(ref this) { ... } // mutating method, writes back
fn into_raw(take this) -> *u8 { ... } // consumes the receiver
}
Bare this is the read-only borrow; ref this may mutate and the change propagates back to the caller (so the receiver place must be var); take this consumes the receiver. The full model, including how a bare receiver and a bare parameter are both read-only borrows, is in Ownership.
Deriving Eq / Ord / Hash / Clone / ToText
An empty impl Type: Interface {} against one of those five interfaces asks the compiler to generate the memberwise implementation. No attribute, no macro: the same idiom as a Send marker impl, extended to code generation. Derived methods are ordinary AST before type-check, so they borrow-check and satisfy bounds like hand-written ones.
struct Key { id: i64, name: str }
impl Key: Eq {}
impl Key: Hash {}
var m = hash_map::new::[Key, i32]();
m.insert(Key { id: 1, name: "a" }, 100);
Field rules: primitives compare and hash directly; str orders through its blessed compare; nested structs recurse through their own (derived or written) methods; payload-free enums compare and hash by discriminant; a generic target carries the bounds you declare (impl Pair[T: Eq]: Eq {}).
Payload-carrying enum fields, arrays, slices, and tuples are not derivable (E0920 names the field). Deriving needs a struct target (E0916 otherwise). Copy stays structural and is never written. ToText needs stdlib/text in the build.
Interface default method bodies
An interface method may carry a body instead of a ;. An implementor may omit it; those that declare it override it.
interface Shape {
fn area(this) -> i32;
fn describe(this) -> i32 { return this.area() * 2; }
}
impl Sq: Shape {
fn area(this) -> i32 { return this.s * this.s; }
}
The body is copied into every impl that left the method out, before type-check: downstream sees a hand-written method, This means the implementing type, and there is no dyn. A default that calls a method the implementor lacks is diagnosed against that type. An interface whose methods all have defaults takes an empty impl (impl A: Greet {}) without E0916. The cost is code size: N implementors get N copies.
Unions, packed structs, and bitfields
#[repr(C)] union, #[repr(C, packed)], and #[bits(N)] exist so a binding can describe a C header without lying about layout. They are documented with the rest of the C ABI on FFI. For an either/or value in ordinary code, use a tagged enum.