Generics and Traits

One function for many types, trait bounds, trait objects, the standard traits you read every day, and macros enough to follow them.

Generics and traits

Option and Result were already generic: one shape, a type filled in later. This lesson explains that mechanism, then traits, which are the bounds those type parameters need. Trait objects come after static dispatch. The standard traits come after you can read an impl. Macros are last, because most of what looks like a macro is better as a function.

Generics

A generic parameter stands for a type the caller chooses. fn identity<T>(x: T) -> T and struct Wrapper<T> { value: T } are the two forms. The typestate post used a parameter as a stage label. Here the parameter is the data.

max<T: Ord>what you writemax for i32emittedmax for Stringemitted
StatusYou write one function

The bound Ord is required because the body compares. Without a bound, the compiler will not guess that T has >.

impl<T> Wrapper<T> is the method for every T. impl Wrapper<u32> adds methods only for that one type. A turbofish, Wrapper::<u32>::new(1), names the type when it cannot be inferred. impl Trait in a parameter is static dispatch, the next section. dyn Trait is the section after that. Do not mix them up: static means the concrete type is known at compile time.

01_generics.rsRust
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> {
    let mut best = items.first()?;
    for item in items {
        if item > best {
            best = item;
        }
    }
    Some(best)
}

struct Wrapper<T> {
    value: T,
}

impl<T> Wrapper<T> {
    fn new(value: T) -> Self {
        Self { value }
    }
}

fn main() {
    let nums = [3, 9, 4];
    println!("{}", largest(&nums).copied().unwrap_or(0));
    let wrapped = Wrapper::new(String::from("ok"));
    println!("{}", wrapped.value);
}

Traits

A trait is the set of methods a type must provide. A default method can call the required ones. impl Summarize for Article fills them in.

Summarizethe contractArticleimplNoteimpl
StatusThe trait names the behavior

fn notify(item: &impl Summarize) accepts any type that implemented it. The longer form fn notify&lt;T: Summarize&gt;(item: &T) is the same static dispatch.

You may implement a trait for a type only when you own the trait or you own the type. A foreign trait on a foreign type needs a newtype. A supertrait, trait Logged: Display, means the implementor is also Display. An associated type, Iterator’s Item, is the one output type for that impl. Use it when there is a single natural choice, instead of a type parameter on every mention of the trait.

02_traits_basics.rsRust
trait Summarize {
    fn summary(&self) -> String;
    fn preview(&self) -> String {
        format!("{}...", self.summary())
    }
}

struct Article {
    title: String,
}

impl Summarize for Article {
    fn summary(&self) -> String {
        self.title.clone()
    }
}

fn notify(item: &impl Summarize) {
    println!("{}", item.preview());
}

fn main() {
    notify(&Article { title: String::from("Rust") });
}

Trait objects

Static dispatch picks the function at compile time. A trait object picks it at runtime, when the concrete type is not known yet: a list of different widgets, a plugin, an error erased to one type. The value is a fat pointer: a pointer to the data and a pointer to the method table.

&dyn Drawdata and vtableButtondatadraw()method table
StatusThe caller sees only the trait

paint(widget: &dyn Draw) can take a Button or a TextField. The function is not copied per type.

Box of dyn Trait, a reference to dyn Trait, and Arc of dyn Trait plus Send plus Sync are the three spellings. The Arc form is the one services use when the object must cross threads. The object has to be behind a pointer. A bare dyn Trait has no size the compiler can put on the stack.

Object safety. A trait can be a trait object only when every method can go in the table. A method that returns Self cannot: the table would not know the size of that Self. Clone is not object-safe for that reason. Iterator is object-safe. A method with its own generic type parameter is not object-safe either.

Reach for dyn when one collection must hold different types that share a trait: plugins, a middleware stack, mixed widgets, test doubles. When there is only one concrete type, static dispatch is the direct call and it is faster. A closed set of variants is still an enum. You get exhaustiveness, and you do not pay for a method table.

