rust generics and traits

Trait

Define characteristics

If different types have the same behavior, then we can define a trait and then implement that trait for those types. Defining characteristics is a combination of methods with the purpose of defining a set of behaviors necessary to achieve certain goals.

For example, we now have two content carriers: Post and Weibo, and we want to summarize the corresponding content. That is, whether it is article content or Weibo content, it can be summarized at a certain point in time. Then summarize This behavior is shared and therefore can be defined using traits:

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

The trait keyword is used here to declare a feature, and Summary is the feature name. All methods of the trait are defined within curly braces, in this case: fn summarize( & amp;self) -> String.

Characteristics only define what the behavior looks like, not what the behavior is like. Therefore, we only define the signature of the characteristic method without implementing it, and the method signature ends with ; instead of a {}.

Implement traits for types

Because traits only define what the behavior looks like, we need to implement specific traits for the type that define exactly what the behavior looks like.

First, let’s implement the Summary feature for Post and Weibo:

pub trait Summary {
    fn summarize( & amp;self) -> String;
}
pub struct Post {
    pub title: String, // title
    pub author: String, // author
    pub content: String, // content
}

impl Summary for Post {
    fn summarize( & amp;self) -> String {
        format!("Article {}, author is {}", self.title, self.author)
    }
}

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

impl Summary for Weibo {
    fn summarize( & amp;self) -> String {
        format!("{}posted on Weibo{}", self.username, self.content)
    }
}

The syntax for implementing features is very similar to implementing methods for structures and enumerations: impl Summary for Post, pronounced as “implement the Summary feature for the Post type”, and then implement the specific method of the feature in the curly braces of impl.

Next, you can call the characteristic method on this type:

fn main() {<!-- -->
    let post = Post{<!-- -->title: "Introduction to the Rust Language".to_string(), author: "Sunface".to_string(), content: "Rust is awesome!".to_string()};
    let weibo = Weibo{<!-- -->username: "sunface".to_string(),content: "It seems that Weibo is not as useful as Tweet".to_string()};

    println!("{}",post.summarize());
    println!("{}",weibo.summarize());
}

Run output:

Article Introduction to Rust Language, written by Sunface
Sunface posted on Weibo. It seems that Weibo is not as useful as Tweet.
The location of feature definition and implementation (orphan rules)

Above we defined Summary as public in pub. That way, if someone else wants to use our Summary feature, they can import it into their package and then implement it.

There is a very important principle regarding the location of trait implementation and definition: if you want to implement trait T for type A, then at least one of A or T is defined in the current scope! For example, we can implement the Display feature in the standard library for the Post type above, because the Post type is defined in the current scope. At the same time, we can also implement the Summary feature for the String type in the current package, because Summary is defined in the current scope.

Default implementation

You can define a method in a trait with a default implementation so that other types do not need to implement the method, or you can choose to override the method:

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

The above defines a default implementation for Summary. Let’s write a piece of code to test it:

impl Summary for Post {<!-- -->}

impl Summary for Weibo {<!-- -->
    fn summarize( & amp;self) -> String {<!-- -->
        format!("{}posted on Weibo{}", self.username, self.content)
    }
}

As you can see, Post selected the default implementation, and Weibo overloaded this method. The call and output are as follows:

 println!("{}",post.summarize());
    println!("{}",weibo.summarize());

(Read more...)
Sunface posted on Weibo. It seems that Weibo is not as useful as Tweet.

The default implementation allows calling other methods in the same trait, even if these methods do not have a default implementation. In this way, features can provide a lot of useful functionality while only needing to implement a specified subset. For example, we could define the Summary trait so that it has a summarize_author method that needs to be implemented, and then define a summarize method whose default implementation calls the summarize_author method:

pub trait Summary {<!-- -->
    fn summarize_author( & amp;self) -> String;

    fn summarize( & amp;self) -> String {<!-- -->
        format!("(Read more from {}...)", self.summarize_author())
    }
}

In order to use Summary, just implement the summarize_author method:

