Tour of Rust’s Standard Library Traits github.com/pretzelhamm… Praying for Rust
Contents ✅ ⏰
- The introduction ✅
- Trait based ✅
- Automatic Trait ✅
- Generic Trait ✅
- Formatting Trait ✅
- Operator Trait ⏰ = > ✅
- Conversion Trait ⏰
- Error handling ⏰
- The iterator traits ⏰
- The I/O Trait ⏰
- Conclusion ⏰
Arithmetic Traits (Arithmetic Traits)
Trait(s) | Category (Category) | Operator(s) | Description: |
---|---|---|---|
Add |
The arithmetic | + |
add |
AddAssign |
The arithmetic | + = |
Add and assign |
BitAnd |
The arithmetic | & |
Bitwise and |
BitAndAssign |
The arithmetic | & = |
Assignment by bitwise and union |
BitXor |
The arithmetic | ^ |
The bitwise exclusive or |
BitXorAssign |
The arithmetic | ^ = |
Bitwise xor union assignment |
Div |
The arithmetic | / |
In addition to |
DivAssign |
The arithmetic | / = |
In addition to the assignment |
Mul |
The arithmetic | * |
take |
MulAssign |
The arithmetic | * = |
Take and assignment |
Neg |
The arithmetic | - |
A dollar complementation |
Not |
The arithmetic | ! |
Unary logic inverse |
Rem |
The arithmetic | % |
For more than |
RemAssign |
The arithmetic | % = |
Take the remainder and assign |
Shl |
The arithmetic | << |
Shift to the left |
ShlAssign |
The arithmetic | < < = |
Shift left and assign |
Shr |
The arithmetic | >> |
Moves to the right |
ShrAssign |
The arithmetic | > > = |
Shift right and assign |
Sub |
The arithmetic | - |
Reduction of |
SubAssign |
The arithmetic | - = |
Reduction and assignment |
It is not necessary to go through all the arithmetic operators; after all, most of them only work on numeric types. We’ll talk about Add and AddAssign, because the + operator is often overridden to do other things, such as adding an item to a collection or concatenating, so that we can start with the most interesting places without having to repeat ourselves.
Add & AddAssign
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
Copy the code
The Add
type can be added to the Rhs type and produce a T as Output.
Add
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,}impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = p1 + p2;
assert_eq!(p3.x, p1.x + p2.x); / / ✅
assert_eq!(p3.y, p1.y + p2.y); / / ✅
}
Copy the code
But what if we only have a reference to Point? Can we add them? Let’s try:
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = &p1 + &p2; / / ❌
}
Copy the code
Obviously not, the compiler throws the following prompt:
error[E0369]: cannot add `&Point` to `&Point`
--> src/main.rs:50:25
|
50 | let p3: Point = &p1 + &p2;
| --- ^ --- &Point
| |
| &Point
|
= note: an implementation of `std::ops::Add` might be missing for `&Point`
Copy the code
In Rust’s type system, T, &T, &mut T are all considered completely different types for a given type T, which means we have to provide implementations of traits for each of them. Let’s implement Add for &point:
impl Add for &Point {
type Output = Point;
fn add(self, rhs: &Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let p3 = &p1 + &p2; / / ✅
assert_eq!(p3.x, p1.x + p2.x); / / ✅
assert_eq!(p3.y, p1.y + p2.y); / / ✅
}
Copy the code
Still, something didn’t feel right. We implemented two Adds for Point and &Point, which happen to do the same thing today, but we can’t guarantee that will happen in the future. For example, suppose we decide that when we Add two points, we want to create a Line type containing both points instead of creating a new Point, then we update the implementation of Add:
use std::ops::Add;
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,}#[derive(Copy, Clone)]
struct Line {
start: Point,
end: Point,
}
// we updated this impl
impl Add for Point {
type Output = Line;
fn add(self, rhs: Point) -> Line {
Line {
start: self,
end: rhs,
}
}
}
// but forgot to update this impl, uh oh!
impl Add for &Point {
type Output = Point;
fn add(self, rhs: &Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let line: Line = p1 + p2; / / ✅
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let line: Line = &p1 + &p2; // ❌ expected Line, found Point
}
Copy the code
Our current Add implementation for &Point created an unnecessary maintenance burden, and we wanted the implementation to automatically match the Point implementation without having to manually maintain updates every time we changed the Point implementation. We want to keep our code as DRY as possible (Don’t Repeat Yourself). Fortunately, this is possible:
// updated, DRY impl
impl Add for &Point {
type Output = <Point as Add>::Output;
fn add(self, rhs: &Point) -> Self::Output {
Point::add(*self, *rhs)
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let line: Line = p1 + p2; / / ✅
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let line: Line = &p1 + &p2; / / ✅
}
Copy the code
The AddAssign
type allows us to add and assign to the Rhs type. The trait declares the following:
trait AddAssign<Rhs = Self> {
fn add_assign(&mut self, rhs: Rhs);
}
Copy the code
Take Point and &Point for example:
use std::ops::AddAssign;
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32
}
impl AddAssign for Point {
fn add_assign(&mut self, rhs: Point) {
self.x += rhs.x;
self.y += rhs.y; }}impl AddAssign<&Point> for Point {
fn add_assign(&mut self, rhs: &Point) {
Point::add_assign(self, *rhs); }}fn main() {
let mut p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
p1 += &p2;
p1 += p2;
assert!(p1.x == 7 && p1.y == 10);
}
Copy the code
Closure Traits
Trait(s) | Category (Category) | Operator(s) | Description: |
---|---|---|---|
Fn |
closure | (... args) |
Immutable closure calls |
FnMut |
closure | (... args) |
Mutable closure calls |
FnOnce |
closure | (... args) |
A one-time closure call |
FnOnce, FnMut, & Fn
trait FnOnce<Args> {
type Output;
fn call_once(self, args: Args) -> Self::Output;
}
trait FnMut<Args>: FnOnce<Args> {
fn call_mut(&mut self, args: Args) -> Self::Output;
}
trait Fn<Args>: FnMut<Args> {
fn call(&self, args: Args) -> Self::Output;
}
Copy the code
These traits exist, but in Rust of stable, we cannot implement them for our own types. The only type we can create that implements these traits is a closure. A closure decides whether it implements FnOnce, FnMut, or Fn based on what it captures from the environment.
The FnOnce closure can only be called once, because it consumes certain values during execution:
fn main() {
let range = 0.10;
let get_range_count = || range.count();
assert_eq!(get_range_count(), 10); / / ✅
get_range_count(); / / ❌
}
Copy the code
The.count() method on an iterator consumes the iterator, so it can only be called once. Therefore, our closure can only be called once. That’s why we get the following error when we try to call it a second time:
error[E0382]: use of moved value: `get_range_count`
--> src/main.rs:5:5| 4 | assert_eq! (get_range_count(), 10); | ----------------- `get_range_count` moved due to this call 5 | get_range_count(); | ^^^^^^^^^^^^^^^ value used here after move | note: closure cannot be invoked more than once because it moves the variable `range` out of its environment --> src/main.rs:3:30
|
3 | let get_range_count = || range.count();
| ^^^^^
note: this value implements `FnOnce`, which causes it to be moved when called
--> src/main.rs:4:16| 4 | assert_eq! (get_range_count(), 10); | ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^Copy the code
The FnMut closure can be called multiple times and can modify the variables it captures from the environment. We could say FnMut was ateful or stateful. Here is an example closure that filters all non-ascending values by tracing the minimum it sees from an iterator.
fn main() {
let nums = vec![0.4.2.8.10.7.15.18.13];
let mut min = i32::MIN;
let ascending = nums.into_iter().filter(|&n| {
if n <= min {
false
} else {
min = n;
true
}
}).collect::<VecThe < _ > > ();assert_eq!(vec![0.4.8.10.15.18], ascending); / / ✅
}
Copy the code
FnOnce takes ownership of its arguments and can only be called once, whereas FnMut only requires mutable references to arguments and can be called multiple times. FnMut refines FnOnce in this sense. FnMut can be used anywhere FnOnce can be used.
The Fn closure can also be called multiple times, but it cannot modify variables captured from the environment. We can say that Fn closures have no side effects or are stateless. Here is an example of filtering out all numbers from an iterator that are less than a variable on the stack that was captured in the environment:
fn main() {
let nums = vec![0.4.2.8.10.7.15.18.13];
let min = 9;
let greater_than_9 = nums.into_iter().filter(|&n| n > min).collect::<VecThe < _ > > ();assert_eq!(vec![10.15.18.13], greater_than_9); / / ✅
}
Copy the code
Fn refines FnMut in the sense that FnMut requires mutable references and can be called multiple times, while Fn requires immutable references and can be called multiple times. Fn can be used anywhere FnMut can be used, including, of course, where FnOnce can be used.
If a closure does not capture any variables from the environment, it is technically not a closure, but rather an inline function that is anonymously declared, and can be used and passed as a normal function pointer (Fn), including where FnMut and FnOnce can be used.
fn add_one(x: i32) - >i32 {
x + 1
}
fn main() {
let mut fn_ptr: fn(i32) - >i32 = add_one;
assert_eq!(fn_ptr(1), 2); / / ✅
// capture-less closure cast to fn pointer
fn_ptr = |x| x + 1; // same as add_one
assert_eq!(fn_ptr(1), 2); / / ✅
}
Copy the code
Here is an example of passing a normal function pointer instead of a closure:
fn main() {
let nums = vec![-1.1, -2.2, -3.3];
let absolutes: Vec<i32> = nums.into_iter().map(i32::abs).collect();
assert_eq!(vec![1.1.2.2.3.3], absolutes); / / ✅
}
Copy the code
Other Traits (Traits)
Trait(s) | Category (Category) | Operator(s) | Description: |
---|---|---|---|
Deref |
other | * |
Immutable dereference |
DerefMut |
other | * |
Variable dereference |
Drop |
other | – | Type destructor |
Index |
other | [] |
Immutable index |
IndexMut |
other | [] |
The variable index |
RangeBounds |
other | . |
interval |
trait Deref {
type Target:?Sized;
fn deref(&self) -> &Self::Target;
}
trait DerefMut: Deref {
fn deref_mut(&mut self) - > &mut Self::Target;
}
Copy the code
The Deref
type can be dereferenced to type T using the * operator. This has obvious use cases in smart pointer types like Box and Rc. However, we rarely see this kind of explicit dereference in Rust code because Rust has a feature called deref coercion.
Rust automatically dereferences types when they are passed as a function parameter, returned from a function, or as part of a method call. This also explains why we can pass &string and &vec
in a function that expects &str and &[T], because String implements Deref
and Vec
implements Deref
.
Deref and DerefMut should only be implemented for smart pointer types. The most common way people misuse and abuse these traits is by trying to stuff OOP (Object-oriented programming) -style data inheritance into Rust. That’s not going to work. Rust is not OOP. Let’s run some tests to see where, how and why it doesn’t work. Let’s start with the following example:
use std::ops::Deref;
struct Human {
health_points: u32,}enum Weapon {
Spear,
Axe,
Sword,
}
// a Soldier is just a Human with a Weapon
struct Soldier {
human: Human,
weapon: Weapon,
}
impl Deref for Soldier {
type Target = Human;
fn deref(&self) -> &Human {
&self.human
}
}
enum Mount {
Horse,
Donkey,
Cow,
}
// a Knight is just a Soldier with a Mount
struct Knight {
soldier: Soldier,
mount: Mount,
}
impl Deref for Knight {
type Target = Soldier;
fn deref(&self) -> &Soldier {
&self.soldier
}
}
enum Spell {
MagicMissile,
FireBolt,
ThornWhip,
}
// a Mage is just a Human who can cast Spells
struct Mage {
human: Human,
spells: Vec<Spell>,
}
impl Deref for Mage {
type Target = Human;
fn deref(&self) -> &Human {
&self.human
}
}
enum Staff {
Wooden,
Metallic,
Plastic,
}
// a Wizard is just a Mage with a Staff
struct Wizard {
mage: Mage,
staff: Staff,
}
impl Deref for Wizard {
type Target = Mage;
fn deref(&self) -> &Mage {
&self.mage
}
}
fn borrows_human(human: &Human) {}
fn borrows_soldier(soldier: &Soldier) {}
fn borrows_knight(knight: &Knight) {}
fn borrows_mage(mage: &Mage) {}
fn borrows_wizard(wizard: &Wizard) {}
fn example(human: Human, soldier: Soldier, knight: Knight, mage: Mage, wizard: Wizard) {
// all types can be used as Humans
borrows_human(&human);
borrows_human(&soldier);
borrows_human(&knight);
borrows_human(&mage);
borrows_human(&wizard);
// Knights can be used as Soldiers
borrows_soldier(&soldier);
borrows_soldier(&knight);
// Wizards can be used as Mages
borrows_mage(&mage);
borrows_mage(&wizard);
// Knights & Wizards passed as themselves
borrows_knight(&knight);
borrows_wizard(&wizard);
}
Copy the code
At first glance, the above code seems fine! But, on closer inspection, it’s not so good. First, the dereference cast only works on references, so it doesn’t work when we want to pass ownership:
fn takes_human(human: Human) {}
fn example(human: Human, soldier: Soldier, knight: Knight, mage: Mage, wizard: Wizard) {
// all types CANNOT be used as Humans
takes_human(human);
takes_human(soldier); / / ❌
takes_human(knight); / / ❌
takes_human(mage); / / ❌
takes_human(wizard); / / ❌
}
Copy the code
In addition, dereference casts do not work in a generic context. Suppose we implement a trait only on humans:
trait Rest {
fn rest(&self);
}
impl Rest for Human {
fn rest(&self) {}}fn take_rest<T: Rest>(rester: &T) {
rester.rest()
}
fn example(human: Human, soldier: Soldier, knight: Knight, mage: Mage, wizard: Wizard) {
// all types CANNOT be used as Rest types, only Human
take_rest(&human);
take_rest(&soldier); / / ❌
take_rest(&knight); / / ❌
take_rest(&mage); / / ❌
take_rest(&wizard); / / ❌
}
Copy the code
And, while dereference casts can be used in many scenarios, they are not a panacea. It does not work on operands, even though operators are just syntactic sugar for method calls. Suppose we want the Mage to learn Spell via the += operator:
impl DerefMut for Wizard {
fn deref_mut(&mut self) - > &mut Mage {
&mut self.mage
}
}
impl AddAssign<Spell> for Mage {
fn add_assign(&mut self, spell: Spell) {
self.spells.push(spell); }}fn example(mut mage: Mage, mut wizard: Wizard, spell: Spell) {
mage += spell;
wizard += spell; // ❌ wizard not coerced to mage here
wizard.add_assign(spell); Oof, we have to call it like this 🤦
}
Copy the code
In programming languages with OOP style data inheritance, the value of self in a method is always equal to the type calling the method, but in Rust, the value of self is always equal to the type implementing the method:
struct Human {
profession: &'static str,
health_points: u32,}impl Human {
// self will always be a Human here, even if we call it on a Soldier
fn state_profession(&self) {
println!("I'm a {}!".self.profession); }}struct Soldier {
profession: &'static str,
human: Human,
weapon: Weapon,
}
fn example(soldier: &Soldier) {
assert_eq!("servant", soldier.human.profession);
assert_eq!("spearman", soldier.profession);
soldier.human.state_profession(); // prints "I'm a servant!"
soldier.state_profession(); // still prints "I'm a servant!" 🤦
}
Copy the code
The pitfalls above are shocking when implementing Deref or DerefMut on a new type. Suppose we want to create a SortedVec type, which is just a Vec but ordered. Here’s how we might do it:
struct SortedVec<T: Ord> (Vec<T>);
impl<T: Ord> SortedVec<T> {
fn new(mut vec: Vec<T>) -> Self {
vec.sort();
SortedVec(vec)
}
fn push(&mut self, t: T) {
self.0.push(t);
self.0.sort(); }}Copy the code
Obviously, we can’t implement DerefMut
> here, otherwise anyone using SortedVec could easily break the sorted order. But is it safe to implement Deref
>? Try to find bugs in the following programs:
use std::ops::Deref;
struct SortedVec<T: Ord> (Vec<T>);
impl<T: Ord> SortedVec<T> {
fn new(mut vec: Vec<T>) -> Self {
vec.sort();
SortedVec(vec)
}
fn push(&mut self, t: T) {
self.0.push(t);
self.0.sort(); }}impl<T: Ord> Deref for SortedVec<T> {
type Target = Vec<T>;
fn deref(&self) - > &Vec<T> {
&self.0}}fn main() {
let sorted = SortedVec::new(vec![2.8.6.3]);
sorted.push(1);
let sortedClone = sorted.clone();
sortedClone.push(4);
}
Copy the code
We haven’t implemented Clone for SortedVec, so when we call the.clone() method, the compiler resolves it to a method call on Vec using a dereference cast, so it returns a Vec instead of a SortedVec!
fn main() {
let sorted: SortedVec<i32> = SortedVec::new(vec![2.8.6.3]);
sorted.push(1); // still sorted
// calling clone on SortedVec actually returns a Vec 🤦
let sortedClone: Vec<i32> = sorted.clone();
sortedClone.push(4); SortedClone no longer sorted 💀
}
Copy the code
Either way, the above limitations, constraints, or traps are not Rust’s fault, because Rust was never designed to be an OO (object-oriented) language or to support OOP (Object-oriented programming) patterns in the first place.
The point of this section is not to try to get clever with Deref and DerefMut implementations. They only work with unstable smart pointer types and are currently only available in the standard library because smart pointer types currently require the feature unstable and compiler magic to work. If we want functionality and behavior similar to Deref and DerefMut, we can look at AsRef and AsMut, which will be mentioned later.
Index & IndexMut
trait Index<Idx: ?Sized> {
type Output:?Sized;
fn index(&self, index: Idx) -> &Self::Output;
}
trait IndexMut<Idx>: Index<Idx> where Idx: ?Sized {
fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}
Copy the code
We can Index [] to Index
with T. The Index operation returns &u. For syntactic purposes, the compiler automatically inserts a dereference operator * before the return value of the index operation:
fn main() {
// Vec<i32> impls Index<usize, Output = i32> so
// indexing Vec<i32> should produce &i32s and yet...
let vec = vec![1.2.3.4.5];
let num_ref: &i32 = vec[0]; // ❌ expected &i32 found i32
// above line actually desugars to
let num_ref: &i32 = *vec[0]; // ❌ expected &i32 found i32
// both of these alternatives work
let num: i32 = vec[0]; / / ✅
let num_ref = &vec[0]; / / ✅
}
Copy the code
To show how we can implement Index ourselves, here is an interesting example that shows how we can implement wrap indexes and non-negative indexes on Vec using a new type and Indextrait:
use std::ops::Index;
struct WrappingIndex<T>(Vec<T>);
impl<T> Index<usize> for WrappingIndex<T> {
type Output = T;
fn index(&self, index: usize) -> &T {
&self.0[index % self.0.len()]
}
}
impl<T> Index<i128> for WrappingIndex<T> {
type Output = T;
fn index(&self, index: i128) -> &T {
let self_len = self.0.len() as i128;
let idx = (((index % self_len) + self_len) % self_len) as usize;
&self.0[idx]
}
}
#[test] / / ✅
fn indexes() {
let wrapping_vec = WrappingIndex(vec![1.2.3]);
assert_eq!(1, wrapping_vec[0_usize]);
assert_eq!(2, wrapping_vec[1_usize]);
assert_eq!(3, wrapping_vec[2_usize]);
}
#[test] / / ✅
fn wrapping_indexes() {
let wrapping_vec = WrappingIndex(vec![1.2.3]);
assert_eq!(1, wrapping_vec[3_usize]);
assert_eq!(2, wrapping_vec[4_usize]);
assert_eq!(3, wrapping_vec[5_usize]);
}
#[test] / / ✅
fn neg_indexes() {
let wrapping_vec = WrappingIndex(vec![1.2.3]);
assert_eq!(1, wrapping_vec[-3_i128]);
assert_eq!(2, wrapping_vec[-2_i128]);
assert_eq!(3, wrapping_vec[-1_i128]);
}
#[test] / / ✅
fn wrapping_neg_indexes() {
let wrapping_vec = WrappingIndex(vec![1.2.3]);
assert_eq!(1, wrapping_vec[-6_i128]);
assert_eq!(2, wrapping_vec[-5_i128]);
assert_eq!(3, wrapping_vec[-4_i128]);
}
Copy the code
There is no requirement that Idx be a numeric type or a Range, it can also be an enumeration! Here is an example of using basketball positions to retrieve players on a team:
use std::ops::Index;
enum BasketballPosition {
PointGuard,
ShootingGuard,
Center,
PowerForward,
SmallForward,
}
struct BasketballPlayer {
name: &'static str,
position: BasketballPosition,
}
struct BasketballTeam {
point_guard: BasketballPlayer,
shooting_guard: BasketballPlayer,
center: BasketballPlayer,
power_forward: BasketballPlayer,
small_forward: BasketballPlayer,
}
impl Index<BasketballPosition> for BasketballTeam {
type Output = BasketballPlayer;
fn index(&self, position: BasketballPosition) -> &BasketballPlayer {
match position {
BasketballPosition::PointGuard => &self.point_guard,
BasketballPosition::ShootingGuard => &self.shooting_guard,
BasketballPosition::Center => &self.center,
BasketballPosition::PowerForward => &self.power_forward,
BasketballPosition::SmallForward => &self.small_forward,
}
}
}
Copy the code
Drop
trait Drop {
fn drop(&mut self);
}
Copy the code
If a type implements Drop, then Drop will be called before the type leaves scope but is destroyed. We rarely need to implement this for our types, but it is useful if a type holds some external resources that need to be cleaned up when the type is destroyed.
There is a BufWriter type in the library that allows us to buffer written data into a Write type. But what if the BufWriter is destroyed before its contents are brushed into the underlying Write type? Fortunately that’s not possible! BufWriter implements the Droptrait, so whenever it leaves scope, Flush is always called!
impl<W: Write> Drop for BufWriter<W> {
fn drop(&mut self) {
self.flush_buf(); }}Copy the code
In addition, Mutexs in Rust do not have unlock() methods because they are not required! Calling lock() on Mutex returns a MutexGuard, which automatically unlocks Mutex when the MutexGuard leaves scope, thanks to its Drop implementation:
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
unsafe {
self.lock.inner.raw_unlock(); }}}Copy the code
In general, if you’re implementing an abstraction of a type of resource that needs to be cleaned up after use, it’s time to make the most of the Drop trait.