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 ⏰

Generic traits

Default

Required knowledge

  • Self
  • Functions
  • Derive Macros
trait Default {
    fn default() - >Self;
}
Copy the code

You can construct defaults for types that implement Default.

struct Color {
    r: u8,
    g: u8,
    b: u8,}impl Default for Color {
    // default color is black
    fn default() - >Self {
        Color {
            r: 0,
            g: 0,
            b: 0,}}}Copy the code

This is useful when prototyping quickly, especially if we don’t require too much and only need an instance of a type:

fn main() {
    // just give me some color!
    let color = Color::default();
}
Copy the code

When we want to explicitly expose the function to the user, we can also choose to do this:

struct Canvas;
enum Shape {
    Circle,
    Rectangle,
}

impl Canvas {
    // let user optionally pass a color
    fn paint(&mut self, shape: Shape, color: Option<Color>) {
        // if no color is passed use the default color
        let color = color.unwrap_or_default();
        // etc}}Copy the code

Default is also useful in the generic context when we need to construct generic types:

fn guarantee_length<T: Default> (mut vec: Vec<T>, min_len: usize) - >Vec<T> {
    for _ in 0..min_len.saturating_sub(vec.len()) {
        vec.push(T::default());
    }
    vec
}
Copy the code

We can also initialize structural parts using the Default type in conjunction with Rust’s struct update syntax. Now we have a Color constructor new that takes all the members of the structure as arguments:

impl Color {
    fn new(r: u8, g: u8, b: u8) - >Self {
        Color {
            r,
            g,
            b,
        }
    }
}
Copy the code

However, we can have more convenient constructors that each take only a portion of the structure’s members and use the default values for the rest of the structure’s members:

impl Color {
    fn red(r: u8) - >Self{ Color { r, .. Color::default() } }fn green(g: u8) - >Self{ Color { g, .. Color::default() } }fn blue(b: u8) - >Self{ Color { b, .. Color::default() } } }Copy the code

There is also a Default derived macro that we can use to write Color like this:

// default color is still black
// because u8::default() == 0
#[derive(Default)]
struct Color {
    r: u8,
    g: u8,
    b: u8
}
Copy the code

Clone

Required knowledge

  • Self
  • Methods (Methods)
  • Default Impls
  • Derive Macros
trait Clone {
    fn clone(&self) - >Self;

    // provided default impls
    fn clone_from(&mut self, source: &Self);
}
Copy the code

We can convert immutable references of Clone type to the possessed value, namely &t ->T. Clone does not guarantee the efficiency of this conversion, so it can be slow and costly. We can quickly implement Clone on a type using derived macros:

#[derive(Clone)]
struct SomeType {
    cloneable_member1: CloneableType1,
    cloneable_member2: CloneableType2,
    // etc
}

// macro generates impl below
impl Clone for SomeType {
    fn clone(&self) - >Self {
        SomeType {
            cloneable_member1: self.cloneable_member1.clone(),
            cloneable_member2: self.cloneable_member2.clone(),
            // etc}}}Copy the code

Clone can be used to construct a type instance in a generic context. Here is an example taken from the previous section, where Default is replaced with Clone:

fn guarantee_length<T: Clone> (mut vec: Vec<T>, min_len: usize, fill_with: &T) -> Vec<T> {
    for _ in 0..min_len.saturating_sub(vec.len()) {
        vec.push(fill_with.clone());
    }
    vec
}
Copy the code

Clones are often used as an escape hatch to avoid borrowing checkers. Managing structures with references can be challenging, but we can clone references into owning values.

// oof, we gotta worry about lifetimes 😟
struct SomeStruct<'a> {
    data: &'a Vec<u8>,}// now we're on easy street 😎
struct SomeStruct {
    data: Vec<u8>,}Copy the code

If the program we’re writing isn’t performance-sensitive, then we don’t need to worry about cloning data. Rust is a language that exposes a lot of the underlying details, so it’s easy to get caught up in premature optimizations instead of really addressing the problem at hand. For many programs, the best order of priority is usually to build correctness first, elegance second, and performance third, only after performance has been dissected and performance bottlenecks identified. In general, this is good advice to follow, but you need to be aware that it may not apply to your program.

Copy

Required knowledge

  • Marker Traits
  • Subtraits & SuperTraits
  • Derive Macros
trait Copy:Clone{}
Copy the code

We Copy the Copy type, for example: T-> t. Copy promises that the Copy operation is simple bitwise Copy, so it is fast and efficient. We can’t implement Copy ourselves, only the compiler can provide it, but we can get the compiler to do so by using the Copy derived macro, just as we did with the Clone derived macro, because Copy is a Clone subtrait:

#[derive(Copy, Clone)]
struct SomeType;
Copy the code

Copy refines Clone. A Clone operation can be slow and expensive, but a copy operation is guaranteed to be fast and inexpensive, so a copy is a faster clone operation. If a type implements Copy, the Clone implementation is irrelevant:

// this is what the derive macro generates
impl<T: Copy> Clone for T {
    // the clone method becomes just a copy
    fn clone(&self) - >Self{*self}}Copy the code

When a type implements Copy, its behavior changes when it is moved. By default, all types have move semantics, but once a type implements Copy, it has Copy semantics. To explain the difference, let’s look at these simple scenarios:

// a "move", src: ! Copy
let dest = src;

// a "copy", src: Copy
let dest = src;
Copy the code

In both cases, dest = SRC copies the contents of SRC bitwise and moves the result to dest. The only difference is that, in the first case (“a move”), the borrowing inspector invalidates the SRC variable and ensures that it is not used anywhere else later; In the second case (“a copy”), SRC is still valid and available.

In short: Copy is move, and move is copy. The only difference between them is the way they treat the borrowed inspector.

For a more concrete example of a move, assume that SEC is a Vec

type and its contents look like this:

{ data: *mut [i32], length: usize, capacity: usize }
Copy the code

When we execute dest = SRC, we get:

src = { data: *mut [i32], length: usize, capacity: usize }
dest = { data: *mut [i32], length: usize, capacity: usize }
Copy the code

In this unknown case, SRC and dest each have a variable reference alias for the same data, which is a big no-no, so the borrow checker invalidates the SRC variable without the compiler reporting an error. So that it can no longer be used.

For a more specific copy example, suppose SRC is an Option

and its contents look like this:

{ is_valid: bool, data: i32 }
Copy the code

Now, when we execute dest = SRC, we get:

src = { is_valid: bool, data: i32 }
dest = { is_valid: bool, data: i32 }
Copy the code

Both of them are available at the same time! Therefore, Option

is Copy.

Although Copy is an automatic trait, the designers of the Rust language decided to let types explicitly choose Copy semantics rather than silently inherit Copy semantics when types meet their conditions, which can lead to messy behavior that often leads to bugs.

Any

Required knowledge

  • Self
  • Generic Blanket Impls
  • Subtraits & Supertraits
  • Trait Objects
trait Any: 'static {
    fn type_id(&self) -> TypeId;
}
Copy the code

Rust’s polymorphic style is parameterized, but if we are trying to use a more ad-hoc polymorphic style similar to a dynamically typed language, we can simulate it by using the Any trait. We don’t have to implement the Any trait for our type manually because this is covered by the Generic Blanket ImpL:

impl<T: 'static+?Sized> Any for T {
    fn type_id(&self) -> TypeId {
        TypeId::of::<T>()
    }
}

Copy the code

We extract a T from a dyn Any using the downcast_ref::

() and downcast_mut::

() methods:

use std::any::Any;

#[derive(Default)]
struct Point {
    x: i32,
    y: i32,}impl Point {
    fn inc(&mut self) {
        self.x += 1;
        self.y += 1; }}fn map_any(mut any: Box<dyn Any>) -> Box<dyn Any> {
    if let Some(num) = any.downcast_mut::<i32>() {
        *num += 1;
    } else if let Some(string) = any.downcast_mut::<String>() {
        *string += "!";
    } else if let Some(point) = any.downcast_mut::<Point>() {
        point.inc();
    }
    any
}

fn main() {
    let mut vec: Vec<Box<dyn Any>> = vec![
        Box::new(0),
        Box::new(String::from("a")),
        Box::new(Point::default()),
    ];
    // vec = [0, "a", Point { x: 0, y: 0 }]
    vec = vec.into_iter().map(map_any).collect();
    // vec = [1, "a!", Point { x: 1, y: 1 }]
}
Copy the code

This trait is rarely needed because, in most cases, parameterized polymorphism is preferable to temporary polymorphism, which can also be modeled with enumerations (enums), which have better type safety and require less indirection (abstraction). For example, we could implement the above example as follows:

#[derive(Default)]
struct Point {
    x: i32,
    y: i32,}impl Point {
    fn inc(&mut self) {
        self.x += 1;
        self.y += 1; }}enum Stuff {
    Integer(i32),
    String(String),
    Point(Point),
}

fn map_stuff(mut stuff: Stuff) -> Stuff {
    match &mut stuff {
        Stuff::Integer(num) => *num += 1,
        Stuff::String(string) => *string += "!",
        Stuff::Point(point) => point.inc(),
    }
    stuff
}

fn main() {
    let mut vec = vec![
        Stuff::Integer(0),
        Stuff::String(String::from("a")),
        Stuff::Point(Point::default()),
    ];
    // vec = [0, "a", Point { x: 0, y: 0 }]
    vec = vec.into_iter().map(map_stuff).collect();
    // vec = [1, "a!", Point { x: 1, y: 1 }]
}

Copy the code

Although Any is rarely needed, it can be handy in some cases, as we’ll see in the Error Handling section below.