Traits
Generics allow a program to reuse the same algorithm on different data types. However, sometimes an algorithm applies to only certain types.
# #![allow(unused_variables)] #fn main() { struct Rectangle { x: i32, y: i32, w: i32, h: i32, } impl Rectangle { fn area(&self) -> i32 { self.w * self.h } } struct Square { x: i32, y: i32, w: i32, } impl Square { fn area(&self) -> i32 { self.w * self.w } } // Error: `T` is not guaranteed to have the `area()` method. fn print_area<T>(x: T) { println!("{}", x.area()); } #}
In the above program, the function print_area()
declares no restriction on the type parameter T
, but not all types have the method area()
. For example, it makes no sense for the program to call print_area(1)
, because i32
has no method area()
. Rust provides traits to restrict what concrete types may instantiate a type parameter.
Trait and trait bound
A trait U
declares a set of methods that a type must implement. When a type V
implements U
, it must implement all of U
's methods in an implementation block. Similar to the implementation block of a struct, in the implementation block of a trait, the program may use the keyword Self
to refer to the type V
, and a method f
may use the keyword self
to refer to the value on which f
is called.
When the program declares a type parameter T
, it can specify that only types that implement a trait U
may instantiate T
. In other words, the program places a trait bound U
on T
.
# #![allow(unused_variables)] #fn main() { trait Area { fn area(&self) -> i32; fn new() -> Self; } struct Rectangle { x: i32, y: i32, w: i32, h: i32, } impl Area for Rectangle { fn area(&self) -> i32 { self.w * self.h } fn new() -> Self { Rectangle{ x: 0, y: 0, w: 1, h: 1 } } } struct Square { x: i32, y: i32, w: i32, } impl Area for Square { fn area(&self) -> i32 { self.w * self.w } fn new() -> Self { Square{ x: 0, y: 0, w: 1 } } } // OK: `T` is bounded by `Area`. fn print_area<T: Area>(x: T) { println!("{}", x.area()); } // OK: `T` is bounded by `Area`. fn new_area<T: Area>() -> T { // Since `new()` doesn't take any value, we must call `new()` directly. T::new() } // Need either type declaration or type hint for x let x: Rectangle = new_area(); let y = new_area::<Square>(); // Ok print_area(x); print_area(y); // Error: `i32` doesn't implement `Area`. // print_area(1); // Error: cannot infer the concrete type for the type parameter // let x = new_area(); #}
Multiple trait bounds
A type may implement multiple traits, and a type parameter may require multiple trait bounds.
# #![allow(unused_variables)] #fn main() { trait Area { fn area(&self) -> i32; fn new() -> Self; } trait Transform { fn scale(&mut self, s: i32); } struct Square { x: i32, y: i32, w: i32, } impl Area for Square { fn area(&self) -> i32 { self.w * self.w } fn new() -> Self { Square{ x: 0, y: 0, w: 1 } } } impl Transform for Square { fn scale(&mut self, s: i32) { self.w *= s; } } // A concrete type for `T` must implement both `Area` and `Transform` fn scale<T: Area + Transform>(mut x: T, s: i32) { println!("Before scale: {}", x.area()); x.scale(s); println!("After scale: {}", x.area()); } fn new_area<T: Area>() -> T { // Since `new()` doesn't take any value, we must call `new()` directly. T::new() } let mut x: Square = new_area(); scale(x, 2); #}
where
clause
Instead of specifying the trait bounds when declaring a type parameter, a method may specify the trait bounds after declaring all the parameters and return type, in a where
clause.
# #![allow(unused_variables)] #fn main() { // Equivalent to `fn scale<T: Area + Transform>(mut x: T, s: i32)` fn scale<T>(mut x: T, s: i32) where T: Area + Transform { println!("Before scale: {}", x.area()); x.scale(s); println!("After scale: {}", x.area()); } #}
The where
clause is more expressive because more than just the type parameter may appear at the left of the colon.
trait MyInto<T> { fn my_into(self) -> T; } impl MyInto<String> for i32 { fn my_into(self) -> String { format!("{}", self) } } impl MyInto<bool> for i32 { fn my_into(self) -> bool { self != 0 } } fn inverse_convert<T>(x: i32) -> T where i32: MyInto<T> { (-x).my_into() } fn main() { let x: bool = inverse_convert(1); assert_eq!(x, true); let x: String = inverse_convert(1); assert_eq!(x, "-1"); }
Default methods
A trait declares a set of methods, and a type implementing the trait must implement all the methods. Sometimes, a trait may provide default implementation of some methods. If a type implementing the trait does not accept the default implementation, it may reimplement these methods.
# #![allow(unused_variables)] #fn main() { trait PartialEq { fn eq(&self, other: &Self) -> bool; // Default method fn ne(&self, other: &Self) -> bool { !self.eq(other) } } struct Square { x: i32, y: i32, w: i32, } impl PartialEq for Square { fn eq(&self, other: &Self) -> bool { self.x == other.x && self.y == other.y && self.w == other.w } // Accepts the default method `PartialEq::ne()`, so does not reimplement it. } struct Rectangle { x: i32, y: i32, w: i32, h: i32, } impl PartialEq for Rectangle { fn eq(&self, other: &Self) -> bool { self.x == other.x && self.y == other.y && self.w == other.w && self.h == other.h } // Reimplements the default method `ne()`. fn ne(&self, other: &Self) -> bool { self.x != other.x || self.y != other.y || self.w != other.w || self.h != other.h } } #}
Inheritance
A trait A
may inherit another trait B
, meaning that any type implementing A
must implement all the methods in both A
and B
.
# #![allow(unused_variables)] #fn main() { trait Area { fn new() -> Self; fn area(&self) -> i32; } // Any type implementing `Equal` must also implement `Area` trait Equal: Area { fn eq(&self, other: &Self) -> bool; fn ne(&self, other: &Self) -> bool { !self.eq(other) } } struct Square { x: i32, y: i32, w: i32, } impl Area for Square { fn area(&self) -> i32 { self.w * self.w } fn new() -> Self { Square{ x: 0, y: 0, w: 1 } } } impl Equal for Square { fn eq(&self, other: &Self) -> bool { self.area() == other.area() } } #}
Special traits
Deref
# #![allow(unused_variables)] #fn main() { trait Deref { type Target: ?Sized; fn deref(&self) -> &Self::Target; } #}
This trait enables deref coercion: If impl Deref<Target=U> for T
, then the compiler automatically coerces &T
to &U
by calling Deref::deref()
. For example, if t: T
, then the compiler coerces &t
to t.deref()
.
If a variable t
is not a reference, the compiler automatically takes a reference of t
in the following context:
*t
: the compiler changes this expression to*((&t).deref())
.t.f()
: the compiler changes this expression to(&t).deref().f()
.
# #![allow(unused_variables)] #fn main() { use std::ops::Deref; struct Foo<'a>(&'a str); impl<'a> Deref for Foo<'a> { type Target = str; fn deref<'b>(&'b self) -> &'b Self::Target { self.0 } } let s = "Rust"; let foo = Foo(s); assert_eq!(s.len(), foo.len()); #}
A similar trait is DerefMut
, which also enables deref coercion.
trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
Deref coercion is commonly used to abstract the distinction between borrowed values and owned values when the distinction is inconsequential to the code in question. Rust provides three types of pointers:
&T
: contains a pointer to a borrowed value.Box<T>
: contains a pointer to an owned value on the heap.Rc<T>
andArc<T>
: each contains a pointer to a reference-counted value on the heap. We will discuss these in concurrency.
These pointers differ in how the program manages the memory of their values. But sometimes a function only accesses the values without caring about their memory management. In this case, it would be laborious to write two versions of the same function where the only difference is the type of pointers that the function takes.
# #![allow(unused_variables)] #fn main() { fn f(x: &i32) { println!("{}", x); } fn g(x: &Box<i32>) { println!("{}", x); } fn h() { f(&1); g(&Box::new(2)); } #}
It would be nice to write only one function that can handle both borrowed and owned pointers. To allow this, Rust's standard library implements the Deref
trait for Box
:
# #![allow(unused_variables)] #fn main() { impl<T> Deref for Box<T> where T: ?Sized { type Target = T; } #}
This instructs the compiler to coerce &Box<T>
to &T
. Therefore, we can remove the function g()
and the above program still compiles.
# #![allow(unused_variables)] #fn main() { fn f(x: &i32) { println!("{}", x); } fn h() { f(&1); f(&Box::new(2)); } #}
Another example of borrowed vs. owned pointers is Rust's strings. Rust provides two types of strings: &str
points to a borrowed static string, and String
points to an owned string on the heap. String
implements the Deref
trait to allow deref coercion to coerce &String
to &str
.
# #![allow(unused_variables)] #fn main() { impl Deref for String { type Target = str; } #}
# #![allow(unused_variables)] #fn main() { fn f(x: &str) { println!("{}", x); } fn g() { // The type of `x` is `&str`. let x = "abc"; f(x); // The type of `y` is `String`. let y = "def".to_owned(); f(&y); } #}
Deref coercion happens only when type mismatch would happen without the coercion.
# #![allow(unused_variables)] #fn main() { fn f(x: &str) { println!("{}", x); } fn g(x: &String) { println!("{}", x); } fn h() { // The type of `x` is `String`. let x = "abc".to_owned(); // Deref coercion happens. f(&x); // Deref coercion does not happen. g(&x); } #}
Other compiler-aware traits
Besides Deref
and DerefMut
, a few other traits also change the behavior of the compiler.
Copy
By default, Rust moves values during assignment. This is called move semantics. However, if a type implements the Copy
trait, Rust copies its values during assignment instead.
Drop
The Drop
trait provides a destructor. If a type implements the Drop
trait, Rust invokes the drop
function when its value disappears, either because it goes out of scope or because it is overwritten.
# #![allow(unused_variables)] #fn main() { trait Drop { fn drop(&mut self); } #}
For example,
# #![allow(unused_variables)] #fn main() { struct Foo(i32); impl Drop for Foo { fn drop(&mut self) { println!("{} dropped.", self.0); } } { let foo = Foo(1); // Prints `1 dropped` } #}
Sized
The sizes of most types are known at compile time. However, the sizes of some types, such as [T]
and str
, aren't. They are called unsized or dynamically sized types. There are restrictions on these types.
- The program may manipulate the value of an unsized type only via a pointer.
- Variables and parameters may not be unsized.
- Only the last field in a struct may be unsized.
The Sized
trait indicates that a type is sized. Since most types are sized, all type parameters have the implicit Sized
bound (i.e., T: Sized
is implicit for all type parameters T
). For example, the following code doesn't compile:
# #![allow(unused_variables)] #fn main() { fn f<T>(t: &T) { } let a = [1, 2, 3]; // `a[..]` is of type `[i32]` and is therefore unsized. // So this does not compile. f(&a[..]); #}
The ?Sized
trait overrides the implicit Sized
bound. It allows the concrete type to be unsized.
# #![allow(unused_variables)] #fn main() { fn f<T: ?Sized>(t: &T) { } let a = [1, 2, 3]; // `a[..]` is of type `[i32]` and is therefore unsized. // Compiles because `T: ?Sized` f(&a[..]); #}
Derivable traits
A program can request the compiler to automatically implement certain traits for a type by applying the derive
attribute to the type declaration and specifying the traits in the attribute.
# #![allow(unused_variables)] #fn main() { // Must derive both `Copy` and `Clone` because `Copy` inherits `Clone`. #[derive(Copy, Clone)] struct Foo(i32); let x = Foo(1); // `x` is copied rather than moved. let y = x; assert_eq!(x.0, y.0); #}
The following traits are derivable:
Clone
: for manually copying values.Copy
: for selecting copy (instead of move) semantics in assignment.Debug
: forprintln!("{:?}", ...)
Default
: for creating default values for the type.- For comparisons:
PartialEq
Eq
PartialOrd
Ord
Hash
: forHashMap
andHashSet
.
Associated types
Just like structs, traits may be generic. The following program may instantiate the type parameters T
with many concrete types to create different concrete traits.
# #![allow(unused_variables)] #fn main() { trait MyInto<T> { fn my_into(self) -> T; } impl MyInto<String> for i32 { fn my_into(self) -> String { format!("{}", self) } } impl MyInto<bool> for i32 { fn my_into(self) -> bool { self != 0 } } let x = 1; let y: String = x.my_into(); assert_eq!(y, "1"); let y: bool = x.my_into(); assert_eq!(y, true); #}
However, sometimes a program may wish to include type parameters in a trait, but for each type implementing the trait, it must instantiate the type parameter in only one way. For example, the trait Deref
provides deref coercion, but for each type T
implementing Deref
, it must specify a single concrete type U
where &T
automatically coerces to &U
.
Consider the following incorrect declaration of Deref
:
// Incorrect
trait Deref<T> {
fn deref(&self) -> &T;
}
It fails to satisfy the above requirement, because it would allow many different U
s where &T
coerces to &U
as in the following:
// Incorrect
impl Deref<str> for String {
fn deref(&self) -> &str { ... }
}
impl Deref<i32> for String {
fn deref(&self) -> &i32 { ... }
}
Looking from another perspective, in MyInto<T>
, the type parameter T
is an input type parameter to the trait MyInto
, so T
may be instantiated into many different concrete types. By contrast, in Deref
, the type parameter T
is an output type parameter, in the sense that the concrete type implementing Deref
determines a unique concrete type for T
. Rust provides associated types to denote output type parameters:
# #![allow(unused_variables)] #fn main() { trait Deref { type Target; fn deref(&self) -> &Self::Target; } #}
A program declares an associated type using the keyword type
in a trait declaration. When a type implements the trait, it must instantiate the associated type:
impl Deref for String {
type Target = str;
fn deref(&self) -> &Self::Target {
....
}
}
Autoref, autoderef, and deref coercion
Rust prefers explicitness. Therefore, it does implicit conversion only in a few cases. Notably, in certain cases Rust automatically takes references (autoref), dereferences (autoderef), and converts between different types of references (deref coercion). These conversions happen only when type mismatch would occur without them.
Autoref
-
If
f(&self, ...)
is a method implemented for the typeT
, andt
is a variable ofT
, then the compiler convertst.f()
to(&t).f()
, or equivalentlyT::f(&t, ...)
. -
If
t
is a non-reference variable of typeT
andimpl Deref for T
, then the compiler converts*t
to*(&t)
and then applies deref coercion on&t
.
Autoderef
- If
f(self, ...)
orf(&self, ...)
is a method implemented for the typeT
, andt
is a reference toT
in one or more levels (e.g.,&T
,&&T
, etc.), then the compiler dereferencest
as many times as necessary to make the resulting type match that of the first parameter off
. E.g., ift: &&&T
andf(&self, ...)
, then the compiler convertst.f()
to(**t).f()
, or equivalentlyT::f(**t)
.
Deref coercion
If impl Deref<Target=U> for T
and t
is a variable of the type T
, then the compiler coerces &t
, either written explicitly in the program or resulting from autoref, to (&t).deref()
, which is of type &U
.