Problem overview
A ticket machine gives every new ticket the next number: the first ticket gets 1, the next gets 2, and so on. The counter belongs to all the tickets together, not to any one of them, while each ticket keeps its own name and its own number.
You write three things:
- the
Ticketstruct, with aname(aString) and anumber(ani32); - a module-level counter,
static mut COUNT: i32, starting at 0; - three functions in
impl Ticket:Ticket::new(name),Ticket::issued()andTicket::reset_counter().
After a reset, creating tickets for Ann, Bob and Cy gives them the numbers 1, 2 and 3, and issued() returns 3. Two tickets for Mia get different numbers (1 and 2), because the number comes from the counter, not from the name.
Approach
A field of a struct belongs to each value separately: two tickets have two number fields. A shared counter needs exactly one copy for the whole program. Rust structs have no static fields, so the counter is a static item placed outside the struct. Because it must change, it is declared static mut.
The rule for a ticket is then short:
- add 1 to the counter;
- copy the counter's new value into the new ticket's
number.
The copy is the important part. The counter keeps moving as more tickets are made, but each ticket's number is a plain i32 that stays what it was when the ticket was created.
Step by step
- Declare the struct with
name: Stringandnumber: i32. Every ticket has its own copy of both. - Declare the counter below the struct:
static mut COUNT: i32 = 0;. There is oneCOUNTfor the whole program, so every ticket sees the same one. - In
new, add 1 toCOUNTinside anunsafeblock. Rust requiresunsafefor reading or changing astatic mut, because two threads could change it at the same time. A single-threaded program like this one is fine, but the compiler can't know that. - Build the ticket as the last expression. The name is a
&strparameter, soname.to_string()makes the ownedStringthat the struct stores. - Set
numbertounsafe { COUNT }. That block's value is the counter, copied into the field. issuedreturnsunsafe { COUNT }. It has noself, so it is called on the type asTicket::issued(), likeTicket::new. This is Rust's version of a static method.reset_counterassigns 0 toCOUNT, again insideunsafe, so the next ticket gets number 1.
The solution in Rust
struct Ticket {
name: String,
number: i32,
}
static mut COUNT: i32 = 0;
impl Ticket {
fn new(name: &str) -> Ticket {
unsafe {
COUNT += 1;
}
Ticket {
name: name.to_string(),
number: unsafe { COUNT },
}
}
fn issued() -> i32 {
unsafe { COUNT }
}
fn reset_counter() {
unsafe {
COUNT = 0;
}
}
}
Using it:
Ticket::reset_counter();
let tickets = vec![Ticket::new("Ann"), Ticket::new("Bob"), Ticket::new("Cy")];
for t in &tickets {
println!("{} #{}", t.name, t.number);
}
println!("{}", Ticket::issued());
Ticket::reset_counter();
println!("{}", Ticket::issued());
println!("{}", tickets[0].number);
It prints:
Ann #1
Bob #2
Cy #3
3
0
1
After the reset, issued() is 0, but Ann's ticket still says 1: it holds its own copy of the number.
Rust details worth knowing:
static mutneedsunsafeeverywhere. Without it,COUNT += 1;is rejected with "use of mutable static is unsafe and requires unsafe function or block".- Two
unsafeblocks innew. The first changes the counter. The second reads it while the struct is built. Anunsafeblock is an expression, sounsafe { COUNT }can sit in the middle of the struct literal. - Associated functions.
issuedandreset_counterhave noself, so they are called withTicket::and need no ticket at all. - The statics are a tool for this lesson. A shared mutable global is rarely the best design in a real program. It is fine here because the program runs one thing at a time. Programs with several threads use types made for sharing, such as atomics.
Complexity
Each function does a constant amount of work, so new, issued and reset_counter all run in O(1) time. The counter is one integer for the whole program, and each ticket adds its name and number.
Edge cases and common mistakes
- Reading
COUNTlater instead of storing it. The tickets are printed after all of them are created. A ticket that readsCOUNTwhen asked for its number reports the last value for everyone: with Ann, Bob and Cy it printsAnn #3,Bob #3andCy #3. Copying the value intonumberat creation avoids that. - Adding 1 after reading. The first ticket must get 1, so the counter is increased first and then copied. Copying first would make the numbers start at 0.
- Forgetting the reset. The tests call
reset_counter()before each case, so every case numbers its tickets from 1. Without the reset, the second case would go on from where the first one stopped. - Counting by name. Two tickets with the same name are still two tickets: Mia's tickets get 1 and 2.
- No tickets. After a reset and before any
new,issued()returns 0.
Testing
- After a reset, Ann, Bob and Cy give
Ann #1,Bob #2,Cy #3and anissued()of 3. - After a reset, two tickets named Mia give
Mia #1,Mia #2and anissued()of 2. - After a reset and no tickets,
issued()gives 0.
