laitimes

Rust Study Notes (Fifty-Six) Advanced Trait

Use association types in traits to specify placeholder types

Associated types are type placeholders in traits, which can be used in trait's method signatures: it is possible to define trats that contain certain types without knowing what those types are before implementing them. example:

pub trait Iterator {
    type Item;//关联类型
    fn next(&self) -> Option<Self::Item>;
}
           

The difference between an association type and a generic

Generic:

  • The type must be annotated each time a traith is implemented
  • A particular (different generic parameter) can be implemented multiple times for a type, for example, Iterator for Counter's String and u32 respectively
pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
struct Counter {}
impl Iterator<String> for Counter {
    fn next(&mut self) -> Option<String> {
        todo!()
    }
}
impl Iterator<u32> for Counter {
    fn next(&mut self) -> Option<u32> {
        todo!()
    }
}
           

Association Type:

  • No dimension type is required for implementation
  • It is not possible to implement a particular task more than once for a single type

Implementation of association types:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

struct Counter {}

impl Iterator for Counter {
    //无需标注类型

    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        todo!()
    }
}
           

Default generic parameters and operator overloading

You can specify a default concrete type for a generic when using generic parameters, syntax: <placeholderType=ConcreteType> this technique is commonly used for operator overloading. Although Rust does not allow you to create its own operators and overload arbitrary operators, it can overload some of the corresponding operators by implementing those traits listed in std::ops. example:

use std::ops::Add;

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

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}
           

The above code implements Add trait to overload the + operator for the Point instance. The Add trait source code is as follows:

trait Add<RHS=Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}
           
This should seem familiar, as it is a trait with a method and an associated type. The stranger part is RHS=Self in angle brackets: this syntax is called default type parameters. RHS is a generic type parameter (short for "right hand side") that defines the rhs parameter in the add method. If you implement add trait without specifying a concrete type of RHS, the type of RHS will be the default Self type, that is, the type on which Add is implemented.
When implementing Add for Point, the default RHS is used because we want to add two Point instances together. Let's look at an example of implementing Add trait in the hope of customizing the RHS type instead of using the default type.
There are two structures that hold different element values, Millimeters and Meters. We want to be able to add millimeters to meters and have Add's implementation handle the conversion correctly. You can implement Add for Millimeters and Meters as RHS, for example:
use std::ops::Add;

#[derive(Debug)]
struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}

fn main() {
    println!("{:?}", Millimeters(1000) + Meters(1));
}
           

The main scenario for the default generic parameter type

  • Extend a type without breaking existing code
  • Allows customization in specific scenarios that are not needed by most users
The Standard Library's Add trait is an example of a second order: most of the time you'll add two similar types, but it provides the ability to customize the extra behavior. Using default type parameters in Add trait definitions means that most of the time you don't need to specify additional parameters. In other words, a small portion of the boilerplate code implemented is unnecessary, making it easier to use traits.
The first purpose is similar, but the process is reversed: if we need to add a type parameter to an existing trait, providing it with a default type will allow us to extend the functionality of the trait without breaking the existing implementation code.

Fully Qualified Syntax how to call a method of the same name

trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {//实现Pilot的fly方法
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {//实现Wizard的fly方法
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {//实现自己的fly方法
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();//调用本身的fly方法
    Pilot::fly(&person);//调用Pilot的fly方法
    Wizard::fly(&person);//调用Wizard的fly方法
}
           

Pilot and Wizard may have other types of implementations in addition to the Person implementation, so how do you tell if it is the Person implementation? Because the fly method has a self parameter, we pass the & person in and we know that it is the type implementation of person. What if it's an associative function with no arguments?

trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
           

For the above example how do we call the correlation function in the Animal trait implemented by the Dog type? That would be useful for fully qualified syntax.

<Type as Trait>::function(receiver_if_method, next_arg, ...);
           

It can be used anywhere a function or method is called, allowing the omission of parts that can be deduced from other contexts. This syntax is only needed if Rust can't distinguish which concrete implementation we expect to call.

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
           

Use supertrait to require that the sit come with other terit features

Sometimes it is necessary to use the functions of other traits in a trait:

  • Traits that need to be relied upon are also implemented
  • The indirectly dependent trate is the supertrait of the current tarot

For example, implementing an OutputPrint trait also requires the implementation of fmt::D isplay trait

use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {} *", output);
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}
           

Use the newtype pattern to implement an external dealit on an external type

We learned the orphan rule before: you can implement a trait for that type only if it is defined in a local package. But we can bypass this rule through newtype mode. That is, to create a new type using the tuple struct (tuple struct), for example:

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}
           

The above example is <String>Vec's implementation of fmt::D isplay trait, but none of them are within our crate, so define a new tuple struct, Wrapr, and wrap it around Vec<String>, so that you can indirectly <String>implement fmt::D isplay trait for Vec.

Read on