Working Code
You've been using Vec<i32>, Option<String>, and Result<String, io::Error> all along. That angle-bracket-with-a-type syntax? It's all generics!
First, let's see what happens without generics:
fn print_i32(value: i32) {
println!("Value: {value}");
}
fn print_str(value: &str) {
println!("Value: {value}");
}
fn print_f64(value: f64) {
println!("Value: {value}");
}
fn main() {
print_i32(42);
print_str("hello");
print_f64(3.14);
}
You need a separate function for each type. Ten types means ten functions!
Try It Yourself
- Add a
print_bool(value: bool)function. Getting tedious, right? - Compare with the generic version below — one function handles them all!
Generic Functions — Write Once, Use for Any Type
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("Value: {value}");
}
fn main() {
print_value(42);
print_value("hello");
print_value(3.14);
print_value(true);
}
<T: Display> means "I'll use a type variable called T, but only types with the Display certificate." The trait bounds from Lesson 20 are in action!
T is a conventional name — short for Type. U, V, and others are also common.
Type Inference — Usually Omitted
Rust infers the type automatically. You can specify it explicitly, but you usually don't:
use std::fmt::Display;
fn print_value<T: Display>(value: T) {
println!("Value: {value}");
}
fn main() {
print_value::<i32>(42); // explicit — usually unnecessary
print_value(42); // inferred — use this
}
Multiple Generic Types
You can use more than one generic type:
use std::fmt::Display;
fn print_pair<T: Display, U: Display>(first: T, second: U) {
println!("{first}, {second}");
}
fn main() {
print_pair(1, "apple");
print_pair(3.14, 42);
}
Generic Structs
Structs can be generic too:
#[derive(Debug)]
struct Pair<T> {
first: T,
second: T,
}
impl<T> Pair<T> {
fn new(first: T, second: T) -> Self {
Pair { first, second }
}
}
fn main() {
let integers = Pair::new(1, 2);
let strings = Pair::new("hello", "world");
println!("{:?}", integers); // Pair { first: 1, second: 2 }
println!("{:?}", strings); // Pair { first: "hello", second: "world" }
}
In Pair<T>, first and second must be the same type T. Want different types?
#[derive(Debug)]
struct MixedPair<T, U> {
first: T,
second: U,
}
fn main() {
let mix = MixedPair { first: 42, second: "hello" };
println!("{:?}", mix);
}
Generics You've Already Used
You've been using generics all along!
fn main() {
// Vec<T> — a vector holding any type
let numbers: Vec<i32> = vec![1, 2, 3];
let names: Vec<&str> = vec!["Alice", "Bob"];
// Option<T> — a value that may or may not exist
let some_number: Option<i32> = Some(42);
let no_number: Option<i32> = None;
// Result<T, E> — success or failure
let ok: Result<i32, String> = Ok(42);
let err: Result<i32, String> = Err("error!".into());
println!("{:?}, {:?}", numbers, names);
println!("{:?}, {:?}", some_number, no_number);
println!("{:?}, {:?}", ok, err);
}
Vec, Option, and Result are all generic types. The i32, String, etc. inside angle brackets are the type arguments.
where Clause — Cleaner Trait Bounds
When trait bounds get long, move them to a where clause:
Inline bounds get crowded
use std::fmt::{Display, Debug};
fn compare_and_print<T: Display + PartialOrd + Debug>(a: T, b: T) {
if a > b {
println!("{a} > {b}");
} else {
println!("{a} <= {b}");
}
}
where clause is cleaner
use std::fmt::{Display, Debug};
fn compare_and_print<T>(a: T, b: T)
where
T: Display + PartialOrd + Debug,
{
if a > b {
println!("{a} > {b}");
} else {
println!("{a} <= {b}");
}
}
fn main() {
compare_and_print(10, 20);
compare_and_print("apple", "banana");
}
Functionally identical. where is purely a readability tool.
Learn More: Monomorphization
"Don't generics make code slower?" Don't worry! Rust uses monomorphization.
At compile time, the compiler creates a concrete copy of the generic function for each type actually used:
fn print_value<T: std::fmt::Display>(value: T) {
println!("Value: {value}");
}
fn main() {
print_value(42); // an i32 version is generated
print_value("hello"); // a &str version is generated
}
The compiler internally produces something like:
fn print_value_i32(value: i32) {
println!("Value: {value}");
}
fn print_value_str(value: &str) {
println!("Value: {value}");
}
Generics reduce code duplication with zero runtime cost.
Exercise 1 (easy). Complete the generic function. It should return the larger of two values.
fn max_of<T: PartialOrd>(a: T, b: T) -> T {
// Complete this
}
fn main() {
println!("{}", max_of(10, 20)); // 20
println!("{}", max_of("apple", "banana")); // banana
}
Exercise 2 (medium). Implement a generic Stack<T> struct.
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
// Complete this
}
fn push(&mut self, item: T) {
// Complete this
}
fn pop(&mut self) -> Option<T> {
// Complete this
// Hint: use Vec's pop() method
}
fn is_empty(&self) -> bool {
// Complete this
}
}
fn main() {
let mut stack = Stack::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(value) = stack.pop() {
println!("{value}"); // prints 3, 2, 1
}
}
Exercise 3 (challenge). Write a generic function with a where clause that requires Display + Clone. It should clone the value and print both.
use std::fmt::Display;
fn print_cloned<T>(value: T)
where
// Add bounds here
{
let cloned = value.clone();
println!("Original: {value}");
println!("Cloned: {cloned}");
}
fn main() {
print_cloned(String::from("Rust"));
print_cloned(42);
}
Q1. What is the main purpose of generics?
- A) Making programs faster
- B) Writing one piece of code that works with many types
- C) Reducing memory usage
- D) Automating error handling
Q2. Which is equivalent to fn foo<T: Display + Clone>(x: T)?
- A)
fn foo<T>(x: T) where T: Display - B)
fn foo<T>(x: T) where T: Display + Clone - C)
fn foo<T>(x: T) where T: Clone - D)
fn foo(x: Display + Clone)
Q3. How do Rust generics affect runtime performance?
- A) Slightly slower
- B) Zero cost (monomorphization converts to concrete types at compile time)
- C) Doubles memory usage
- D) Requires garbage collection