impl Summary for Weibo {<!-- -->
    fn summarize_author( & amp;self) -> String {<!-- -->
        format!("@{}", self.username)
    }
}
println!("1 new weibo: {}", weibo.summarize());

weibo.summarize() will first call the summarize method implemented by default for the Summary feature, and then call the summarize_author method implemented by Weibo for Summary. The final output is: 1 new weibo: (Read more from @horse_ebooks…).

Use features as function parameters
pub fn notify(item: & amp;impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

impl Summary, I can only say that the person who came up with this type is really a genius at naming it. It is so appropriate. As the name suggests, it means the item parameter that implements the Summary feature.

You can use any type that implements the Summary trait as a parameter of this function, and within the function body, you can also call methods of this trait, such as the summarize method. Specifically, you can pass Post or Weibo instances as parameters, but other types such as String or i32 cannot be used as parameters of this function because they do not implement the Summary feature.

Trait bound

Although the impl Trait syntax is very easy to understand, it is actually just syntactic sugar:

pub fn notify<T: Summary>(item: & amp;T) {<!-- -->
    println!("Breaking news! {}", item.summarize());
}

The true complete written form, as mentioned above, is of the form T: Summary and is called a feature constraint.

In simple scenarios, the syntactic sugar of impl Trait is enough, but for complex scenarios, feature constraints can give us greater flexibility and syntax expression capabilities. For example, a function accepts two parameters of impl Summary:

pub fn notify(item1: & amp;impl Summary, item2: & amp;impl Summary) {<!-- -->}

If the two parameters of the function are of different types, then the above method is fine, as long as both types implement the Summary trait. But what if we want to force two parameters of a function to be of the same type? The above syntax cannot achieve this restriction. At this time, we can only use feature constraints to achieve it:

pub fn notify<T: Summary>(item1: & amp;T, item2: & amp;T) {<!-- -->}

The generic type T specifies that item1 and item2 must have the same type, and T: Summary specifies that T must implement the Summary feature.

Multiple constraints

In addition to a single constraint, we can also specify multiple constraints. For example, in addition to letting the parameter implement the Summary feature, we can also let the parameter implement the Display feature to control its formatted output:

pub fn notify(item: & amp;(impl Summary + Display)) {<!-- -->}

In addition to the above syntactic sugar forms, feature constraints can also be used:

pub fn notify<T: Summary + Display>(item: & amp;T) {<!-- -->}

With these two features, you can use the item.summarize method and println!(“{}”, item) to format the output item.

Where constraint

When feature constraints become numerous, the signature of the function becomes complex:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: & amp;T, u: & amp;U) -> i32 {<!-- -->}

Strictly speaking, the above example is still not complex enough, but we can still make some formal improvements to it, through where:

fn some_function<T, U>(t: & amp;T, u: & amp;U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{<!-- -->}
Use feature constraints to conditionally implement methods or features

Characteristic constraints allow us to implement methods under the conditions of specified types + specified characteristics, for example:

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( & amp;self) {<!-- -->
        if self.x >= self.y {<!-- -->
            println!("The largest member is x = {}", self.x);
        } else {<!-- -->
            println!("The largest member is y = {}", self.y);
        }
    }
}

cmp_display method, not all Pair structure objects can have this method. Only Pair whose T implements Display + PartialOrd at the same time can have this method. The readability of this function will be better because the generic parameters, parameters, and return values are all together and can be read quickly. At the same time, the characteristics of each generic parameter are also constrained through characteristic constraints in new lines of code.

Traits can also be implemented conditionally, for example, the standard library implements the ToString trait for any type that implements the Display trait:

impl<T: Display> ToString for T {<!-- -->
    // --snip--
}

We can call the to_string method defined by ToString on any type that implements the Display trait. For example, you can convert an integer to its corresponding String value because integers implement Display:

let s = 3.to_string();
impl Trait in function return

You can use impl Trait to illustrate that a function returns a type that implements a certain characteristic:

fn returns_summarizable() -> impl Summary {<!-- -->
    Weibo {<!-- -->
        username: String::from("sunface"),
        content: String::from(
            "m1 max is so powerful, the computer will never freeze again",
        )
    }
}

