Problem overview
A shop ships parcels in three ways. Standard is the basic option: its name is "Standard" and its cost for a parcel of weight kilograms is 5 + weight. Express and Economy answer the same two questions in their own way. Express costs 10 + 2 * weight, and Economy costs 3 + weight / 2, where the division drops the remainder (7 / 2 is 3).
Rust has no class inheritance, so the shared behavior lives in a trait. The trait Shipping holds the standard option as default methods, and each type decides whether to keep or replace them.
Approach: defaults in the trait, overrides in the types
A trait method that has a body is a default. Any type that implements the trait gets that method for free. If the type writes a method with the same name in its own impl block, its version is used for that type only, and the default stays untouched for the others.
That gives a simple layout. Standard has an empty block, impl Shipping for Standard {}, so it keeps both defaults. Express and Economy write both methods and replace both defaults.
Step by step
- Write the default
name. Its body is just the text"Standard", with no semicolon, because the last expression of a function is its return value. A string literal has type&str, which matches the return type. - Write the default
cost. It returns5 + weight. - Replace
nameforExpressinsideimpl Shipping for Express. - Replace
costforExpress:10 + 2 * weight. If you forget this method, an express parcel quietly gets the default price,5 + weight. - Replace
nameforEconomy. - Replace
costforEconomy:3 + weight / 2. Both operands arei32, so the division is an integer division.
The solution in Rust
trait Shipping {
fn name(&self) -> &str {
"Standard"
}
fn cost(&self, weight: i32) -> i32 {
5 + weight
}
}
struct Standard;
impl Shipping for Standard {}
struct Express;
impl Shipping for Express {
fn name(&self) -> &str {
"Express"
}
fn cost(&self, weight: i32) -> i32 {
10 + 2 * weight
}
}
struct Economy;
impl Shipping for Economy {
fn name(&self) -> &str {
"Economy"
}
fn cost(&self, weight: i32) -> i32 {
3 + weight / 2
}
}
The code that uses these types holds each option as a Box<dyn Shipping> and calls name() and cost(weight) the same way for every option. Here is a small program that does that, with the types above in the same file:
fn main() {
let options: Vec<Box<dyn Shipping>> = vec![Box::new(Standard), Box::new(Express), Box::new(Economy)];
for option in &options {
println!("{}: {}", option.name(), option.cost(4));
}
}
It prints:
Standard: 9
Express: 18
Economy: 5
Each box runs the method of the type inside it. Standard answers with the defaults, the other two with their own methods.
Rust details worth knowing
- A unit struct.
struct Standard;has no fields. You create it by writing its name,Standard, with no braces and nonew. - Integer division truncates. With
weightequal to 7,weight / 2is 3, so Economy costs 6. With a weight of 1 the half is 0 and Economy costs 3. - Methods with the same signature. A replacement must match the trait's signature exactly:
fn name(&self) -> &strandfn cost(&self, weight: i32) -> i32.
Edge cases and common mistakes
- A trailing semicolon.
"Standard";turns the body into a statement, and the function returns()instead of a&str. The compiler reports a type mismatch. - Replacing only one method. An
Expressthat writesnamebut notcostreports itself as "Express" and charges the standard price. - Dividing the wrong thing. The Economy rule halves the weight, not the whole price. For a weight of 4 that gives 3 + 2 = 5, not (3 + 4) / 2 = 3.
- Weight 0. Only the base prices remain: 5 for Standard, 10 for Express and 3 for Economy.
Why defaults instead of copies
You could write name and cost out in full for all three types. The result would work, but the standard price would then be written wherever a type happens to want it, and a change to the base price would have to be repeated in several places. With a default, the standard rule lives once in the trait, and only the types that differ say so. Reading impl Shipping for Standard {} tells you at a glance that Standard has no special behavior.
The order of work in the lesson follows the same logic: first the defaults, which already make Standard complete, then one override at a time for Express and Economy. After each override, the type answers for itself and nothing else changes.
Adding a fourth option
The point of defaults is that a new type only writes what is different. An overnight option with its own price but the standard name needs only one method:
struct Overnight;
impl Shipping for Overnight {
fn cost(&self, weight: i32) -> i32 {
20 + 3 * weight
}
}
Held in a Box<dyn Shipping> next to Standard and called with a weight of 2, it prints:
Standard: 7
Standard: 26
The second line shows the default name at work: Overnight never wrote one, so it reports "Standard". In this lesson, Express and Economy replace both methods, so their names are their own. Whether a type keeps a default is a choice made type by type.
If you do write a method in an impl Shipping for ... block, it must have the trait's exact signature. And a body such as "Standard";, with the semicolon, fails to compile with mismatched types: it expected &str and found ().
Testing
Running the three options on a few weights gives:
standard 4 -> Standard: 9
express 4 -> Express: 18
economy 4 -> Economy: 5
economy 7 -> Economy: 6
economy 1 -> Economy: 3
economy 0 -> Economy: 3
express 0 -> Express: 10
