Structs and Enums
Structs
A struct bundles multiple values together. Similar to classes or objects in other languages.
struct User {
name: String,
age: i32,
}
fn main() {
let user = User {
name: String::from("Alice"),
age: 25,
};
println!("{} is {} years old", user.name, user.age);
}
Adding Methods (impl)
Use an impl block to add methods to a struct:
struct User {
name: String,
age: i32,
}
impl User {
fn greet(&self) {
println!("Hi, I'm {}!", self.name);
}
fn have_birthday(&mut self) {
self.age += 1;
}
}
fn main() {
let mut user = User {
name: String::from("Alice"),
age: 25,
};
user.greet(); // Hi, I'm Alice!
user.have_birthday();
println!("{}", user.age); // 26
}
The first argument of a method is "self":
| Syntax | Meaning |
|---|---|
&self |
Read only. No modification |
&mut self |
Can modify self |
Oversimplification warning:
&is actually an important concept called "references," but&selfand&mut selfin methods are basically boilerplate. Just think "read only" vs "can modify" and you'll be fine.
Associated Functions (Constructor-like)
Functions without self can also be written in impl. Call them with :: like User::new(...):
impl User {
fn new(name: String, age: i32) -> User {
User { name, age }
}
}
fn main() {
let user = User::new(String::from("Bob"), 30);
user.greet();
}
User { name, age } is shorthand for User { name: name, age: age }. When the variable name matches the field name, you can omit it.
Enums
An enum represents "one of these":
enum Direction {
Up,
Down,
Left,
Right,
}
fn main() {
let dir = Direction::Up;
match dir {
Direction::Up => println!("Going up!"),
Direction::Down => println!("Going down!"),
Direction::Left => println!("Going left!"),
Direction::Right => println!("Going right!"),
}
}
Works perfectly with match. The compiler will yell at you if you don't cover all variants, preventing missed cases.
Enums with Data
Each variant of an enum can hold data. This is where Rust enums really shine:
enum Shape {
Circle(f64), // radius
Rectangle(f64, f64), // width, height
}
fn area(shape: Shape) -> f64 {
match shape {
Shape::Circle(r) => 3.14 * r * r,
Shape::Rectangle(w, h) => w * h,
}
}
fn main() {
let c = Shape::Circle(5.0);
let r = Shape::Rectangle(3.0, 4.0);
println!("Circle: {}", area(c)); // 78.5
println!("Rectangle: {}", area(r)); // 12.0
}
You can branch on the variant with match and extract the inner data at the same time.
derive: Boilerplate Magic
Adding #[derive(...)] to structs and enums automatically adds useful features:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 1.0, y: 2.0 };
// Debug: display contents with {:?}
println!("{:?}", p); // Point { x: 1.0, y: 2.0 }
// Clone: copy with .clone()
let p2 = p.clone();
// PartialEq: compare with ==
println!("{}", p == p2); // true
}
| derive | What it enables |
|---|---|
Debug |
Display contents with {:?} |
Clone |
Copy with .clone() |
PartialEq |
Compare with == |
Just slap these 3 on everything and you'll be fine.
Works on enums too:
#[derive(Debug, Clone, PartialEq)]
enum Color {
Red,
Green,
Blue,
}
fn main() {
let c = Color::Red;
println!("{:?}", c); // Red
}
Now you can create your own data types. Next, we'll look at Vec, String, and other "dynamic things," and touch on something that resembles ownership.