Because Weibo implements Summary, it can be used as the return value here. It should be noted that although we know that this is a Weibo type, for the caller of returns_summarizable, he only knows that an object that implements the Summary feature is returned, but does not know that a Weibo type is returned.

This return value in the form of impl Trait is very, very useful in one scenario, that is, when the actual type returned is very complex and you don’t know how to declare it (after all, Rust requires you to mark all types), in this case It can be simply returned using impl Trait. For example, closures and iterators are very complicated. Only the compiler knows the true type of those things. If you were asked to write out their specific types, you would probably have thousands of horses galloping in your heart. Fortunately, you can use impl Iterator to tell The caller returns an iterator because all iterators implement the Iterator trait.

But this return value method has a big limitation: there can only be one specific type, for example:

fn returns_summarizable(switch: bool) -> impl Summary {<!-- -->
    if switch {<!-- -->
        Post {<!-- -->
            title: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {<!-- -->
        Weibo {<!-- -->
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
        }
    }
}
Deriving features through derive

Code in the form #[derive(Debug)] has appeared many times. This is a feature derivation syntax. The object marked with derive will automatically implement the corresponding default feature code and inherit the corresponding functions.

For example, the Debug feature has a set of automatically implemented default codes. After you mark a structure, you can use println!(“{:?}”, s) to print the object of the structure.

Another example is the Copy feature, which also has a set of automatically implemented default codes. When marked on a type, the type can automatically implement the Copy feature, and then the copy method can be called to copy itself.

In short, derive derives the features provided by Rust by default, which greatly simplifies the need to manually implement the corresponding features during the development process. Of course, if you have special needs, you can also manually overload the implementation yourself.

Characteristics need to be introduced when calling methods

In some scenarios, using the as keyword for type conversion will be more restrictive, because you want to have complete control over type conversion, such as handling conversion errors, then you will need TryInto:

use std::convert::TryInto;

fn main() {
  let a: i32 = 10;
  let b: u16 = 100;

  let b_ = b.try_into()
            .unwrap();

  if a < b_ {
    println!("Ten is less than one hundred.");
  }
}

Characteristic object

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw( & amp;self) {
        //Code to draw the button
    }
}

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw( & amp;self) {
        //Code to draw SelectBox
    }
}



Learn more about features

Association type

The associated type is to declare a custom type in the statement block of the characteristic definition, so that the type can be used in the method signature of the characteristic:

pub trait Iterator {<!-- -->
    type Item;

    fn next( & amp;mut self) -> Option<Self::Item>;
}

The above is the iterator feature in the standard library, Iterator, which has an Item associated type that is used to replace the type of the traversed value.

At the same time, the next method also returns an Item type, but it is wrapped using the Option enumeration. If the value in the iterator is of type i32, then calling the next method will obtain an Option value.

:

impl Iterator for Counter {<!-- -->
    type Item = u32;

    fn next( & amp;mut self) -> Option<Self::Item> {<!-- -->
        // --snip--
    }
}

fn main() {<!-- -->
    let c = Counter{<!-- -->..}
    c.next()
}

In the above code, we have implemented the Iterator trait for the Counter type, and the variable c is an instance of the trait Iterator and the caller of the next method. Combining the previous bold content, it can be concluded that for the next method, Self is the specific type of the caller c: Counter, and Self::Item is the Item type defined in Counter: u32.

pub trait Iterator<Item> {<!-- -->
    fn next( & amp;mut self) -> Option<Item>;
}

The answer is actually very simple. For the readability of the code, when you use generics, you need to write Iterator everywhere, but when using associated types, you only need to write Iterator. When the type definition is complex, This way of writing can greatly increase readability:

pub trait CacheableItem: Clone + Default + fmt::Debug + Decodable + Encodable {<!-- -->
  type Address: AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash;
  fn is_null( & amp;self) -> bool;
}

For example, in the above code, the way of writing Address is naturally much simpler than AsRef<[u8]> + Clone + fmt::Debug + Eq + Hash, and the meaning is clear.

