Generic Types in Rust
Generics are a powerful concept allowing types to be "expected" or known without knowing what they
are. Previously, you've encountered generics when dealing with the Option<T>
enum, where T
is a
generic that can accept any type. They are placeholder types that optionally can use traits to
define a set of expectations while still being abstract and generic.
Generics can be used in traits, methods, functions, enums, and structs.
Scenario: Why Generic Code is Useful
Let's assume that I wish to have a simple function that is meant to square two numbers, as we've previously done:
fn square(x: i32) -> i32 {
x * x
}
The above code is acceptable - however, there is a problem. What if other number types must be
compared? After all, this same function could apply to f32
, f64
, u32
, and so on.
Generics remove this redundancy by defining an abstract type, usually referred to as T
, which
allows this function to be compatible with many types.
Defining Generics: Functions
To convert the above function into a generic one, the following syntax must be applied:
fn square<T>(x: T) -> T {
x * x
}
The most glaring difference is the introduction of <T>
. This syntax precedes the parameters list,
defining what the generic is called and what properties it should entail regarding traits (more
on that next!).
The type T
in this context quite literally means any type, meaning the function can now be
called as follows (note the use of ::
to define the type):
let squared = square::<i32>(10); // 5
When we call this function, we replace the <T>
with the type we wish to represent - in this case,
i32
. Taking a closer look at the function's signature, it essentially converts from T
to i32
across the board:
fn square<i32>(square: i32) -> i32;
Now, this is possible for any number! With one caveat - not all types can utilize the *
operator.
For example, if you used a String, this function would panic, as it is impossible to compare Strings
directly this way. The above code shouldn't work together, as the compiler is unsure what to expect
since it can expect any type.
Adding Trait Bounds to Generics
Traits will be covered more in-depth in the next section. However, know that they can define certain
behavior for generics to ensure the type is compatible with the function. This particular trait,
Mul
, is a trait that ensures that a type can be multiplied. By using the syntax T: Mul
, we limit
all possible types T
could be to anything that implements Mul
:
Output
is an associated type, which will also be covered later.
fn square<T: Mul<Output = T> + Copy>(x: T) -> T {
x * x
}
This translates into our code being compatible with a whole host of various types that already
implement this trait, Mul
, by default.
// 32-Bit Signed Integer, note how you can also use ::<type> to define what type to expect.
let squared_i32 = square::<i32>(10);
// 32-Bit Unsigned Integer
let squared_u32: u32 = square(10);
// Floating Point Number
let squared_f32: f32 = square(10.0);
Defining Generics: Structs & Methods
As with functions, generics may be applied to structs, methods, and enums.
For structs, generics may be used to define abstract field types:
struct Point<T> {
x: T,
y: T
}
Multiple generics may be used, meaning the type of x
must differ from the type of y
. Generic
labels usually follow this convention, but in theory, can be named anything:
struct Point<T, U> {
x: T,
y: U
}
Point
can now be created with different types for x
and y
fields.
If we were to define some methods for Point
, generics may also be used to further provide type
dynamism:
impl<T, U> Point<T, U> {
fn x(&self) -> &T {
&self.x
}
fn y(&self) -> &U {
&self.y
}
}
Try it yourself!
What is happening here?
This example features two primary usages of Rust generics. The first illustrates the usage of
generics within a function, which reduces the boilerplate for supporting multiple compatible types
that want to utilize fn square
. Generics may also be used as a part of a struct, as seen with
Point
. An associated type is also "hidden" in the first example, where the associated type
Output
is defined as part of the Mul
trait when declaring the trait bound.