- Declare a struct, create an instance and print it with
#[derive(Debug)] - Write
&selfand&mut selfmethods and associated functions in animplblock - Create an
enumwith data-carrying variants and handle it with an exhaustivematch - Safely handle a value that may be missing with
Option,if letandunwrap_or
In an electronic class register, every student has a name, a grade and an average score — keeping these three values in separate variables is awkward; they belong together. A traffic light, on the other hand, is in exactly one state at a time: red, yellow or green. Rust has structs (struct) for the first case and enums (enum) for the second.
Structs
A struct is a type with named fields. The type name is written in PascalCase and the fields in snake_case. When you create an instance, every field must get a value, and a field is accessed with a dot: aysel.score. To change a field, the whole instance must be mut — Rust does not let you mark a single field as mut.
#[derive(Debug)]
struct Student {
name: String,
grade: u8,
score: f64,
}
fn main() {
let mut aysel = Student {
name: String::from("Aysel"),
grade: 9,
score: 91.5,
};
aysel.score += 2.0;
println!("{} is in grade {}", aysel.name, aysel.grade);
println!("{:?}", aysel);
println!("{:#?}", aysel);
}Aysel is in grade 9
Student { name: "Aysel", grade: 9, score: 93.5 }
Student {
name: "Aysel",
grade: 9,
score: 93.5,
}#[derive(Debug)] attribute teaches the compiler to print the struct with {:?}. {:#?} pretty-prints it with every field on its own line.Methods and impl
Functions that belong to a type are written in an impl block. A function whose first parameter is self is a method and is called with a dot: room.area(). A function without a self parameter is an associated function and is called with ::: Rectangle::new(4, 3) — String::from is exactly this kind. Inside impl, Self stands for the type's own name.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
fn area(&self) -> u32 {
self.width * self.height
}
fn scale(&mut self, factor: u32) {
self.width *= factor;
self.height *= factor;
}
}
fn main() {
let mut room = Rectangle::new(4, 3);
println!("Area: {}", room.area());
room.scale(2);
println!("Scaled: {}x{}, area {}", room.width, room.height, room.area());
}Area: 12 Scaled: 8x6, area 48
| First parameter | Meaning | Call |
|---|---|---|
&self | only reads the instance | room.area() |
&mut self | changes the instance | room.scale(2) |
self | takes ownership; the instance cannot be used afterwards | text.into_bytes() |
no self | associated function, often a constructor | Rectangle::new(4, 3) |
Enums and match
An enum says that a value is one of several variants. In Rust, variants can carry their own data: nothing, named fields or values in parentheses. Enums are handled with match: each arm has the form pattern => expression. A match must be exhaustive — every possible variant must be covered, or the code will not compile.
enum Command {
Quit,
Move { x: i32, y: i32 },
Say(String),
}
fn run(cmd: &Command) -> String {
match cmd {
Command::Quit => String::from("bye"),
Command::Move { x, y } => format!("move to ({x}, {y})"),
Command::Say(text) => format!("say: {text}"),
}
}
fn main() {
let commands = [
Command::Say(String::from("hello")),
Command::Move { x: 3, y: -1 },
Command::Quit,
];
for cmd in &commands {
println!("{}", run(cmd));
}
}say: hello move to (3, -1) bye
format! works like println! but returns the text as a String instead of printing it. In the match arms, x, y and text are bound to the data inside the variant.fn day_type(day: u8) -> char {
match day {
1..=5 => 'W',
6 | 7 => 'H',
}
}fn day_type(day: u8) -> char {
match day {
1..=5 => 'W',
6 | 7 => 'H',
_ => '?',
}
}u8 can be anything from 0 to 255, so the compiler says: non-exhaustive patterns: 0_u8 and 8_u8..=u8::MAX not covered. _ means “everything else”. Patterns may also use ranges (1..=5) and “or” (6 | 7).Option instead of null
Rust has no null. When a value may or may not be present, the standard library's Option<T> enum is used: Some(value) or None. You cannot use the value inside it directly — the compiler forces you to handle the None case too. If only one variant interests you, write a short if let instead of a full match.
fn find_index(names: &[&str], target: &str) -> Option<usize> {
for (i, name) in names.iter().enumerate() {
if *name == target {
return Some(i);
}
}
None
}
fn main() {
let names = ["Elvin", "Leyla", "Murad"];
match find_index(&names, "Leyla") {
Some(i) => println!("Leyla is at {i}"),
None => println!("Leyla not found"),
}
if let Some(i) = find_index(&names, "Nigar") {
println!("Nigar is at {i}");
} else {
println!("Nigar not found");
}
let murad = find_index(&names, "Murad");
println!("{:?} {}", murad, murad.unwrap_or(0));
}Leyla is at 1 Nigar not found Some(2) 2
unwrap_or(0) returns the value inside Some, or the given fallback when it is None. The standard library also has a ready-made method that does the same job: names.iter().position(|n| *n == target).Key points
- A
structgroups named fields into one type;#[derive(Debug)]lets you print it with{:?}. - Methods live in an
implblock:&selfreads,&mut selfchanges; a function withoutselfis called asType::name(). enumvariants can carry data;matchmust cover every variant, and_catches the rest.- Rust has no
null: a value that may be missing is anOption<T>—Some(x)orNone. unwrap()panics onNone;match,if letandunwrap_orare safer.
Check yourself
10 questions. Every correct answer earns XP.