Rust organises behaviour through structs (named-field data types), enums (sum types with optional data in each variant), impl blocks (methods and associated functions), and traits (shared behaviour contracts) — with generics for type-parameterised code and trait objects (dyn Trait) for runtime polymorphism.
Rust's type system uses structs for data, enums for variants, and traits for shared behaviour — there are no classes, no inheritance, and no null values.
Structs and enums
// Struct — named fields of different types
#[derive(Debug)] // auto-implement Debug for println!("{:?}", ...)
struct User {
name: String,
email: String,
age: u32,
active: bool,
}
impl User {
// Associated function (like a static method) — constructor pattern
fn new(name: &str, email: &str, age: u32) -> Self {
User {
name: name.to_string(),
email: email.to_string(),
age, // shorthand when field = variable name
active: true,
}
}
// Method — takes &self (immutable borrow of the instance)
fn greeting(&self) -> String {
format!("Hello, {}!", self.name)
}
// Mutable method — takes &mut self
fn deactivate(&mut self) {
self.active = false;
}
}
// Enum — a type that can be one of several variants
#[derive(Debug)]
enum Direction { North, South, East, West }
// Enum with data — each variant can hold different types
#[derive(Debug)]
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // struct-like
Write(String), // tuple-like
ChangeColor(u8, u8, u8), // tuple with three values
}
fn main() {
let mut user = User::new("Priya", "priya@example.com", 28);
println!("{}", user.greeting()); // "Hello, Priya!"
user.deactivate();
println!("{:#?}", user); // pretty debug print
let msg = Message::Move { x: 10, y: 20 };
println!("{:?}", msg);
}Pattern matching
fn describe_message(msg: &Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({x}, {y})"),
Message::Write(text) => println!("Write: {text}"),
Message::ChangeColor(r,g,b) => println!("Color: #{r:02x}{g:02x}{b:02x}"),
}
// match must be exhaustive — all variants covered or use _ wildcard
}
// Option<T> — the Rust null replacement
// Option<T> is either Some(value) or None — no null pointer dereferences
fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn main() {
let result = divide(10.0, 2.0);
match result {
Some(v) => println!("Result: {v}"),
None => println!("Division by zero"),
}
// if let — pattern match for one variant
if let Some(v) = divide(10.0, 3.0) {
println!("Got: {v:.2}");
}
// unwrap_or — default value if None
let v = divide(10.0, 0.0).unwrap_or(0.0);
// ? operator — propagate None (or Err) upward
// fn double_divide(a: f64, b: f64) -> Option<f64> {
// let first = divide(a, b)?; // returns None if divide returns None
// Some(first * 2.0)
// }
}Traits
// Trait — defines shared behaviour (like an interface)
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> String;
// Default implementation — can be overridden
fn describe(&self) -> String {
format!("{} says {}", self.name(), self.sound())
}
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> String { "Woof".to_string() }
}
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> String { "Meow".to_string() }
// Override default
fn describe(&self) -> String {
format!("{} says {} (quietly)", self.name(), self.sound())
}
}
// Trait as a function parameter — accepts any type implementing Animal
fn make_noise(animal: &impl Animal) {
println!("{}", animal.describe());
}
// Generic syntax — equivalent to above
fn make_noise_generic<T: Animal>(animal: &T) {
println!("{}", animal.describe());
}
fn main() {
let dog = Dog { name: "Rex".to_string() };
let cat = Cat { name: "Whiskers".to_string() };
make_noise(&dog); // "Rex says Woof"
make_noise(&cat); // "Whiskers says Meow (quietly)"
}Generics and trait bounds
use std::fmt::Display;
// Generic function with multiple trait bounds
fn print_and_return<T: Display + Clone>(value: T) -> T {
println!("{}", value);
value.clone()
}
// Where clause — same as above, cleaner for complex bounds
fn print_and_return2<T>(value: T) -> T
where T: Display + Clone
{
println!("{}", value);
value.clone()
}
// Generic struct
#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
impl<T: Display + PartialOrd> Pair<T> {
fn new(first: T, second: T) -> Self { Pair { first, second } }
fn larger(&self) -> &T {
if self.first > self.second { &self.first } else { &self.second }
}
}
// Dynamic dispatch — trait objects
// dyn Trait — pointer to any type implementing the trait
// Stored as a fat pointer: (data pointer, vtable pointer)
fn make_sounds(animals: &[Box<dyn Animal>]) {
for a in animals {
println!("{}", a.describe()); // dispatched via vtable at runtime
}
}
fn main() {
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: "Rex".to_string() }),
Box::new(Cat { name: "Whiskers".to_string() }),
];
make_sounds(&animals);
}Important standard traits
| Trait | What it enables | How to get it |
|---|---|---|
Debug | println!("{:?}", value) | #[derive(Debug)] |
Display | println!("{}", value) | implement manually |
Clone | value.clone() — deep copy | #[derive(Clone)] |
Copy | silent copy on assignment (stack types) | #[derive(Copy, Clone)] |
PartialEq | == and != | #[derive(PartialEq)] |
Ord | <, >, sorting | #[derive(PartialOrd, Ord, Eq)] |
Hash | use as HashMap key | #[derive(Hash)] |
Default | Type::default() | #[derive(Default)] |
Iterator | for loops, map/filter/collect | implement next() |
From / Into | type conversions | implement From<T> |
impl Trait or a generic T: Trait, Rust monomorphises — it generates a separate compiled version of the function for each concrete type. This is zero-cost abstraction: no vtable, no pointer indirection. Only dyn Trait uses a vtable and incurs dynamic dispatch.Option<T> is the replacement for null — not an addition. Rust has no null keyword and no null pointer. Every value is either present or absent in an explicit, type-checked way. You cannot accidentally dereference a None as if it were a value — the compiler forces you to handle both cases.Option<T> and Result<T, E> are both enums. This is fundamentally different from Java/C# enums which are just named integers.Monomorphisation and zero-cost abstractions
When the Rust compiler encounters a generic function fn f<T: Trait>(x: T), it performs monomorphisation at compile time: it generates a separate compiled version of f for each concrete type T that is used. f::<i32> and f::<String> become two different compiled functions. The result is the same machine code as if you had written a non-generic, concrete function — no runtime overhead. This is what Rust means by "zero-cost abstractions": the abstraction compiles away entirely. The trade-off is larger binary size (each monomorphised copy is compiled separately) and longer compile times.
Coherence and the orphan rule
Rust's trait coherence rules prevent conflicting trait implementations. The orphan rule: you can implement a trait for a type only if either the trait or the type is defined in your crate. You cannot implement a foreign trait for a foreign type (e.g., you cannot implement std::fmt::Display for Vec<T> in your crate — both are from std). This prevents two crates from providing conflicting implementations that the compiler cannot choose between. The newtype pattern — wrapping a foreign type in a tuple struct — is the standard workaround when you need to implement a foreign trait for a foreign type.