Generics
Generics Syntax
Generics enable writing code that can work with different data types without having to write separate versions for each type. It is like creating a template or a placeholder for a type that will be specified later.
Functions, type aliases (type
), structs (struct
), enumerations (enum
), unions, traits (trait
), and implementations (impl
) may be parameterized by types, constants, and lifetimes. These parameters are listed in angle brackets (<...>), usually immediately after the name of the item and before its definition.
use std::collections::HashMap; use std::fmt::Debug; /// Function with a type parameter and a lifetime. fn print_data<'a, T: std::fmt::Debug>(value: &'a T) { println!("Data: {:?}", value); } /// Type alias with a type parameter. type StringMap<K> = HashMap<K, String>; /// Struct with a type parameter and a lifetime. struct DataHolder<'a, T> { data: &'a T, } /// Method implementation for the struct. impl<'a, T> DataHolder<'a, T> { fn get_data(&self) -> &'a T { self.data } } /// Trait with a type parameter. trait Processor<T> { fn process(&self, item: T) -> String; } /// Tuple struct with a type parameter. struct TupleStruct<T>(T); // Implementing the trait for the tuple struct. impl<T: Debug> Processor<T> for TupleStruct<T> { fn process(&self, item: T) -> String { format!("Processed: {:?}", item) } } // Const generic parameters allow items to be generic over constant values. struct InnerArray<T, const N: usize>([T; N]); fn main() { print_data(&10); let _map = StringMap::<String>::new(); let data = DataHolder { data: &10 }; println!("Data: {:?}", data.get_data()); let tuple = TupleStruct(10); println!("{}", tuple.process(10)); let _inner_array: InnerArray<i32, 3> = InnerArray([1, 2, 3]); }
Generic Structs
Generic structs are declared by adding one or more type parameters between < and >, after the name of the struct.
use std::fmt::Display; /// A generic struct `Point` that can hold two values of the same type `T`: struct Point<T> { x: T, y: T, } /// Implementation block for `Point<T>` where `T` can be any type: impl<T> Point<T> { fn x(&self) -> &T { &self.x } } /// Implementation block for `Point<f32>` only: impl Point<f32> { /// A method specific to `Point<f32>`: fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } /// You may also use Trait Bounds to conditionally implement methods. /// The following is only implemented for `Point<T>` when `T` implements both /// the `Display` and `PartialOrd` traits. impl<T: Display + PartialOrd> Point<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } } fn main() { // Instantiate `Point` with integer coordinates (type `i32`). let integer_point = Point { x: 5, y: 10 }; // Instantiate `Point` with floating-point coordinates (type `f64`). let float_point = Point { x: 1.0, y: 4.0 }; // Rust can often infer the type, but you could also be explicit: // let integer_point: Point<i32> = Point { x: 5, y: 10 }; // let float_point: Point<f64> = Point { x: 1.0, y: 4.0 }; println!( "Integer point: x = {}, y = {}", integer_point.x, integer_point.y ); println!("Float point: x = {}, y = {}", float_point.x, float_point.y); // This would cause a compile-time error because x and y must be the same // type `T`. let wont_compile = Point { x: 5, y: 4.0 }; }
See Structs for more details.
Generic Enums
Generic enums are declared similarly to generic structs, with type parameters between < and > after the enum name.
The most common example is the Result
enum. It can either hold a value of type T
(in the Ok
variant) or an error value of type E
(in the Err
variant).
enum Result<T, E> {
Ok(T),
Err(E),
}
See Enums for more details.
Generic Functions
Generic functions allow you to write code that can operate on parameters of various types without needing to rewrite the function for each specific type.
{{#include ../../crates/language/tests/generics/generic_functions.rs:example}}
See Functions for more details.
Generic Traits
Generic traits allow you to define traits that can work with different types.
/// A trait that can be implemented for different types. trait Test<T> { /// A function that takes a generic type `T` as input. fn test(_t: T); } struct SomeStruct; /// Implement the `Test` trait for a `struct`. // Note that the `<>` brackets appear in two places. impl<T> Test<T> for SomeStruct { fn test(_t: T) { println!("This is the test function."); } } fn main() { SomeStruct::test(1); SomeStruct::test(true); }
Trait bounds are often used with generics to specify that a generic type must implement a particular trait.
use std::collections::hash_map::DefaultHasher; use std::hash::Hash; use std::hash::Hasher; /// Trait bounds allow you to specify that a generic type must implement a /// certain trait. /// /// Here, the `print_hash` function is generic over an unknown /// type `T`, but requires that `T` implements the `Hash` trait. fn print_hash<T: Hash>(t: &T) { let mut hasher = DefaultHasher::new(); t.hash(&mut hasher); // The `finish` method returns the computed hash. println!("The hash is {:x}", hasher.finish()); } /// This struct represents a pair of values. /// /// It is generic over the types of the two values. struct Pair<A, B> { first: A, second: B, } // Generics make it possible to implement a trait conditionally. // Here, the `Pair` type implements `Hash` if, and only if, // its components do. impl<A: Hash, B: Hash> Hash for Pair<A, B> { fn hash<H: Hasher>(&self, state: &mut H) { self.first.hash(state); self.second.hash(state); } } fn main() { let p = Pair { first: 1, second: "2", }; print_hash(&p); }
See Traits for more details.
See Also
- Rust Patterns.