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 ⏰

Formatting Traits

We can serialize a type into a string using formatting macros in STD :: FMT, the best known of which is println! . We can pass formatting parameters to {} placeholders, which are used to choose which traits to serialize placeholder parameters.

Trait Placeholder Description
Display {} According to said
Debug {:? } Debugging said
Octal {:o} Octal representation
LowerHex {:x} The value is in lowercase hexadecimal format
UpperHex {:X} The value is in uppercase hexadecimal notation
Pointer {:p} Memory address
Binary {:b} Binary representation
LowerExp {:e} Lower case exponent representation
UpperExp {:E} Capital exponent

Display & ToString

trait Display {
    fn fmt(&self, f: &mut Formatter<'_- > >)Result;
}
Copy the code

The Display type can be serialized to the more user-friendly String type. In the Point type column:

use std::fmt;

#[derive(Default)]
struct Point {
    x: i32,
    y: i32,}impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})".self.x, self.y)
    }
}

fn main() {
    println!("origin: {}", Point::default());
    // prints "origin: (0, 0)"

    // get Point's Display representation as a String
    let stringified_point = format!("{}", Point::default());
    assert_eq!("(0, 0)", stringified_point); / / ✅
}
Copy the code

In addition to using format! Macros make a type appear as a String. We can also use the ToString trait:

trait ToString {
    fn to_string(&self) - >String;
}
Copy the code

This trait doesn’t need to be implemented. In fact, we can’t implement it due to the Generic Blanket impl, because all types that implement Display automatically implement ToString:

impl<T: Display + ?Sized> ToString for T;
Copy the code

Using ToString on Point:

#[test] / / ✅
fn display_point() {
    let origin = Point::default();
    assert_eq!(format!("{}", origin), "(0, 0)");
}

#[test] / / ✅
fn point_to_string() {
    let origin = Point::default();
    assert_eq!(origin.to_string(), "(0, 0)");
}

#[test] / / ✅
fn display_equals_to_string() {
    let origin = Point::default();
    assert_eq!(format!("{}", origin), origin.to_string());
}
Copy the code

Debug

trait Debug {
    fn fmt(&self, f: &mut Formatter<'_- > >)Result;
}
Copy the code

Debug and Display have the same signature. The only difference is that {:? } will call the Debug implementation. Debug can be derived:

use std::fmt;

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,}// derive macro generates impl below
impl fmt::Debug for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Point")
            .field("x", &self.x)
            .field("y", &self.y)
            .finish()
    }
}
Copy the code

Implementing Debug for a type enables that type to be used in DBG! DBG! Macros are faster at printing logs than println! Some of its advantages are as follows:

  1. dbg!Print to stderr instead of stdout, so we can easily differentiate the output from standard output in our program.
  2. dbg!It is printed along with the expression passed in and the result of the expression’s evaluation.
  3. dbg!Takes ownership of the passed argument and returns it, so you can use it in expressions:
fn some_condition() - >bool {
    true
}

