Generics in Rust allow you to write flexible and reusable code. They enable you to define functions, structs, enums, and methods that can operate on many different types while still being type-safe. This section will cover the basics of generics, how to use them, and provide practical examples and exercises to solidify your understanding.
Key Concepts
- Generic Functions: Functions that can operate on different data types.
- Generic Structs: Structs that can hold or operate on different data types.
- Generic Enums: Enums that can hold different data types.
- Trait Bounds: Constraints that specify what functionality a type must provide to be used in a generic context.
Generic Functions
Example
Let's start with a simple example of a generic function that returns the largest element in a slice:
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut largest = &list[0]; for item in list.iter() { if item > largest { largest = item; } } largest }
Explanation
fn largest<T: PartialOrd>(list: &[T]) -> &T
: This defines a functionlargest
that takes a slice of typeT
and returns a reference to an element of typeT
.T: PartialOrd
: This is a trait bound that ensures the typeT
can be compared using the>
operator.
Practical Exercise
Exercise: Write a generic function smallest
that returns the smallest element in a slice.
fn smallest<T: PartialOrd>(list: &[T]) -> &T { let mut smallest = &list[0]; for item in list.iter() { if item < smallest { smallest = item; } } smallest }
Generic Structs
Example
Here's an example of a generic struct that can hold any type of value:
Explanation
struct Point<T>
: This defines a structPoint
with a generic typeT
.impl<T> Point<T>
: This implements methods for thePoint
struct with the generic typeT
.
Practical Exercise
Exercise: Extend the Point
struct to include a method distance_from_origin
that works only when T
is a floating-point number.
use std::ops::Add; impl Point<f64> { fn distance_from_origin(&self) -> f64 { (self.x.powi(2) + self.y.powi(2)).sqrt() } }
Generic Enums
Example
Here's an example of a generic enum that can hold different types of values:
Explanation
enum Option<T>
: This defines an enumOption
with a generic typeT
.Some(T)
: This variant holds a value of typeT
.None
: This variant represents the absence of a value.
Practical Exercise
Exercise: Create a generic enum Result
that can hold either a value of type T
or an error of type E
.
Trait Bounds
Example
Here's an example of using trait bounds to constrain generic types:
Explanation
fn print<T: std::fmt::Display>(value: T)
: This defines a functionprint
that takes a value of typeT
and prints it.T: std::fmt::Display
: This is a trait bound that ensures the typeT
implements theDisplay
trait.
Practical Exercise
Exercise: Write a generic function compare_and_print
that takes two values of type T
and prints the larger one. Ensure T
implements the PartialOrd
and Display
traits.
fn compare_and_print<T: PartialOrd + std::fmt::Display>(a: T, b: T) { if a > b { println!("Larger value: {}", a); } else { println!("Larger value: {}", b); } }
Summary
In this section, you learned about generics in Rust, including how to define and use generic functions, structs, and enums. You also learned about trait bounds and how they can be used to constrain generic types. Generics are a powerful feature that allows you to write flexible and reusable code. In the next module, we will explore concurrency in Rust, which will build on the concepts you've learned so far.