For another example, if you use generics, you will get the following code:

trait Container<A,B> {<!-- -->
    fn contains( & amp;self,a: A,b: B) -> bool;
}

fn difference<A,B,C>(container: & amp;C) -> i32
  where
    C : Container<A,B> {<!-- -->...}

It can be seen that due to the use of generics, a generic declaration must also be added to the function header. Using associated types will result in much more readable code:

trait Container{<!-- -->
    type A;
    type B;
    fn contains( & amp;self, a: & amp;Self::A, b: & amp;Self::B) -> bool;
}

fn difference<C: Container>(container: & amp;C) {<!-- -->}
Default generic type parameters

When using a generic type parameter, you can specify a default concrete type for it, such as the std::ops::Add trait in the standard library:

trait Add<RHS=Self> {<!-- -->
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}

It has a generic parameter RHS, but it is different from our previous usage. Here it gives RHS a default value, that is, when the user does not specify RHS, it defaults to adding two values of the same type and then returns an associated type. Output.

Maybe the above paragraph is not easy to understand, let’s use code as an 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 mainly does one thing, which is to provide the + capability for the Point structure. This is operator overloading. However, Rust does not support the creation of custom operators, and you cannot overload all operators. Currently, , only operators defined in std::ops can be overloaded.

The characteristic corresponding to + is std::ops::Add. We have seen its definition trait Add before. However, in the above example, the Add characteristic is not implemented for Point. Add feature (no default generic type parameters), which means that we use the default type of RHS, which is Self. In other words, what we define here is the addition of two identical Point types, so there is no need to specify RHS.

In contrast to the above example, in the following example, we create two different types of addition:

use std::ops::Add;

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))
    }
}

Here, the + operation of two data types Millimeters + Meters is performed, so the default RHS cannot be used at this time, otherwise it will become the form of Millimeters + Millimeters. Use Add to specify RHS as Meters, then fn add(self, rhs: RHS) naturally becomes the addition of Millimeters and Meters.

Default type parameters are mainly used for two aspects:

  1. Reduce implementation boilerplate code
  2. Extend types without significantly modifying existing code
Call the method with the same name

It is normal for different traits to have methods with the same name, and there is nothing you can do to prevent this; even in addition to methods with the same name on the trait, there are also methods with the same name on your types:

trait Pilot {<!-- -->
    fn fly(&self);
}

trait Wizard {<!-- -->
    fn fly(&self);
}

struct Human;

impl Pilot for Human {<!-- -->
    fn fly( & amp;self) {<!-- -->
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {<!-- -->
    fn fly( & amp;self) {<!-- -->
        println!("Up!");
    }
}

impl Human {<!-- -->
    fn fly( & amp;self) {<!-- -->
        println!("*waving arms furiously*");
    }
}

Here, not only the two features Pilot and Wizard have fly methods, but even the Human unit structure that implements those two features also has a method fly with the same name.

Call methods on types first

When calling fly on a Human instance, the compiler defaults to calling the methods defined in that type:

fn main() {<!-- -->
    let person = Human;
    person.fly();
}

This code will print waving arms furiously, indicating that the method defined on the type is directly called.

Calling methods on characteristics

In order to be able to call the methods of the two characteristics, you need to use the explicit calling syntax:

fn main() {<!-- -->
    let person = Human;
    Pilot::fly( & amp;person); // Call the method on the Pilot feature
    Wizard::fly( & amp;person); // Call the method on the Wizard feature
    person.fly(); // Call the method of the Human type itself
}

After running, the output is:

This is your captain speaking.
Up!
*waving arms furiously*

Because the parameter of the fly method is self, when called explicitly, the compiler can determine which method to call based on the type of call (the type of self).

At this time the question arises again, what if the method has no self parameter? Wait a moment, some readers may ask: Is there any method without self parameter?

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());
}

The calling method of Dog::baby_name() obviously does not work, because this is just the pet name of the dog mother for the baby. You may think of querying the names of dogs by other animals in the following way:

