Structs
Rust structs are like tuples in that they are homogeneous and defined statically, but their fields are named. They are to Rust what classes are to Java and C++: the primary means of building abstractions. But they do not put data and code together like a class does. Nor do they support inheritance.
A new struct is defined as a set of keys and value types. This struct models a zoo animal:
struct Animal {
name: String,
binomial: String,
is_captive_born: bool,
annual_cost: u32,
annual_visitors: u32,
}
We create a new instance of a Animal
using this syntax:
let elephant = Animal {
name: String::from("Hank"),
binomial: String::from("Elephas maximus"),
is_captive_born: true,
annual_cost: 82_000,
annual_visitors: 1_100_000,
};
The ordering of the key-value pairs in the initializer doesn't matter. There is no new
keyword. In the current version of Rust, all struct instances begin their life on the stack. We will see how to explicitly copy them to the heap when we later discuss memory management. As we will see, we use the heap less often in Rust.
We extract fields from a struct as either lvalues or rvalues using the .
operator:
println!("{} ({})", elephant.name, elephant.binomial);
elephant.annual_cost += 1000; // compile error
Assigning to these fields is legal only if the receiver is mutable. Variable elephant
is not, so this code does not compile.
As with JavaScript objects and Haskell records, we may destructure a struct to bind its fields to new local variables:
let Animal { name, binomial, .. } = elephant;
println!("{} ({})", name, binomial);
To extract only a subset of the fields, as is done here, an explicit ..
token is needed to discard the rest.
Methods are not defined in a Rust struct as they are in Java class. Rather, they are defined separately in an impl
or implementation block. This block adds a cost_per_visitor
method to all instances of Animal
:
struct Animal {
// ...
}
impl Animal {
fn cost_per_visitor(&self) -> f64 {
self.annual_cost as f64 / self.annual_visitors as f64
}
// other methods...
}
The receiver of the method is explicitly listed as the first parameter. It must be named self
if the method is to be considered an instance method. The type of self
is usually omitted, but we could write the formal parameter as either self: &Self
or self: &Animal
. Either way, it is an immutable reference. The compiler replaces the Self
token with the type of the impl
block.
If a method mutates the fields of a struct, then the receiver must be declared as &mut self
instead of &self
. This qualifier is used by the compiler to determine if the method may be called on a receiver. If the receiver is not declared mutable, a call to a &mut self
method won't compile.
Methods in an impl
block that don't have self
as their first parameter are associated functions. These are the equivalent of static methods in classes. They are tied to the overall type, but not a particular instance. This associated function concatenates a genus and species together to form a binomial:
impl Animal {
fn make_binomial(genus: &str, species: &str) -> String {
format!("{} {}", genus, species)
}
}
Calls to associated functions are qualified by the struct type, and the type and function name are separated by ::
rather than .
:
let binomial = Animal::make_binomial("Macropus", "rufus");
println!("binomial: {:?}", binomial); // prints "Macropus rufus"
Associated functions are often used to write constructors. This associated function constructs a new instance of Animal
but uses Self
as its return type:
impl Animal {
fn blank() -> Self {
Animal {
name: String::from(""),
binomial: String::from(""),
is_captive_born: false,
annual_cost: 0,
annual_visitors: 0,
}
}
// Others: from_json, from_file, ...
}
This isn't an official constructor as Rust doesn't have a notion of constructors beyond the struct initialization syntax. Many struct writers create an associated function named new
to act as a default constructor. But others may exist.
The advantage of associated constructor functions over overloaded constructors found in Java and C++ is that each function may be given a meaningful name that describes how or from what data a new instance is built. For example, a LinearFunction
class can have constructors with the meaningful names from_slope_intercept
, from_two_points
, and from_point_slope
. In Java, all constructors would be labeled with the vague name LinearFunction
.