03_trait_objects.rsRust
trait Draw {
    fn draw(&self);
}

struct Button;
struct TextField;

impl Draw for Button {
    fn draw(&self) {
        println!("button");
    }
}

impl Draw for TextField {
    fn draw(&self) {
        println!("field");
    }
}

fn paint(widget: &dyn Draw) {
    widget.draw();
}

fn main() {
    paint(&Button);
    paint(&TextField);
}

Standard traits

These names are the vocabulary of signatures.

TraitWhat it is for
Debug{:?} and {:#?}. Derive it.
Display{}. Hand-write it for text a user will read. Do not derive it and hope the debug dump is friendly.
CloneThe explicit duplicate. You already met it with ownership.
CopyThe implicit bit copy, and only for trivial types.
DefaultDefault::default() builds a plain value. In a struct update, ..Default::default() fills every field you did not name. derive(Default) writes it when every field has a default.
PartialEq, EqEquality, ==. A HashMap key needs Eq and Hash.
PartialOrd, OrdOrdering. Floats only get the partial forms, because NaN is not equal to itself. Do not use a float as a map key.
From, IntoWrite From, not Into. impl From of A for B gives Into for free.
TryFrom, TryIntoThe same pair when the conversion can fail. They return Result.
AsRefA cheap reference conversion. impl AsRef of Path accepts a Path, a PathBuf, a string slice, and a String.
BorrowThe same idea for HashMap: a map keyed by String can be queried with a string slice.
ToOwnedThe other direction, slice to owned.
DerefHow a smart pointer exposes the inner type’s methods. Do not use it to pretend one type inherits from another.
DropCleanup when the owner ends.
Iterator, IntoIterator, FromIteratorFor loops and collect.
SendThe value can move to another thread.
SyncA shared reference is Send, so many threads may hold it. Those two auto traits wait for the thread lesson.
ErrorThe failure trait from the error lesson.
Fn, FnMut, FnOnceThe closure traits. Closures are the next lesson after lifetimes.

Conversion habit. At a boundary, implement From or TryFrom. In an API that should be flexible, take AsRef or impl Into. Leave as for a width cast you have already decided is safe. as can silently truncate, so it is the wrong tool for turning domain values into each other.

04_common_std_traits.rsRust
#[derive(Debug, Clone)]
struct Config {
    port: u16,
}

impl Default for Config {
    fn default() -> Self {
        Self { port: 8080 }
    }
}

struct Port(u16);

impl TryFrom<&str> for Port {
    type Error = String;
    fn try_from(raw: &str) -> Result<Self, Self::Error> {
        let n: u16 = raw.parse().map_err(|_| String::from("port"))?;
        if n == 0 {
            return Err(String::from("zero"));
        }
        Ok(Self(n))
    }
}

fn main() {
    let port = Port::try_from("8080").unwrap();
    let cfg = Config { port: port.0, ..Config::default() } ;
    println!("{:?}", cfg);
}

Macros, enough to read

You already call macros: println!, vec!, format!, assert_eq!. Writing one is optional early. Reading one is not. macro_rules! matches a pattern and expands it before type checking, so rustc then compiles ordinary Rust. The fragments you will see are expr, ident, ty, block, and a repetition written as a list of expr.

Hygiene means a name the macro introduces does not collide with a local at the call site. Procedural macros are Rust programs that emit Rust. derive(Serialize) is a derive macro. tokio::main is an attribute macro. sqlx::query! is a function-like procedural macro. App code uses those constantly. Writing them is rare.

Reach for a function first. A macro is for repetition that a function or a generic cannot express cleanly. Macros hide the stack trace and confuse the editor. If a generic can say it, do not hide it behind macro_rules!.

05_macros_enough_to_read.rsRust
macro_rules! my_vec {
    ( $( $x:expr ),* $(,)? ) => {
        {
            let mut v = Vec::new();
            $( v.push($x); )*
            v
        }
    };
}

fn main() {
    let nums = my_vec![1, 2, 3];
    println!("{}", nums.len());
}