Structures can be made generic over one or more type parameters. These types are given enclosed in <>
when referring to the type:
struct Gen<T> {
x: T,
z: isize,
}
// ...
let _: Gen<bool> = Gen{x: true, z: 1};
let _: Gen<isize> = Gen{x: 42, z: 2};
let _: Gen<String> = Gen{x: String::from("hello"), z: 3};
Multiple types can be given using comma:
struct Gen2<T, U> {
x: T,
y: U,
}
// ...
let _: Gen2<bool, isize> = Gen2{x: true, y: 42};
The type parameters are a part of the type, so two variables of the same base type, but with different parameters, are not interchangeable:
let mut a: Gen<bool> = Gen{x: true, z: 1};
let b: Gen<isize> = Gen{x: 42, z: 2};
a = b; // this will not work, types are not the same
a.x = 42; // this will not work, the type of .x in a is bool
If you want to write a function that accepts a struct
regardless of its type parameter assignment, that function would also need to be made generic:
fn hello<T>(g: Gen<T>) {
println!("{}", g.z); // valid, since g.z is always an isize
}
But what if we wanted to write a function that could always print g.x
? We would need to restrict T
to be of a type that can be displayed. We can do this with type bounds:
use std::fmt;
fn hello<T: fmt::Display>(g: Gen<T>) {
println!("{} {}", g.x, g.z);
}
The hello
function is now only defined for Gen
instances whose T
type implements fmt::Display
. If we tried passing in a Gen<(bool, isize)>
for example, the compiler would complain that hello
is not defined for that type.
We can also use type bounds directly on the type parameters of the struct
to indicate that you can only construct that struct
for certain types:
use std::hash::Hash;
struct GenB<T: Hash> {
x: T,
}
Any function that has access to a GenB
now knows that the type of x
implements Hash
, and thus that they can call .x.hash()
. Multiple type bounds for the same parameter can be given by separating them with a +
.
Same as for functions, the type bounds can be placed after the <>
using the where
keyword:
struct GenB<T> where T: Hash {
x: T,
}
This has the same semantic meaning, but can make the signature easier to read and format when you have complex bounds.
Type parameters are also available to instance methods and associated methods of the struct
:
// note the <T> parameter for the impl as well
// this is necessary to say that all the following methods only
// exist within the context of those type parameter assignments
impl<T> Gen<T> {
fn inner(self) -> T {
self.x
}
fn new(x: T) -> Gen<T> {
Gen{x: x}
}
}
If you have type bounds on Gen
's T
, those should also be reflected in the type bounds of the impl
. You can also make the impl
bounds tighter to say that a given method only exists if the type satisfies a particular property:
impl<T: Hash + fmt::Display> Gen<T> {
fn show(&self) {
println!("{}", self.x);
}
}
// ...
Gen{x: 42}.show(); // works fine
let a = Gen{x: (42, true)}; // ok, because (isize, bool): Hash
a.show(); // error: (isize, bool) does not implement fmt::Display