Lesson 20 of 94
Methods & Associated Functions
In Rust, a struct holds the data and a separate impl block holds the functions attached to it. The key new idea is that every method declares how it uses the instance: read it, change it, or consume it.
| Concept | Rust |
|---|---|
| Instance method | Method with an explicit first parameter: &self, &mut self, or self |
| Function without an instance | Associated function (no self), called with Type::name() |
| Constructor | Associated function, by convention User::new(...) |
| The instance and its type | self (the value) and Self (the type) |
| Constant tied to a type | Associated constant: const MAX: u32 = 10; inside impl |
| Privacy | Fields are private to their module unless marked pub |
1. A method that reads the instance: &self
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let r = Rectangle { width: 3.0, height: 4.5 };
println!("area {} square {}", r.area(), r.is_square());
}area 13.5 square false&self is short for self: &Self, where Self means "the type this impl is for". The method borrows the rectangle, so r is still usable afterwards.
2. A method that changes the instance: &mut self
#[derive(Debug)]
struct Counter {
count: u32,
}
impl Counter {
fn increment(&mut self) {
self.count += 1;
}
fn reset(&mut self) {
self.count = 0;
}
}
fn main() {
let mut c = Counter { count: 0 };
c.increment();
c.increment();
println!("{:?}", c);
c.reset();
println!("{:?}", c);
}Counter { count: 2 }
Counter { count: 0 }The signature tells every caller that this method may change the value. The variable must be mut to call it.
3. Calling a &mut self method needs a mut variable
struct Counter {
count: u32,
}
impl Counter {
fn increment(&mut self) {
self.count += 1;
}
}
fn main() {
let c = Counter { count: 0 };
c.increment();
}error[E0596]: cannot borrow `c` as mutable, as it is not declared as mutable
--> src/main.rs:13:5
|
13 | c.increment();
| ^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
12 | let mut c = Counter { count: 0 };
| +++4. A method that consumes the instance: self
A method taking self by value takes ownership. After the call, the instance is gone. Use this for "finishing" operations that turn a value into something else.
struct Draft {
title: String,
body: String,
}
struct Published {
html: String,
}
impl Draft {
fn publish(self) -> Published {
Published { html: format!("<h1>{}</h1><p>{}</p>", self.title, self.body) }
}
}
fn main() {
let draft = Draft { title: String::from("Hello"), body: String::from("First post") };
let post = draft.publish();
println!("{}", post.html);
}<h1>Hello</h1><p>First post</p>Using draft after publish is an error, which is exactly what you want: a draft cannot be edited after it has been published.
struct Draft {
title: String,
}
impl Draft {
fn publish(self) -> String {
format!("<h1>{}</h1>", self.title)
}
}
fn main() {
let draft = Draft { title: String::from("Hello") };
let html = draft.publish();
println!("{html}, {}", draft.title);
}error[E0382]: borrow of moved value: `draft`
--> src/main.rs:14:28
|
12 | let draft = Draft { title: String::from("Hello") };
| ----- move occurs because `draft` has type `Draft`, which does not implement the `Copy` trait
13 | let html = draft.publish();
| --------- `draft` moved due to this method call
14 | println!("{html}, {}", draft.title);
| ^^^^^^^^^^^ value borrowed here after move
|
note: `Draft::publish` takes ownership of the receiver `self`, which moves `draft`
--> src/main.rs:6:16
|
6 | fn publish(self) -> String {
| ^^^^5. Associated functions: Rust's static methods and constructors
A function in an impl block without self is an associated function. You call it with Type::name(...). By convention, the main constructor is called new.
#[derive(Debug)]
struct User {
name: String,
email: String,
active: bool,
}
impl User {
fn new(name: &str, email: &str) -> Self {
Self {
name: name.to_string(),
email: email.to_lowercase(),
active: true,
}
}
}
fn main() {
let user = User::new("Ada", "Ada@Example.com");
println!("{:?}", user);
}User { name: "Ada", email: "ada@example.com", active: true }Self inside an impl block is an alias for the type (User here). new is only a convention: it is not a keyword and has no special powers.
6. Several constructors with descriptive names
Without overloading, each constructor gets its own name. The standard library uses the same style: String::new, String::from, String::with_capacity.
#[derive(Debug)]
struct Color {
r: u8,
g: u8,
b: u8,
}
impl Color {
fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
fn gray(level: u8) -> Self {
Self { r: level, g: level, b: level }
}
fn from_hex(hex: u32) -> Self {
Self {
r: ((hex >> 16) & 0xFF) as u8,
g: ((hex >> 8) & 0xFF) as u8,
b: (hex & 0xFF) as u8,
}
}
fn black() -> Self {
Self::gray(0)
}
}
fn main() {
println!("{:?}", Color::rgb(255, 128, 0));
println!("{:?}", Color::gray(200));
println!("{:?}", Color::from_hex(0x1E90FF));
println!("{:?}", Color::black());
}Color { r: 255, g: 128, b: 0 }
Color { r: 200, g: 200, b: 200 }
Color { r: 30, g: 144, b: 255 }
Color { r: 0, g: 0, b: 0 }7. Getters use the field name, without get_
Rust convention names a getter after the field. A method and a field can share a name, and parentheses tell them apart.
struct Temperature {
celsius: f64,
}
impl Temperature {
fn celsius(&self) -> f64 {
self.celsius
}
fn fahrenheit(&self) -> f64 {
self.celsius * 9.0 / 5.0 + 32.0
}
}
fn main() {
let t = Temperature { celsius: 21.5 };
println!("field {} / method {} / derived {}", t.celsius, t.celsius(), t.fahrenheit());
}field 21.5 / method 21.5 / derived 70.78. Methods that call other methods
struct Invoice {
amounts_cents: Vec<u64>,
tax_percent: u64,
}
impl Invoice {
fn subtotal(&self) -> u64 {
let mut sum = 0;
for a in &self.amounts_cents {
sum += a;
}
sum
}
fn tax(&self) -> u64 {
self.subtotal() * self.tax_percent / 100
}
fn total(&self) -> u64 {
self.subtotal() + self.tax()
}
}
fn main() {
let inv = Invoice { amounts_cents: vec![1_000, 2_500, 499], tax_percent: 20 };
println!("subtotal={} tax={} total={}", inv.subtotal(), inv.tax(), inv.total());
}subtotal=3999 tax=799 total=47989. Methods that take another instance
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width >= other.width && self.height >= other.height
}
fn scaled(&self, factor: u32) -> Rectangle {
Rectangle { width: self.width * factor, height: self.height * factor }
}
}
fn main() {
let box_ = Rectangle { width: 30, height: 50 };
let small = Rectangle { width: 10, height: 40 };
let large = small.scaled(4);
println!("box can hold small: {}", box_.can_hold(&small));
println!("box can hold large ({}x{}): {}", large.width, large.height, box_.can_hold(&large));
}box can hold small: true
box can hold large (40x160): false10. Automatic referencing at the call site
You write r.area(), not (&r).area(). Rust adds the &, &mut, or * needed to match the method's self type. This works through references too.
struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance_to(&self, other: &Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
fn main() {
let a = Point { x: 0.0, y: 0.0 };
let b = Point { x: 6.0, y: 8.0 };
let ref_a = &a;
let ref_ref_a = &ref_a;
println!("{}", a.distance_to(&b));
println!("{}", ref_a.distance_to(&b));
println!("{}", ref_ref_a.distance_to(&b));
println!("{}", Point::distance_to(&a, &b)); // the fully explicit form
}10
10
10
10The last line shows what a method call really is: an associated function whose first argument is the instance.
11. Associated constants
struct Password {
value: String,
}
impl Password {
const MIN_LENGTH: usize = 12;
const MAX_LENGTH: usize = 128;
fn is_valid(&self) -> bool {
let len = self.value.len();
len >= Self::MIN_LENGTH && len <= Self::MAX_LENGTH
}
}
fn main() {
let p = Password { value: String::from("correct horse battery") };
println!("valid: {} (min {})", p.is_valid(), Password::MIN_LENGTH);
}valid: true (min 12)12. Several impl blocks
A type can have any number of impl blocks. This is often used to group related methods.
struct Circle {
radius: f64,
}
impl Circle {
fn new(radius: f64) -> Self {
Self { radius }
}
}
impl Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
fn circumference(&self) -> f64 {
2.0 * std::f64::consts::PI * self.radius
}
}
fn main() {
let c = Circle::new(2.0);
println!("area {:.2}, circumference {:.2}", c.area(), c.circumference());
}area 12.57, circumference 12.5713. Methods on tuple structs and newtypes
#[derive(Debug, Clone, Copy)]
struct Celsius(f64);
#[derive(Debug, Clone, Copy)]
struct Fahrenheit(f64);
impl Celsius {
fn to_fahrenheit(self) -> Fahrenheit {
Fahrenheit(self.0 * 9.0 / 5.0 + 32.0)
}
}
impl Fahrenheit {
fn to_celsius(self) -> Celsius {
Celsius((self.0 - 32.0) * 5.0 / 9.0)
}
}
fn main() {
let body = Celsius(37.0);
let f = body.to_fahrenheit();
println!("{:?} = {:?}", body, f);
println!("{:?}", Fahrenheit(212.0).to_celsius());
}Celsius(37.0) = Fahrenheit(98.6)
Celsius(100.0)For a small Copy type like this, taking self by value is idiomatic: copying 8 bytes is as cheap as passing a reference.
14. Chaining &mut self methods
A method can return &mut Self so calls can be chained on the same instance.
#[derive(Debug, Default)]
struct Query {
table: String,
conditions: Vec<String>,
limit: Option<u32>,
}
impl Query {
fn from(&mut self, table: &str) -> &mut Self {
self.table = table.to_string();
self
}
fn filter(&mut self, condition: &str) -> &mut Self {
self.conditions.push(condition.to_string());
self
}
fn limit(&mut self, n: u32) -> &mut Self {
self.limit = Some(n);
self
}
fn to_sql(&self) -> String {
let mut sql = format!("SELECT * FROM {}", self.table);
if !self.conditions.is_empty() {
sql.push_str(&format!(" WHERE {}", self.conditions.join(" AND ")));
}
if let Some(n) = self.limit {
sql.push_str(&format!(" LIMIT {n}"));
}
sql
}
}
fn main() {
let mut q = Query::default();
q.from("orders").filter("status = 'paid'").filter("total > 100").limit(20);
println!("{}", q.to_sql());
}SELECT * FROM orders WHERE status = 'paid' AND total > 100 LIMIT 20limit: Option<u32> holds either Some(n) or None, and if let Some(n) = ... runs only when there is a value. Both are covered in Option.
15. A consuming builder
The other builder style takes and returns self by value. The final build(self) consumes the builder. This is the most common builder pattern in Rust libraries.
#[derive(Debug)]
struct HttpClient {
base_url: String,
timeout_secs: u32,
retries: u8,
user_agent: String,
}
struct HttpClientBuilder {
base_url: String,
timeout_secs: u32,
retries: u8,
user_agent: String,
}
impl HttpClientBuilder {
fn new(base_url: &str) -> Self {
Self {
base_url: base_url.to_string(),
timeout_secs: 30,
retries: 0,
user_agent: String::from("rust-client"),
}
}
fn timeout(mut self, secs: u32) -> Self {
self.timeout_secs = secs;
self
}
fn retries(mut self, n: u8) -> Self {
self.retries = n;
self
}
fn build(self) -> HttpClient {
HttpClient {
base_url: self.base_url,
timeout_secs: self.timeout_secs,
retries: self.retries,
user_agent: self.user_agent,
}
}
}
fn main() {
let client = HttpClientBuilder::new("https://api.example.com")
.timeout(5)
.retries(3)
.build();
println!("{:#?}", client);
}HttpClient {
base_url: "https://api.example.com",
timeout_secs: 5,
retries: 3,
user_agent: "rust-client",
}mut self makes the owned self mutable inside the method, like mut on any other parameter.
16. A &mut self call while a field is borrowed
The borrow rules apply to self like any other value. Holding a reference into the struct blocks methods that need &mut self.
struct Playlist {
songs: Vec<String>,
}
impl Playlist {
fn add(&mut self, song: &str) {
self.songs.push(song.to_string());
}
}
fn main() {
let mut p = Playlist { songs: vec![String::from("Intro")] };
let first = &p.songs[0];
p.add("Outro");
println!("{first}");
}error[E0502]: cannot borrow `p` as mutable because it is also borrowed as immutable
--> src/main.rs:14:5
|
13 | let first = &p.songs[0];
| ------- immutable borrow occurs here
14 | p.add("Outro");
| ^^^^^^^^^^^^^^ mutable borrow occurs here
15 | println!("{first}");
| ----- immutable borrow later used here17. Returning references from methods
A method can return a reference into self. The compiler knows it borrows from self.
struct Library {
books: Vec<(String, u32)>, // (title, year)
}
impl Library {
fn oldest(&self) -> &str {
let mut best = &self.books[0];
for book in &self.books {
if book.1 < best.1 {
best = book;
}
}
&best.0
}
fn titles(&self) -> Vec<&str> {
let mut titles = Vec::new();
for (title, _) in &self.books {
titles.push(title.as_str());
}
titles
}
}
fn main() {
let lib = Library {
books: vec![
(String::from("Dune"), 1965),
(String::from("Neuromancer"), 1984),
(String::from("Foundation"), 1951),
],
};
println!("oldest: {}", lib.oldest());
println!("titles: {:?}", lib.titles());
}oldest: Foundation
titles: ["Dune", "Neuromancer", "Foundation"]18. Returning a &mut to a field
#[derive(Debug)]
struct Inventory {
items: Vec<(String, u32)>,
}
impl Inventory {
fn quantity_mut(&mut self, name: &str) -> &mut u32 {
let mut index = self.items.len();
for (i, item) in self.items.iter().enumerate() {
if item.0 == name {
index = i;
}
}
if index == self.items.len() {
self.items.push((name.to_string(), 0));
}
&mut self.items[index].1
}
}
fn main() {
let mut inv = Inventory { items: vec![(String::from("apple"), 3)] };
*inv.quantity_mut("apple") += 2;
*inv.quantity_mut("pear") += 7;
println!("{:?}", inv);
}Inventory { items: [("apple", 5), ("pear", 7)] }The method finds the index first and only then creates the &mut. The more obvious version, which returns &mut item.1 from inside the for loop and pushes after the loop, is rejected by today's borrow checker even though it is actually safe. This is a known limitation, and Fixing Borrow Checker Errors shows it and its workarounds.
19. Privacy: fields are private to their module
In one file with no modules, everything can see everything. Privacy in Rust works at the level of modules, which you declare with mod. Items inside a module are private unless marked pub. Here the balance field is private, so code outside the bank module must go through the methods. Modules covers this fully.
mod bank {
pub struct Account {
pub owner: String,
balance_cents: i64, // private
}
impl Account {
pub fn open(owner: &str) -> Self {
Self { owner: owner.to_string(), balance_cents: 0 }
}
pub fn deposit(&mut self, cents: i64) {
if cents > 0 {
self.balance_cents += cents;
}
}
pub fn balance(&self) -> i64 {
self.balance_cents
}
}
}
fn main() {
let mut acc = bank::Account::open("Ana");
acc.deposit(5_000);
acc.deposit(-9_999); // ignored by the method's rule
println!("{} has {}", acc.owner, acc.balance());
}Ana has 5000Reaching for the private field directly does not compile:
mod bank {
pub struct Account {
balance_cents: i64,
}
impl Account {
pub fn open() -> Self {
Self { balance_cents: 0 }
}
}
}
fn main() {
let mut acc = bank::Account::open();
acc.balance_cents = 1_000_000;
}error[E0616]: field `balance_cents` of struct `Account` is private
--> src/main.rs:15:9
|
15 | acc.balance_cents = 1_000_000;
| ^^^^^^^^^^^^^ private field20. Realistic example: a bank account with history
#[derive(Debug)]
struct Account {
owner: String,
balance_cents: i64,
history: Vec<String>,
}
impl Account {
const OVERDRAFT_LIMIT_CENTS: i64 = 10_000;
fn open(owner: &str, initial_cents: i64) -> Self {
let mut acc = Self { owner: owner.to_string(), balance_cents: 0, history: Vec::new() };
acc.deposit(initial_cents);
acc
}
fn deposit(&mut self, cents: i64) {
self.balance_cents += cents;
self.history.push(format!("+{}", Self::fmt(cents)));
}
fn withdraw(&mut self, cents: i64) -> bool {
if self.balance_cents - cents < -Self::OVERDRAFT_LIMIT_CENTS {
self.history.push(format!("DECLINED -{}", Self::fmt(cents)));
return false;
}
self.balance_cents -= cents;
self.history.push(format!("-{}", Self::fmt(cents)));
true
}
fn fmt(cents: i64) -> String {
format!("{}.{:02}", cents / 100, (cents % 100).abs())
}
fn statement(&self) -> String {
format!("{}: {} [{}]", self.owner, Self::fmt(self.balance_cents), self.history.join(", "))
}
}
fn main() {
let mut acc = Account::open("Grace", 50_00);
acc.withdraw(120_00);
acc.withdraw(50_00);
acc.deposit(25_50);
println!("{}", acc.statement());
}Grace: -44.50 [+50.00, -120.00, DECLINED -50.00, +25.50]21. Realistic example: a bounded stack
A stack that wraps a Vec and enforces a maximum size. pop and peek return Option because the stack may be empty.
struct BoundedStack {
items: Vec<i32>,
capacity: usize,
}
impl BoundedStack {
fn with_capacity(capacity: usize) -> Self {
Self { items: Vec::with_capacity(capacity), capacity }
}
fn push(&mut self, value: i32) -> bool {
if self.items.len() == self.capacity {
return false;
}
self.items.push(value);
true
}
fn pop(&mut self) -> Option<i32> {
self.items.pop()
}
fn peek(&self) -> Option<&i32> {
self.items.last()
}
fn len(&self) -> usize {
self.items.len()
}
fn is_full(&self) -> bool {
self.items.len() == self.capacity
}
}
fn main() {
let mut stack = BoundedStack::with_capacity(3);
for v in [10, 20, 30, 40] {
println!("push {v}: {}", stack.push(v));
}
println!("full: {}, len: {}, peek: {:?}", stack.is_full(), stack.len(), stack.peek());
println!("pop: {:?}", stack.pop());
println!("pop: {:?}", stack.pop());
println!("len: {}", stack.len());
}push 10: true
push 20: true
push 30: true
push 40: false
full: true, len: 3, peek: Some(30)
pop: Some(30)
pop: Some(20)
len: 122. Realistic example: a shopping cart that checks out once
checkout(self) consumes the cart and returns a receipt. After checkout, the compiler makes it impossible to add items to the old cart or check out twice.
#[derive(Debug)]
struct Receipt {
lines: Vec<String>,
total_cents: u64,
}
struct Cart {
items: Vec<(String, u64, u32)>, // (name, unit price, quantity)
}
impl Cart {
fn new() -> Self {
Self { items: Vec::new() }
}
fn add(&mut self, name: &str, price_cents: u64, quantity: u32) {
for item in &mut self.items {
if item.0 == name {
item.2 += quantity;
return;
}
}
self.items.push((name.to_string(), price_cents, quantity));
}
fn remove(&mut self, name: &str) {
let mut i = 0;
while i < self.items.len() {
if self.items[i].0 == name {
self.items.remove(i);
} else {
i += 1;
}
}
}
fn checkout(self) -> Receipt {
let mut lines = Vec::new();
let mut total = 0;
for (name, price, qty) in self.items {
let line_total = price * qty as u64;
lines.push(format!("{qty} x {name} = {}.{:02}", line_total / 100, line_total % 100));
total += line_total;
}
Receipt { lines, total_cents: total }
}
}
fn main() {
let mut cart = Cart::new();
cart.add("coffee", 899, 1);
cart.add("mug", 1250, 2);
cart.add("coffee", 899, 1);
cart.add("filter", 350, 1);
cart.remove("filter");
let receipt = cart.checkout();
for line in &receipt.lines {
println!("{line}");
}
println!("total {}.{:02}", receipt.total_cents / 100, receipt.total_cents % 100);
}2 x coffee = 17.98
2 x mug = 25.00
total 42.9823. Realistic example: a stopwatch with laps
Time is passed in as milliseconds, which keeps the example deterministic. Real code would use std::time::Instant.
struct Stopwatch {
started_at_ms: u64,
laps: Vec<u64>,
last_lap_at_ms: u64,
}
impl Stopwatch {
fn start(now_ms: u64) -> Self {
Self { started_at_ms: now_ms, laps: Vec::new(), last_lap_at_ms: now_ms }
}
fn lap(&mut self, now_ms: u64) -> u64 {
let lap = now_ms - self.last_lap_at_ms;
self.laps.push(lap);
self.last_lap_at_ms = now_ms;
lap
}
fn fastest_lap(&self) -> u64 {
let mut best = u64::MAX;
for &lap in &self.laps {
best = best.min(lap);
}
best
}
fn stop(self, now_ms: u64) -> (u64, Vec<u64>) {
(now_ms - self.started_at_ms, self.laps)
}
}
fn main() {
let mut sw = Stopwatch::start(1_000);
sw.lap(62_400);
sw.lap(120_900);
sw.lap(181_150);
println!("fastest lap: {} ms", sw.fastest_lap());
let (total, laps) = sw.stop(185_000);
println!("total {total} ms, laps {:?}", laps);
}fastest lap: 58500 ms
total 184000 ms, laps [61400, 58500, 60250]24. Realistic example: a 2x2 matrix type
Small math types usually take self by value and return new values, which makes expressions read naturally.
#[derive(Debug, Clone, Copy, PartialEq)]
struct Matrix2 {
a: f64,
b: f64,
c: f64,
d: f64,
}
impl Matrix2 {
const IDENTITY: Matrix2 = Matrix2 { a: 1.0, b: 0.0, c: 0.0, d: 1.0 };
fn new(a: f64, b: f64, c: f64, d: f64) -> Self {
Self { a, b, c, d }
}
fn mul(self, o: Matrix2) -> Matrix2 {
Matrix2::new(
self.a * o.a + self.b * o.c,
self.a * o.b + self.b * o.d,
self.c * o.a + self.d * o.c,
self.c * o.b + self.d * o.d,
)
}
fn determinant(self) -> f64 {
self.a * self.d - self.b * self.c
}
fn pow(self, n: u32) -> Matrix2 {
let mut result = Self::IDENTITY;
for _ in 0..n {
result = result.mul(self);
}
result
}
}
fn main() {
let fib = Matrix2::new(1.0, 1.0, 1.0, 0.0);
println!("fib^10 = {:?}", fib.pow(10));
println!("det = {}", fib.determinant());
println!("m * I == m: {}", fib.mul(Matrix2::IDENTITY) == fib);
}fib^10 = Matrix2 { a: 89.0, b: 55.0, c: 55.0, d: 34.0 }
det = -1
m * I == m: trueThe top-right entry of fib^10 is the 10th Fibonacci number, 55. Advanced Traits shows how to let a * b call mul with operator overloading.
25. Realistic example: a rate limiter
A token bucket allows short bursts but limits the average rate. Timestamps are passed in to keep the example deterministic.
struct TokenBucket {
capacity: f64,
tokens: f64,
refill_per_sec: f64,
last_refill_ms: u64,
}
impl TokenBucket {
fn new(capacity: u32, refill_per_sec: f64, now_ms: u64) -> Self {
Self { capacity: capacity as f64, tokens: capacity as f64, refill_per_sec, last_refill_ms: now_ms }
}
fn refill(&mut self, now_ms: u64) {
let elapsed_secs = (now_ms - self.last_refill_ms) as f64 / 1000.0;
self.tokens = (self.tokens + elapsed_secs * self.refill_per_sec).min(self.capacity);
self.last_refill_ms = now_ms;
}
fn try_acquire(&mut self, now_ms: u64) -> bool {
self.refill(now_ms);
if self.tokens >= 1.0 {
self.tokens -= 1.0;
true
} else {
false
}
}
}
fn main() {
let mut limiter = TokenBucket::new(3, 1.0, 0);
let requests_at_ms = [0, 10, 20, 30, 500, 1_100, 1_200, 3_500];
for t in requests_at_ms {
let verdict = if limiter.try_acquire(t) { "allowed" } else { "throttled" };
println!("t={t:>5}ms {verdict}");
}
}t= 0ms allowed
t= 10ms allowed
t= 20ms allowed
t= 30ms throttled
t= 500ms throttled
t= 1100ms allowed
t= 1200ms throttled
t= 3500ms allowed26. Realistic example: a type-safe state change
Consuming methods can model a workflow where each step returns a different type. An unpaid order simply has no ship method, so shipping an unpaid order is a compile error rather than a runtime check.
struct NewOrder {
id: u32,
total_cents: u64,
}
struct PaidOrder {
id: u32,
payment_ref: String,
}
struct ShippedOrder {
id: u32,
tracking: String,
}
impl NewOrder {
fn pay(self, payment_ref: &str) -> PaidOrder {
println!("order {} paid {} cents", self.id, self.total_cents);
PaidOrder { id: self.id, payment_ref: payment_ref.to_string() }
}
}
impl PaidOrder {
fn ship(self, tracking: &str) -> ShippedOrder {
println!("order {} (payment {}) shipped", self.id, self.payment_ref);
ShippedOrder { id: self.id, tracking: tracking.to_string() }
}
}
fn main() {
let order = NewOrder { id: 77, total_cents: 4_200 };
// order.ship("X"); // does not compile: NewOrder has no `ship` method
let paid = order.pay("pay_abc123");
let shipped = paid.ship("1Z999AA10123456784");
println!("order {} tracking {}", shipped.id, shipped.tracking);
}order 77 paid 4200 cents
order 77 (payment pay_abc123) shipped
order 77 tracking 1Z999AA10123456784This is the typestate pattern. API Design Idioms builds on it.
Summary
- Methods live in
impl Type { ... }blocks. A type can have severalimplblocks. - The first parameter says how the method uses the instance:
&selfreads,&mut selfchanges,selfconsumes. - Functions without
selfare associated functions, called asType::name().newis the conventional constructor name. Selfis the type being implemented. Associated constants live inimplblocks too.- Method calls add
&,&mut, or*automatically. - Builders return
&mut Self(chaining on one instance) orSelf(consuming builder). - Privacy is per module: fields are private unless marked
pub.
Next: Enums.