// no logging
fn example() {
    if some_condition() {
        // some code}}// println! logging
fn example_println() {
    / / 🤦
    let result = some_condition();
    println!("{}", result); // just prints "true"
    if result {
        // some code}}// dbg! logging
fn example_dbg() {
    / / 😍
    ifdbg! (some_condition()) {// prints "[src/main.rs:22] some_condition() = true"
        // some code}}Copy the code

dbg! The only downside of the release is that it doesn’t crop automatically in the release build, so we’ll have to remove it manually if we don’t want to include it in the final generated binary.

The Operator Trait

All operators in Rust are associated with traits, and if we want to implement some operators for our types, we must implement the traits associated with them.

Trait(s) Category (Category) Operator(s) Description:
Eq.PartialEq To compare = = equal
Ord.PartialOrd To compare <.>.< =.> = To compare
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
Fn closure (... args) Immutable closure calls
FnMut closure (... args) Mutable closure calls
FnOnce closure (... args) A one-time closure call
Deref other * Immutable dereference
DerefMut other * Variable dereference
Drop other Type destructor
Index other [] Immutable index
IndexMut other [] The variable index
RangeBounds other . interval

Comparison Traits

Trait(s) Category (Category) Operator(s) Description:
Eq.PartialEq To compare = = equal
Ord.PartialOrd To compare <.>.< =.> = To compare

PartialEq & Eq

trait PartialEq<Rhs = Self>
where
    Rhs: ?Sized,
{
    fn eq(&self, other: &Rhs) -> bool;

    // provided default impls
    fn ne(&self, other: &Rhs) -> bool;
}

Copy the code

The PartialEq

type can be checked for equality with the Rhs type using the == operator.

All PartialEq

implementations must ensure that equality is symmetric and transitive. This means that for any a, B, and C:

  • a == bAlso means thatb == a(Symmetry)
  • a == b && b == cmeansa == c(Transitivity)

By default, Rhs = Self, because we almost always want to compare different instances of the same type, not different instances of different types. This also ensures that our implementation is symmetric and transitive.

struct Point {
    x: i32,
    y: i32
}

// Rhs == Self == Point
impl PartialEq for Point {
    // impl automatically symmetric & transitive
    fn eq(&self, other: &Point) -> bool {
        self.x == other.x && self.y == other.y
    }
}
Copy the code

If all members of a type implement PartialEq, it derives the PartialEq implementation:

#[derive(PartialEq)]
struct Point {
    x: i32,
    y: i32
}

#[derive(PartialEq)]
enum Suit {
    Spade,
    Heart,
    Club,
    Diamond,
}

Copy the code

Once we implement PartialEq for our own type, we can easily compare equality between references to types thanks to the Generic Blanket Impls:

// this impl only gives us: Point == Point
#[derive(PartialEq)]
struct Point {
    x: i32,
    y: i32
}

// all of the generic blanket impls below
// are provided by the standard library

// this impl gives us: &Point == &Point
impl<A, B> PartialEqThe < &'_ B> for &'_ A
where A: PartialEq<B> + ?Sized, B: ?Sized;

// this impl gives us: &mut Point == &Point
impl<A, B> PartialEqThe < &'_ B> for &'_ mut A
where A: PartialEq<B> + ?Sized, B: ?Sized;

// this impl gives us: &Point == &mut Point
impl<A, B> PartialEqThe < &'_ mut B> for &'_ A
where A: PartialEq<B> + ?Sized, B: ?Sized;

// this impl gives us: &mut Point == &mut Point
impl<A, B> PartialEqThe < &'_ mut B> for &'_ mut A
where A: PartialEq<B> + ?Sized, B: ?Sized;

Copy the code

Because the trait is generic, we can define equality (comparison) between different types. The library takes advantage of this to compare String types like String, & STR, PathBuf, &Path, OsString, &OsStr, and so on.

In general, we should implement equality only between specific different types that contain the same type of data, and the only differences between them are the way they represent and interact with the data.

Here is a negative example of someone trying to implement PartialEq to check integrity between types that do not meet the above rules:

#[derive(PartialEq)]
enum Suit {
    Spade,
    Club,
    Heart,
    Diamond,
}

#[derive(PartialEq)]
enum Rank {
    Ace,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine,
    Ten,
    Jack,
    Queen,
    King,
}

#[derive(PartialEq)]
struct Card {
    suit: Suit,
    rank: Rank,
}

// check equality of Card's suit
impl PartialEq<Suit> for Card {
    fn eq(&self, other: &Suit) -> bool {
        self.suit == *other
    }
}

// check equality of Card's rank
impl PartialEq<Rank> for Card {
    fn eq(&self, other: &Rank) -> bool {
        self.rank == *other
    }
}

fn main() {
    let AceOfSpades = Card {
        suit: Suit::Spade,
        rank: Rank::Ace,
    };
    assert!(AceOfSpades == Suit::Spade); / / ✅
    assert!(AceOfSpades == Rank::Ace); / / ✅
}

Copy the code

Eq is a tagged trait and is a subtrait of PartialEq

.

trait Eq: PartialEq<Self> {}
Copy the code

If we implement Eq for a type, on top of the symmetry and transitivity required by the PartialEq, we also guarantee reflexivity, that is, for any A, a == a. In this sense, Eq refines the PartialEq because it represents a more rigorous equality. If all members of a type implement Eq, then the implementation of Eq can be derived from that type.

Floating point implements PartialEq but not Eq because NaN! = NaN. Almost all other types that implement PartialEq implement Eq, unless they contain floating point types.

Once a type implements PartialEq and Debug, we can then use assert_eq! Use it in macros. We can also compare collections that implement the PartialEq type.

#[derive(PartialEq, Debug)]
struct Point {
    x: i32,
    y: i32,}fn example_assert(p1: Point, p2: Point) {
    assert_eq!(p1, p2);
}

fn example_compare_collections<T: PartialEq>(vec1: Vec<T>, vec2: Vec<T>) {
    // if T: PartialEq this now works!
    if vec1 == vec2 {
        // some code
    } else {
        // other code}}Copy the code

Hash

trait Hash {
    fn hash<H: Hasher>(&self, state: &mut H);

    // provided default impls
    fn hash_slice<H: Hasher>(data: &[Self], state: &mut H);
}
Copy the code

This trait is not associated with any operators, but the best time to discuss it is after the PartialEq and Eq, so it is written here. The Hash type can be hashed by a Hasher.

use std::hash::Hasher;
use std::hash::Hash;

struct Point {
    x: i32,
    y: i32,}impl Hash for Point {
    fn hash<H: Hasher>(&self, hasher: &mut H) {
        hasher.write_i32(self.x);
        hasher.write_i32(self.y); }}Copy the code

Derived macros can be used to generate the same implementation as above:

#[derive(Hash)]
struct Point {
    x: i32,
    y: i32,}Copy the code

If a type implements both Hash and Eq, then these implementations must agree to ensure that for all a and B, if a == b then A.hash () == b.hash(). Therefore, when implementing both traits for a type, either use derived macros or implement them manually, but don’t mix them, or we run the risk of breaking the above invariance.

The biggest benefit of implementing Eq and Hash for a type is that it allows us to store the type as a key in a HashMap and a HashSet.

use std::collections::HashSet;

// now our type can be stored
// in HashSets and HashMaps!
#[derive(PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,}fn example_hashset() {
    let mut points = HashSet::new();
    points.insert(Point { x: 0, y: 0 }); / / ✅
}

Copy the code

PartialOrd & Ord

enum Ordering {
    Less,
    Equal,
    Greater,
}

trait PartialOrd<Rhs = Self> :PartialEq<Rhs>
where
    Rhs: ?Sized,
{
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;

    // provided default impls
    fn lt(&self, other: &Rhs) -> bool;
    fn le(&self, other: &Rhs) -> bool;
    fn gt(&self, other: &Rhs) -> bool;
    fn ge(&self, other: &Rhs) -> bool;
}

Copy the code

The PartialOrd

type can be compared with the Rhs type using the <, <=, >= operators. All PartialOrd

implementations must be guaranteed to be asymmetric and transitive when comparing. This means that for any a, B, and C:

  • a < bmeans! (a>b)(Asymmetry)
  • a < b && b < cmeansa < c(Transitivity)

PartialOrd is a subtrait of PartialEq, and their implementations must be consistent with each other.

fn must_always_agree<T: PartialOrd + PartialEq>(t1: T, t2: T) {
    assert_eq!(t1.partial_cmp(&t2) == Some(Ordering::Equal), t1 == t2);
}

Copy the code

When comparing PartialEq type, we can check whether they are equal or unequal, but when comparing PartialOrd type, we can check whether they are equal or not equal to yourself, if they are not equal, we can also check are not equal because of the first item is less than a second or first is greater than the second.

By default, Rhs == Self, because we always want to compare instances of the same type, not instances of different types. This also automatically ensures that our implementation is symmetric and transitive.

use std::cmp::Ordering;

#[derive(PartialEq, PartialOrd)]
struct Point {
    x: i32,
    y: i32
}

// Rhs == Self == Point
impl PartialOrd for Point {
    // impl automatically symmetric & transitive
    fn partial_cmp(&self, other: &Point) -> Option<Ordering> {
        Some(match self.x.cmp(&other.x) {
            Ordering::Equal => self.y.cmp(&other.y),
            ordering => ordering,
        })
    }
}

Copy the code

A type can be derived if all its members implement PartialOrd:

#[derive(PartialEq, PartialOrd)]
struct Point {
    x: i32,
    y: i32,}#[derive(PartialEq, PartialOrd)]
enum Stoplight {
    Red,
    Yellow,
    Green,
}
Copy the code

The derived macro PartialOrd sorts their members according to lexicographical order:

// generates PartialOrd impl which orders
// Points based on x member first and
// y member second because that's the order
// they appear in the source code
#[derive(PartialOrd, PartialEq)]
struct Point {
    x: i32,
    y: i32,}// generates DIFFERENT PartialOrd impl
// which orders Points based on y member
// first and x member second
#[derive(PartialOrd, PartialEq)]
struct Point {
    y: i32,
    x: i32,}Copy the code

Ord is a subtrait of Eq and PartialOrd

:

trait Ord: Eq + PartialOrd<Self> {
    fn cmp(&self, other: &Self) -> Ordering;

    // provided default impls
    fn max(self, other: Self) - >Self;
    fn min(self, other: Self) - >Self;
    fn clamp(self, min: Self, max: Self) - >Self;
}

Copy the code

If we implement Ord for a type, in addition to the asymmetry and transitivity guaranteed by PartialOrd, we can also guarantee the asymmetry of the whole, that is, for any given a and B, one of a < b, a == b, or a > b must be true. In this sense, Ord refines Eq and PartialOrd because it represents a more rigorous comparison. If a type implements Ord, we can use this implementation to implement PartialOrd, PartialEq, and Eq:

use std::cmp::Ordering;

// of course we can use the derive macros here
#[derive(Ord, PartialOrd, Eq, PartialEq)]
struct Point {
    x: i32,
    y: i32,}// note: as with PartialOrd, the Ord derive macro
// orders a type based on the lexicographical order
// of its members

// but here's the impls if we wrote them out by hand
impl Ord for Point {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.x.cmp(&other.x) {
            Ordering::Equal => self.y.cmp(&other.y),
            ordering => ordering,
        }
    }
}
impl PartialOrd for Point {
    fn partial_cmp(&self, other: &Self) - >Option<Ordering> {
        Some(self.cmp(other))
    }
}
impl PartialEq for Point {
    fn eq(&self, other: &Self) - >bool {
        self.cmp(other) == Ordering::Equal
    }
}
impl Eq for Point {}

Copy the code

Floating point implements PartialOrd but not Ord because NaN < 0 == false and NaN >= 0 == false are both true. Almost all other PartialOrd types implement Ord, unless they contain floating point types.

Once a type implements Ord, we can store it in BTreeMap and BTreeSet, and sort it on slice using the sort() method. This also applies to other types that can dereference to slice, such as arrays, Vec, and VecDeque.

use std::collections::BTreeSet;

// now our type can be stored
// in BTreeSets and BTreeMaps!
#[derive(Ord, PartialOrd, PartialEq, Eq)]
struct Point {
    x: i32,
    y: i32,}fn example_btreeset() {
    let mut points = BTreeSet::new();
    points.insert(Point { x: 0, y: 0 }); / / ✅
}

// we can also .sort() Ord types in collections!
fn example_sort<T: Ord> (mut sortable: Vec<T>) -> Vec<T> {
    sortable.sort();
    sortable
}

Copy the code