Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last lesson, you used functions and control flow to organize calculations. This lesson gives related values—and the calculations that belong to them—a shared home. You’ll define a struct for a small domain model, add methods that read or change it, and write an associated function that creates it. Allow about 40 minutes, including the videos and a run through the code in your Cargo project.
Give related data a type
Suppose a program records a trip segment’s distance and duration. Two separate f64 variables work, but nothing in their types says that they describe the same segment. A struct makes that relationship explicit:
struct TripSegment {
distance_km: f64,
duration_hours: f64,
}
TripSegment is now a type you have defined. Its fields have names and types; an instance supplies their values. Put the definition above main in src/main.rs, then create and use an instance:
fn main() {
let mut trip = TripSegment {
distance_km: 12.0,
duration_hours: 0.5,
};
println!("distance: {} km", trip.distance_km);
trip.distance_km = 20.0;
println!("revised distance: {} km", trip.distance_km);
}
The TripSegment { ... } expression must supply a value of the right type for every field. The fields need not appear in declaration order. Dot notation accesses a field; assigning to one requires the instance to be declared mut. You do not mark individual fields mutable in the struct definition.
Watch the opening of Structs in Rust from Let’s Get Rusty for a visual walkthrough. Its example uses account details rather than a trip; concentrate on the struct syntax, named fields, and instance mutability rather than the string-specific details.
Structs in Rust from Let’s Get Rusty shows how a definition becomes a concrete value.
Watch named fields for the contrast with positional tuple fields. Continue with instances and updates; notice that the whole instance is made mutable before a field is changed, and that fields can be supplied in a different order.
A useful design test is whether the fields describe one thing. Distance and duration together describe a trip segment; grouping every number in the program into a single struct would not clarify the model.
Put behavior beside the data
Given a TripSegment, its average speed is a calculation specific to that type. Rust places such functions in a separate impl block:
impl TripSegment {
fn average_speed_kmh(&self) -> f64 {
self.distance_km / self.duration_hours
}
}
This is a method because its first parameter is self. Here, &self means the method reads the instance without changing it. Call it with dot notation:
println!("speed: {} km/h", trip.average_speed_kmh());
There is no need to pass trip again inside the parentheses: it is the receiver of the call. Within the method, self.distance_km selects a field of that receiver. The return value is the division expression, using the same tail-expression rule you learned for functions.
Methods can accept further typed parameters. For example, one segment can compare its distance with another’s:
fn is_longer_than(&self, other: &TripSegment) -> bool {
self.distance_km > other.distance_km
}
This method belongs inside impl TripSegment. The call would be trip.is_longer_than(&alternative). Rust handles the borrow of the receiver for a method taking &self; the & before alternative is written explicitly to match the other parameter’s type. The ownership module will examine references in depth. For now, read &TripSegment as access to a segment without handing it over.
Watch Methods for Structs from Francesco Ciulla to see an ordinary calculation become a method, followed by a method with an additional parameter.
Methods for Structs - Full Crash Rust Tutorial for Beginners
Methods for Structs from Francesco Ciulla demonstrates what changes when a function is associated with a struct instance.
Watch the method refactor, focusing on impl, &self, and the dot-notation call. Then watch another parameter for a method that compares two instances. You can stop before the discussion of rotating rectangles.
A method can also change its receiver. Write &mut self and call it on a mutable instance:
fn extend(&mut self, extra_km: f64, extra_hours: f64) {
self.distance_km += extra_km;
self.duration_hours += extra_hours;
}
This method belongs in the same impl block. It changes both fields and has no return value specified. The receiver form communicates intent: &self reads; &mut self changes. A method can also take self without &, but that has ownership consequences you’ll study in the next module.
Create instances with an associated function
Writing out every field is clear, but sometimes creation itself deserves a name. An impl block may contain a function without a self parameter:
impl TripSegment {
fn new(distance_km: f64, duration_hours: f64) -> Self {
Self {
distance_km,
duration_hours,
}
}
}
This is an associated function, but not a method: it has no existing instance to receive. Call it on the type with :::
let trip = TripSegment::new(12.0, 0.5);
Inside impl TripSegment, the capitalized Self means TripSegment. The field initializer distance_km is shorthand for distance_km: distance_km: the parameter and field have the same name. new is a convention, not a special Rust keyword, and Rust does not generate this function automatically.
The same video shows a one-argument associated function that creates a square. It uses a different name, making the :: call particularly easy to distinguish from a method call.
Methods for Structs - Full Crash Rust Tutorial for Beginners
Return to Methods for Structs for its example of a function associated with the type rather than an instance.
Watch the constructor. Focus on why it has no self, returns a struct instance, and is called with the type name followed by ::.
A constructor may enforce rules, but merely naming one new does not do so. The example below assumes a positive duration; this new function does not check that assumption.
Assemble the model
Replace src/main.rs with this complete version. It keeps the fields and their behavior together without putting function bodies inside the struct definition:
struct TripSegment {
distance_km: f64,
duration_hours: f64,
}
impl TripSegment {
fn new(distance_km: f64, duration_hours: f64) -> Self {
Self {
distance_km,
duration_hours,
}
}
fn average_speed_kmh(&self) -> f64 {
self.distance_km / self.duration_hours
}
fn extend(&mut self, extra_km: f64, extra_hours: f64) {
self.distance_km += extra_km;
self.duration_hours += extra_hours;
}
fn is_longer_than(&self, other: &TripSegment) -> bool {
self.distance_km > other.distance_km
}
}
fn main() {
let mut trip = TripSegment::new(12.0, 0.5);
let alternative = TripSegment {
distance_km: 15.0,
duration_hours: 1.0,
};
println!("initial speed: {} km/h", trip.average_speed_kmh());
trip.extend(8.0, 0.5);
println!("extended speed: {} km/h", trip.average_speed_kmh());
println!(
"longer than alternative: {}",
trip.is_longer_than(&alternative)
);
}
Run cargo run. The initial speed is 24 km/h. After the extension, the trip covers 20 km in one hour, so its speed is 20 km/h; its distance is also greater than the alternative’s 15 km. Notice that you can create the same type through either TripSegment::new(...) or a struct literal. Both produce a TripSegment, so both can use its methods.
What to carry forward
A struct names and types related fields. An impl block attaches behavior to that type: methods use self and are called on instances with ., while associated functions without self are called on the type with ::. Choose &self for a reading method and &mut self for one that changes a mutable instance.
Next, you’ll bring functions, control flow, and structs together in the module’s command-line unit converter.