Intermediate Level
Generics
Chapter 11: Generics š§¬
Write flexible, reusable code with generics while maintaining Rust's type safety.
Generic Data Types
Generic Functions
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest char: {}", largest(&chars));
}Generic Structs
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn new(x: T, y: T) -> Point<T> {
Point { x, y }
}
fn x(&self) -> &T {
&self.x
}
}
// Specialized implementation for f64
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let integer_point = Point::new(5, 10);
let float_point = Point::new(1.0, 4.0);
println!("Distance: {}", float_point.distance_from_origin());
}Generic Enums
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
fn main() {
let some_number = Option::Some(5);
let some_string = Option::Some("Hello");
let success: Result<i32, String> = Result::Ok(42);
let failure: Result<i32, String> = Result::Err(String::from("Error occurred"));
}Generic Methods
Implementing Generic Methods
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}Traits as Parameters
Trait Bounds
trait Display {
fn display(&self) -> String;
}
struct Person {
name: String,
age: u32,
}
impl Display for Person {
fn display(&self) -> String {
format!("{} is {} years old", self.name, self.age)
}
}
fn print_display<T: Display>(item: T) {
println!("{}", item.display());
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
print_display(person);
}Multiple Trait Bounds
use std::fmt::Display;
use std::fmt::Debug;
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
// Implementation
0
}
// Alternative syntax using where clause
fn some_function_alt<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// Implementation
0
}Generic Collections
Working with Generic Vectors
fn process_items<T: Clone + std::fmt::Display>(items: Vec<T>) -> Vec<T> {
let mut results = Vec::new();
for item in items {
println!("Processing: {}", item);
results.push(item.clone());
}
results
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let processed_numbers = process_items(numbers);
let strings = vec!["Hello", "World", "Rust"];
let processed_strings = process_items(strings);
}Performance Considerations
Monomorphization
fn main() {
let integer_list = vec![1, 2, 3, 4, 5];
let float_list = vec![1.0, 2.0, 3.0, 4.0, 5.0];
println!("Largest integer: {}", largest(&integer_list));
println!("Largest float: {}", largest(&float_list));
}This generates specialized code at compile time:
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_f64(list: &[f64]) -> f64 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}Practical Examples
Example 1: Generic Stack Implementation
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Stack<T> {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn size(&self) -> usize {
self.items.len()
}
}
fn main() {
let mut int_stack = Stack::new();
int_stack.push(1);
int_stack.push(2);
int_stack.push(3);
while let Some(item) = int_stack.pop() {
println!("Popped: {}", item);
}
let mut string_stack = Stack::new();
string_stack.push(String::from("Hello"));
string_stack.push(String::from("World"));
while let Some(item) = string_stack.pop() {
println!("Popped: {}", item);
}
}Example 2: Generic Binary Search Tree
use std::cmp::Ordering;
struct Node<T> {
value: T,
left: Option<Box<Node<T>>>,
right: Option<Box<Node<T>>>,
}
struct BST<T: PartialOrd> {
root: Option<Box<Node<T>>>,
}
impl<T: PartialOrd> Node<T> {
fn new(value: T) -> Node<T> {
Node {
value,
left: None,
right: None,
}
}
}
impl<T: PartialOrd> BST<T> {
fn new() -> BST<T> {
BST { root: None }
}
fn insert(&mut self, value: T) {
match self.root {
None => {
self.root = Some(Box::new(Node::new(value)));
}
Some(ref mut node) => {
Self::insert_recursive(node, value);
}
}
}
fn insert_recursive(node: &mut Box<Node<T>>, value: T) {
match value.partial_cmp(&node.value).unwrap() {
Ordering::Less => {
match node.left {
None => {
node.left = Some(Box::new(Node::new(value)));
}
Some(ref mut left_node) => {
Self::insert_recursive(left_node, value);
}
}
}
Ordering::Greater => {
match node.right {
None => {
node.right = Some(Box::new(Node::new(value)));
}
Some(ref mut right_node) => {
Self::insert_recursive(right_node, value);
}
}
}
Ordering::Equal => {
// Value already exists, do nothing
}
}
}
fn search(&self, value: &T) -> bool {
Self::search_recursive(&self.root, value)
}
fn search_recursive(node: &Option<Box<Node<T>>>, value: &T) -> bool {
match node {
None => false,
Some(ref n) => {
match value.partial_cmp(&n.value).unwrap() {
Ordering::Equal => true,
Ordering::Less => Self::search_recursive(&n.left, value),
Ordering::Greater => Self::search_recursive(&n.right, value),
}
}
}
}
}
fn main() {
let mut bst = BST::new();
bst.insert(5);
bst.insert(3);
bst.insert(7);
bst.insert(1);
bst.insert(9);
println!("Contains 3: {}", bst.search(&3));
println!("Contains 8: {}", bst.search(&8));
let mut string_bst = BST::new();
string_bst.insert(String::from("Rust"));
string_bst.insert(String::from("Python"));
string_bst.insert(String::from("JavaScript"));
println!("Contains 'Rust': {}", string_bst.search(&String::from("Rust")));
println!("Contains 'Java': {}", string_bst.search(&String::from("Java")));
}Common Mistakes
ā Forgetting Trait Bounds
// This won't compile without trait bounds
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest { // Error: can't compare T values
largest = item;
}
}
largest
}ā Adding Trait Bounds
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}Key Takeaways
- ā Generics allow writing flexible, reusable code
- ā Use trait bounds to specify what operations generic types support
- ā Generic functions and structs are monomorphized at compile time
- ā
Multiple trait bounds can be specified using
+orwhereclauses - ā Generics don't incur runtime performance penalties
- ā Follow Rust naming conventions (T, U, V for generic types)
Ready for Chapter 12? ā Traits