Rust

[Rust] Trait

llHoYall 2022. 8. 8. 21:08

A trait defines functionality a particular type has and can share with other types.

We can use traits to define shared behavior in an abstract way.

We can use trait bounds to specify that a generic type can be any type that has certain behavior.

Define Trait

Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose.

Traits can be declared using the trait keyword and the trait's name.

pub trait Summary {
    fn summarize(&self) -> String;
}

We've also declared the trait as pub so that crates depending on this crate can make use of this trait too.

Inside the curly brackets, we declare the method signatures that describe the behaviors of the types that implement this trait.

After the method signature, instead of providing an implementation within curly brackets, we use a semicolon.

A trait can have multiple methods in its body.

Implement Trait

Implementing a trait on a type is similar to implementing regular methods.

pub struct Tweet {
    pub username: String,
    pub content: String,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn main() {
    let tweet = Tweet {
        username: String::from("HoYa"),
        content: String::from("Melong!"),
    };

    println!("{}", tweet.summarize());
}

The difference is that after impl, we put the trait name we want to implement, then use the for keyword, and then specify the name of the type we want to implement the trait for.

Default Implementation

Sometimes it's useful to have default behavior for some or all of the methods in a trait instead of requiring implementations for all methods of every type.

Then, as we implement the trait on a particular type, we can keep or override each method's default behavior.

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(more...)")
    }
}

pub struct News {
    pub title: String,
    pub content: String,
}

impl Summary for News { }

fn main() {
    let news = News {
        title: String::from("Headline"),
        content: String::from("Blah blah"),
    };

    println!("{}", news.summarize());
}

Default implementations can call other methods in the same trait, even if those other methods don't have a default implementation.

Traits as Parameters

To define a function with traits as parameters, we use the impl Trait syntax.

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

fn main() {
    let tweet = Tweet {
        username: String::from("HoYa"),
        content: String::from("Melong!"),
    };

    notify(&tweet);
}

The item parameter accepts any type that implements the specified trait.

Trait Bound Syntax

The impl Trait syntax works for straightforward cases but is actually syntax sugar for a longer form known as a trait bound.

pub fn notify<T: Summary>(item: &T) { ... }

We can easily extend using this syntax.

pub fn notify<T: Summary>(item1: &T, item2: &T) { ... }

Multiple Trait Bounds

We can also specify more than one trait bound using + syntax.

pub fn notify(item: &(impl Summary + Display)) { ... }
pub fn notify<T: Summary + Display>(item: &T) { ... }

Trait Bounds with where Clauses

Lots of trait bounds make the function signature hard to read.

where clauses make it easy.

// Before
fn func<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { ... }

// After
fn func<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{ ... }

Traits as Return Type

We can use impl Trait syntax in the return position to return a value of some type that implements a trait.

fn summarizable() -> impl Summary {
    Tweet {
        username: String::from("HoYa"),
        content: String::from("Melong"),
    }
}

Trait Bounds to Conditionally Implement Methods

By using a trait bound with an impl block that uses generic type parameters, we can implement methods conditionally for types that implement the specified traits.

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

fn main() {
    let pair = Pair::new(3, 4);
    pair.cmp_display();
}