fn main() {<!-- -->
    println!("A baby dog is called a {}", Animal::baby_name());
}
error[E0283]: type annotations needed // Type annotations needed
  --> src/main.rs:20:43
   |
20 | println!("A baby dog is called a {}", Animal::baby_name());
   | ^^^^^^^^^^^^^^^^^ cannot infer type // The type cannot be inferred
   |
   = note: cannot satisfy `_: Animal`

Fully qualified syntax

Fully qualified syntax is the most explicit way to call a function:

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

In the angle brackets, through the as keyword, we provide the Rust compiler with a type annotation, that is, Animal is Dog, not other animals, so the method in impl Animal for Dog will eventually be called to get the information about the dog baby from other animals. Name: puppy.

Feature constraints in feature definition

Sometimes, we will need to enable a certain feature A to use the functionality of another feature B (another form of feature constraint). In this case, we must not only implement feature A for the type, but also implement feature B for the type. Okay, this is supertrait (I really don’t know how to translate it, can anyone give me some guidance?)

For example, there is a feature OutlinePrint, which has a method that can format the output of the current implementation type:

use std::fmt::Display;

trait OutlinePrint: Display {<!-- -->
    fn outline_print( & amp;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));
    }
}

Wait, there is a familiar syntax here: OutlinePrint: Display, which feels very similar to the feature constraints mentioned before, except that it is used in the feature definition instead of the parameters of the function. Yes, in a sense, this Very similar to feature constraints, they are used to indicate that one feature needs to implement another feature. Here is: If you want to implement the OutlinePrint feature, you first need to implement the Display feature.

Imagine, if there is no such feature constraint, can self.to_string still be called (the to_string method is automatically implemented for types that implement the Display feature)? The compiler is definitely unwilling and will report an error saying that the method to_string for the &Self type cannot be found in the current scope:

struct Point {<!-- -->
    x: i32,
    y:i32,
}

impl OutlinePrint for Point {<!-- -->}

Because Point does not implement the Display feature, you will get the following error:

error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied
  --> src/main.rs:20:6
   |
20 | impl OutlinePrint for Point {<!-- -->}
   | ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter;
try using `:?` instead if you are using a format string
   |
   = help: the trait `std::fmt::Display` is not implemented for `Point`

Since we have requirements from the compiler, we can only choose to satisfy it:

use std::fmt;

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

The above code implements the Display feature for Point, then the to_string method will also be automatically implemented: the final string obtained is obtained through the fmt method here.

Implement external characteristics (newtype) on external types

There are mentions of orphan rules. To put it simply, at least one of the characteristics or types must be local before a characteristic can be defined on this type.

Here is a way to bypass the orphan rule, which is to use the newtype pattern. In short: create a new type for a tuple structure. This tuple structure encapsulates a field, which is the specific type of the feature you want to implement.

The wrapper type is local, so we can implement external traits for this type.

newtype can not only achieve the above functions, but also does not have any performance loss at runtime, because the type will be automatically ignored at compile time.

use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {<!-- -->
    fn fmt( & amp;self, f: & amp;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);
}

Among them, struct Wrapper(Vec) is a tuple structure, which defines a new type Wrapper. The code is very simple and I believe it is easy for everyone to understand.

Since new type has so many benefits, does it have any disadvantages? The answer is yes. Notice how we access the array inside? self.0.join(“, “), yes, it is very verbose, because you need to get the array: self.0 from the Wrapper first, and then you can execute the join method.

Similarly, you cannot call any method on an array directly. You need to use self.0 to get the array first and then call it.

Of course, there are still solutions, otherwise Rust is an extremely powerful and flexible programming language! Rust provides a feature called Deref. After implementing this feature, a layer of similar type conversion operations can be automatically performed, and Wrapper can be turned into Vec for use. This will allow you to use the Wrapper as if you were using an array directly, without adding self.0 to each operation.

At the same time, if we don’t want Wrapper to expose all methods of the underlying array, we can also overload these methods for Wrapper to achieve hiding purposes.