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 ⏰

Automatic Trait

Send & Sync

Required knowledge

  • Marker Traits
  • Auto Traits
  • An Unsafe Trait.
unsafe auto trait Send {}
unsafe auto trait Sync {}
Copy the code

If a type is Send, it means that it can be safely sent between threads. If a type is Sync, this means it can safely share references between threads. To be more precise, type T is Sync if and only if &T is Send.

Almost all of them are Send and Sync. The only notable Send exception is Rc, and the Sync exception is Rc, Cell, and RefCell. If we need an Rc that satisfies Send, we can use Arc. If we need a Sync version of the Cell or RefCell, we can use Mutex or RwLock. Although we use Mutex and RwLock to wrap a primitive type, it is generally better to use the atomic types provided by the standard library, such as AtomicBool, AtomicI32, AtomicUsize, and so on.

It may come as a surprise to some that almost all types are Sync, but it’s true even for types that don’t have any internal synchronization. This is possible thanks to Rust’s strict borrowing rules.

We can pass multiple immutable references to the same piece of data into multiple threads, and since Rust statically guarantees that the underlying data will not be modified as long as immutable references exist, we can guarantee that data contention will not occur.

use crossbeam::thread;

fn main() {
    let mut greeting = String::from("Hello");
    let greeting_ref = &greeting;

    thread::scope(|scoped_thread| {
        // spawn 3 threads
        for n in 1..=3 {
            // greeting_ref copied into every thread
            scoped_thread.spawn(move| _ | {println!("{} {}", greeting_ref, n); // prints "Hello {n}"
            });
        }

        // line below could cause UB or data races but compiler rejects it
        greeting += " world"; ❌ cannot mutate greeting while immutable refs exist
    });

    // can mutate greeting after every thread has joined
    greeting += " world"; / / ✅
    println!("{}", greeting); // prints "Hello world"
}
Copy the code

Similarly, we can pass a mutable reference to data to a separate thread, and since Rust statically guarantees that there is no mutable reference alias, the underlying data will not be modified by another mutable reference, so we can also guarantee that data contention will not occur.

use crossbeam::thread;

fn main() {
    let mut greeting = String::from("Hello");
    let greeting_ref = &mut greeting;

    thread::scope(|scoped_thread| {
        // greeting_ref moved into thread
        scoped_thread.spawn(move |_| {
            *greeting_ref += " world";
            println!("{}", greeting_ref); // prints "Hello world"
        });

        // line below could cause UB or data races but compiler rejects it
        greeting += "!!!"; ❌ cannot mutate greeting while mutable refs exist
    });

    // can mutate greeting after the thread has joined
    greeting += "!!!"; / / ✅
    println!("{}", greeting); // prints "Hello world!!!"
}
Copy the code

This is why most types satisfy Sync without requiring any explicit synchronization. When we need to modify some data T simultaneously in multiple threads, the compiler will not allow us to do so unless we wrap the data in Arc

> or Arc

>, so the compiler will force explicit synchronization if necessary.

Sized

Preliminary knowledge:

  • Marker Traits
  • Auto Traits

If a type is Sized, this means that its type size is known at compile time and an instance of the type can be created on the stack.

The size and meaning of types is a subtle and huge topic that affects many aspects of a programming language. Because it’s so important, I wrote a separate article called Sizedness in Rust, which I highly recommend for anyone who wants to learn more about Sizedness. I will summarize the key points of this article below.

  1. All generic types have an implicitSizedConstraints.
fn func<T>(t: &T) {}

// example above desugared
fn func<T: Sized>(t: &T) {}
Copy the code
  1. Because all generic types have an implicitSizedConstraint. If we want to opt out of this constraint, we need to use the specific relaxed bound syntax —? Sized, the syntax is currently only forSizedTrait.
// now T can be unsized
fn func<T: ?Sized>(t: &T) {}
Copy the code
  1. All traits have an implicit one? SizedConstraints.
trait Trait {}

// example above desugared
trait Trait:?Sized {}
Copy the code

This is to enable trait objects to implement traits. Again, all the nitty-gritty is in Sizedness in Rust.