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 functionlargestthat takes a slice of typeTand returns a reference to an element of typeT.T: PartialOrd: This is a trait bound that ensures the typeTcan 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 structPointwith a generic typeT.impl<T> Point<T>: This implements methods for thePointstruct 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 enumOptionwith 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 functionprintthat takes a value of typeTand prints it.T: std::fmt::Display: This is a trait bound that ensures the typeTimplements theDisplaytrait.
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.
