In Chapter 8: Option, we explored how to handle values that might be present or absent using the Option enum. Now, let's learn about Generics, which let us write code that works with many different data types without having to write separate code for each type.
The "Box of Holding" Analogy
Imagine you have a box. You can put all sorts of things in the box: toys, books, tools. Generics are like creating a "smart box" that can hold any type of item you want. You don't need a separate "toy box," a "book box," and a "tool box." One "smart box" works for everything!
Central Use Case: You want to write a function that finds the largest item in a list. You want this function to work for lists of numbers, lists of strings, or lists of anything that can be compared. Generics allow you to write one find_largest function that works for all these different types!
Key Concepts
Let's break down the key things to understand about generics:
Type Parameters: Generics use type parameters, which are like placeholders for actual data types. We usually use the letter
Tas a type parameter, but you can use any valid name (likeItem,Value, etc.).Generic Functions: Functions can be generic, meaning they can take arguments of generic types and/or return values of generic types.
Generic Structs: Structs can also be generic. This means that the fields of the struct can be of generic types.
<>Syntax: We use angle brackets<>to declare type parameters. For example,fn my_function<T>(arg: T)declares a generic function namedmy_functionthat takes one argument of typeT.
Generics in Action: A Simple Function
Let's start with a simple example: a function that returns the same value it receives. Sounds pointless, but it's a good way to show generics in action!
fn identity<T>(value: T) -> T {
value
}
Explanation:
fn identity<T>(value: T) -> T: This defines a generic function namedidentity.<T>: This declaresTas a type parameter. It means "this function works with some typeT".value: T: This means the function takes one argument namedvalue, and its type isT(whatever typeTis).-> T: This means the function returns a value of typeT.
value: The function simply returns thevaluethat was passed in.
Now, let's use this function with different data types:
fn main() {
let number = identity(5);
let text = identity(String::from("hello"));
println!("Number: {}", number);
println!("Text: {}", text);
}
Output:
Number: 5
Text: hello
Explanation:
let number = identity(5);: We callidentitywith the integer5. Rust automatically figures out thatTisi32in this case.let text = identity(String::from("hello"));: We callidentitywith aString. Rust figures out thatTisStringin this case.The same
identityfunction works for bothi32andString! That's the power of generics.
Generics in Action: A Struct
Let's create a generic struct:
struct Point<T> {
x: T,
y: T,
}
Explanation:
struct Point<T>: This defines a generic struct namedPoint.<T>: This declaresTas a type parameter.
x: T, y: T: This means that thexandyfields of thePointstruct will both have the same typeT.
Now, let's create instances of the Point struct with different types:
fn main() {
let integer_point = Point { x: 10, y: 20 };
let float_point = Point { x: 1.5, y: 2.5 };
//You need to specify the type, or rust will throw an error since "" doesn't
//have a concrete type
let string_point: Point<&str> = Point { x: "hi", y: "world"};
//This won't work as the point can't support multiple types at once
//let string_point: Point<String, i32> = Point {x: "hi".to_string(), y: 1};
println!("Integer Point x: {}", integer_point.x);
println!("Float Point x: {}", float_point.x);
println!("String Point y: {}", string_point.y);
}
Output:
Integer Point x: 10
Float Point x: 1.5
String Point y: world
Explanation:
let integer_point = Point { x: 10, y: 20 };: We create aPointwhereTisi32.let float_point = Point { x: 1.5, y: 2.5 };: We create aPointwhereTisf64.The same
Pointstruct works for both integers and floating-point numbers!
Multiple Type Parameters
You can use multiple type parameters in a generic struct or function:
struct Rectangle<T, U> {
width: T,
height: U,
}
Explanation:
struct Rectangle<T, U>: This defines a generic struct namedRectanglewith two type parameters:TandU.width: T, height: U: Thewidthfield has typeT, and theheightfield has typeU. This meanswidthandheightcan have different types.
Generics in Action: The find_largest Function
Let's revisit our central use case: writing a function that finds the largest item in a list. To do this, the type T must implement the PartialOrd trait, which allows for comparison.
fn find_largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Explanation:
fn find_largest<T: PartialOrd>(list: &[T]) -> &T: This defines a generic function namedfind_largest.T: PartialOrd: This is a trait bound. It means that the typeTmust implement thePartialOrdtrait, which allows values of typeTto be compared using operators like>.list: &[T]: The function takes a slice ofTvalues as input.-> &T: The function returns a reference to the largest value in the list.
The function iterates through the list, comparing each
itemto the currentlargestvalue.If
itemis greater thanlargest, we updatelargestto beitem.Finally, we return a reference to the
largestvalue.
Now, let's use this function with different types of lists:
fn main() {
let numbers = [1, 5, 2, 8, 3];
let largest_number = find_largest(&numbers);
println!("Largest number: {}", largest_number);
let strings = ["apple", "banana", "cherry", "date"];
let largest_string = find_largest(&strings);
println!("Largest string: {}", largest_string);
}
Output:
Largest number: 8
Largest string: date
Explanation:
We call
find_largestwith a list of integers and a list of strings.The same
find_largestfunction works for both types because bothi32and&strimplement thePartialOrdtrait.
Internal Implementation (Simplified)
When you use generics, Rust performs a process called monomorphization at compile time. This means that for each concrete type you use with a generic function or struct, Rust creates a separate version of that function or struct specialized for that type.
Let's see a simplified sequence diagram of how this works:
Explanation:
You write generic code in your source code.
The Rust compiler sees that you're using
find_largestwithi32andString.The compiler creates two separate versions of the function:
find_largest_i32andfind_largest_String. These functions are no longer generic; they're specific toi32andString, respectively.The compiler compiles these specialized functions into your executable.
This means that generics have no runtime cost. The code is just as efficient as if you had written separate functions for each type.
Conclusion
Generics are a powerful tool in Rust that allows you to write flexible, reusable, and efficient code that works with multiple data types. By using type parameters and trait bounds, you can create generic functions and structs that can be adapted to a wide range of situations.
In the next chapter, we'll explore Traits, which define shared behavior that different types can implement.


