Utility Traits
Drop
You can customize hwo Rust drops values of your type by implementing the std::ops::Drop
trait:
#![allow(unused)] fn main() { trait Drop { fn drop(&mut self); } }
Implicit Behavior: The
drop
method of theDrop
trait is called implicity by Rust, if you try to call it yourself, it'll be flagged as an error.
You'll never need to implement Drop
unless you're defining a type that owns resources Rust doesn't already know about.
Warning: If a type implements
Drop
, it cannot implementCopy
.
Sized
Term: A type whose values all have the same size in memory is called a sized type. In Rust, almost all types are sized types.
All sized types implement the std::marker::Sized
trait, which has no methods nor associated types. Rust implements it automatically for all types to which it applies; you can't implement it yourself.
Use Case: The only use for the
Sized
trait is as a bound for type variables: a bound likeT: Sized
requiresT
to be a type whose size is known at compile time.
Term: A trait that can only be used as a type parameter bound, and cannot be explicitly implemented (like
Sized
), is called a marker trait.
Implicit Behavior: Since unsized types are so limited, Rust implicitly assumes that generic type parameters have a
Sized
bound. This mean that when you writestruct S<T>
, Rust assumes you meanstruct S<T: Sized>
.
Syntax: Since Rust assumes all type parameters have a
Sized
bound, you have to explicitly opt-out of it using the?Sized
syntax:struct S<T: ?Sized>
.
Clone
The std::clone::Clone
trait is for types that can make copies of themselves. It's a subtrait of Sized
and is defined like this:
#![allow(unused)] fn main() { trait Clone: Sized { fn clone(&self) -> Self; fn clone_from(&mut self, source: &Self) { *self = source.clone() } } }
Warning: Cloning values can be computationally expensive!
The clone_from
method modifies self
into a copy of source
.
Convention: In generic code, you should use
clone_from
whenever possible.
Tip: If your
Clone
implementation simply appliesclone
to each field of your type, then Rust can implement it for you by adding#[derive(Clone)]
above your type definition.
Warning: The
clone
method of types that implementClone
must be infallible!
Copy
A type is Copy
if it implements the std::marker::Copy
marker trait, a subtrait of Clone
and defined as:
#![allow(unused)] fn main() { trait Copy: Clone {} }
Tip: Like
Clone
,Copy
can be automatically implemented using#[derive(Copy)]
.
Deref
and DerefMut
You can specify how dereferencing operators like *
and .
behave on your types by implementing the std::ops::Deref
and std::ops::DerefMut
traits:
#![allow(unused)] fn main() { trait Deref { type Target: ?Sized; fn deref(&self) -> &Self::Target; } // DerefMut is a subtrait of Deref trait DerefMut: Deref { fn deref_mut(&mut self) -> &mut Self::Target; } }
Term: If inserting a
deref
call prevents a type mismatch, Rust insterts one for you. These are called deref coercions: one type is "coerced" into behaving as another.
Add notes about the implications of deref coercions.
Rust will apply several deref coercions in succession if necessary.
Use Case: The
Deref
andDerefMut
traits are designed for implementing smart pointer types likeBox
,Rc
, andArc
, and types that serve as owning versions of something you would frequently use by reference, the wayVec<T>
andString
serve as owning versions of[T]
and[str]
.
Anti-Pattern: Do not implement
Deref
andDerefMut
for a type just to make theTarget
type's methods appear on it automatically, the way a C++ base class's methods are visible on a subclass.
Warning: Rust applies deref coercions to resolve type conflicts, but it does not apply them to satisfy bounds on type variables.
Example
Say we have a struct called Selector<T>
that has a field elements: Vec<T>
and a field named current: usize
that behaves like a pointer to the current element.
Given a value s
of type Selector<T>
, we want to be able to do these things:
- Use the expression
*s
to get the value of the current element - Apply methods implemented by the type of the currently pointed to element
- Change the value of the currently pointed to element with
*s = '?'
#![allow(unused)] fn main() { use std::ops::{Deref, DerefMut}; struct Selector<T> { elements: Vec<T>, current: usize, } // Implementing Deref allows us to use *s to get the current element impl<T> Deref for Selector<T> { type Target = T; fn deref(&self) -> &T { &self.elements[self.current] } } // Implementing DerefMut allows us to set the value of the current element impl<T> DerefMut for Selector<T> { fn deref_mut(&mut self) -> &mut T { &mut self.elements[self.current] } } let mut s = Selector { elements: vec!['x', 'y', 'z'], current: 2 }; println!("current element {}", *s); println!("is alphabetic? {}", s.is_alphabetic()); *s = 'w'; println!("current element {}", *s); }
Default
Types with a reasonably obvious default value can implement the std::default::Default
trait:
#![allow(unused)] fn main() { trait Default { fn default() -> Self; } }
All of Rust's collection types (like Vec
, HashMap
, BinaryHeap
, etc) implement Default
, with default
methods that return an empty collection.
Use Case:
Default
is commonly used to produce default values for structs that represent a large collection of parameters, most of which you won't usually want to change. (thinkoptions
objects in JS)
A perfect example of making good use of Default
is when using the OpenGL crate called glium
. Drawing with OpenGL requires a ton of parameters, most of which you don't care about. So, we can use Default
to provide those parameters for us:
#![allow(unused)] fn main() { let params = glium::DrawParameters { line_width: Some(0.02), point_size: Some(0.02), .. Default::default(), } target.draw(..., ¶ms).unwrap(); }
Tip: Rust does not implicitly implement
Default
for struct types, but if all of a struct's fields implementDefault
, you can implementDefault
for the struct automatically using#[derive(Default)]
.
AsRef
and AsMut
When a type implements AsRef<T>
, that means you can borrow a &T
from it efficiently; AsMut
is the analogue for mutable references:
#![allow(unused)] fn main() { trait AsRef<T: ?Sized> { fn as_ref(&self) -> &T; } trait AsMut<T: ?Sized> { fn as_ref(&mut self) -> &mut T; } }
Use Case:
AsRef
is typically used to make functions more flexible in the argument types they accept. For instance,std::fs::File::open
is declared like this:#![allow(unused)] fn main() { fn open<P: AsRef<Path>>(path: P) -> Result<File>; }
Concept: You can think of using
AsRef
as a type bound as a bit like function overloading.
In the above use case, what open
really wants is a &Path
, the type representing a filesystem path. But with the above declaration, open
accepts anything it can borrow a &Path
from--that is, anything that implements AsRef<Path>
.
Use Case: It only makes sense for a type to implement
AsMut<T>
if modifying the givenT
cannot violate the type's invariants.
Anti-Pattern: You should avoid defining your own
AsFoo
traits when you could just implementAsRef<Foo>
instead.
Borrow
and BorrowMut
The std::borrow::Borrow
trait is similar to AsRef
: if a type implements Borrow<T>
, then its borrow
method efficiently borrows a &T
from it. But Borrow
imposes more restrictions.
Use Case: A type should implement
Borrow<T>
ony when a&T
hashes and compares the same way as the value it's borrowed from.Borrow
is valuable in dealing with keys in hash tables and trees, or when dealing with values that will be hashed or compared for some other reason.
Come back to the example implementation related to HashMap
.
From
and Into
The std::convert::From
and std::convert::To
traits represent conversions that consume a value of one type, and return a value of another.
From
and Into
do not borrow; they take ownership of their argument, transform it, and then return ownership of the result back to the caller:
#![allow(unused)] fn main() { trait Into<T>: Sized { fn into(self) -> T; } trait From<T>: Sized { fn from(T) -> Self; } }
Use Case:
Into
is generally used to make functions more flexible in the arguments they accept. For example, in this code, theping
function can accept any typeA
that can be converted into anIpv4Addr
:#![allow(unused)] fn main() { fn ping<A>(address: A) -> std::io::Result<bool> where A: Into<Ipv4Addr> { let ipv4_address = address.into(); ... } }
Use Case: The
from
method ofFrom
serves as a generic constructor for producing an instance of a type from some other single value.
Tip: Given an appropriate
From
implementation, you get theInto
trait implementation trait for free!
Warning:
from
andinto
operations must be infallible!
ToOwned
The ToOwned
trait is an alternative to Clone
. Types that implement Clone
must be sized. The std::borrow::ToOwned
trait provides a slightly looser way to convert a reference to an owned value:
#![allow(unused)] fn main() { trait ToOwned { type Owned: Borrow<Self>; fn to_owned(&self) -> Self::Owned; } }
Borrow
and ToOwned
at Work: The Humble Cow
In some cases, you cannot decide whether to borrow or own a value until the program is running. The std::borrow::Cow
type ("clone on write") provides one way to do this:
#![allow(unused)] fn main() { enum Cow<'a, B: ?Sized + 'a> where B: ToOwned { Borrowed(&'a B), Owned(<B as ToOwned>::Owned), } }
Concept: A
Cow<B>
either borrows a shared reference toB
, or owns a value from which we could borrow such a reference.
Use Case: One common use for
Cow
is to return either a statically allocated string constant or a computed string.
Example
Suppose you need to convert an error enum to a message via a function called describe
.
Most variants can be handled with fixed strings, but others have additional data that should be included in the message. For such a case, you can return Cow<'static, str>
.
Using Cow
helps describe
and its callers put off allocation until the moment it becomes necessary.
#![allow(unused)] fn main() { use std::borrow::Cow; fn describe(error: &Error) -> Cow<'static, str> { match *error { Error::OutOfMemory => "out of memory".into(), Error::StackOverflow => "stack overflow".into(), Error::MachineOnFire => "machine on fire".into(), Error::Unfathomable => "machine bewildered".into(), Error::FileNotFound(ref path) => { format!("file not found: {}", path.display()).into() } } } }