characteristic
In Rust, attributes consist of method signatures. If a type implements all the methods defined by an attribute, the type is said to have this attribute. When defining a function or method, you can set the type of the parameter as a feature. When passing parameters, you can pass all variables of the type with this feature. In this regard, it is similar to the interface in Go.
Define properties
trait Summary { fn summarize(&self) -> String; }
For example, the above code defines the Summary feature. In {} is the method signature, and each method signature is marked with; ending. The Summary attribute contains only one summarize method. The parameter of this method is & self and the return value is of String type.
Implementation features
Considering the following scenarios, we have two structure types, Article and Book, which are defined as follows:
struct Article { author: String, content: String, } struct Book { author: String, chapters: Vec<String>, }
Among them, Article and Book contain the member author, the content member of Article stores the content of the Article, and the chapters of Book stores the content of each chapter. We expect both types to implement the summarize method to summarize the content, as follows:
impl Summary for Article { fn summarize(&self) -> String { String::from("This is article.") } } impl Summary for Book { fn summarize(&self) -> String { String::from("This is book.") } }
View the running results as follows:
fn main() { let article = Article { author: String::from("someone"), content: String::from("content"), }; let book = Book { author: String::from("anyone"), chapters: vec![], }; println!("{}", article.summarize()); println!("{}", book.summarize()); } // output // This is article. // This is book.
Default implementation
When defining features, you can provide a default implementation for one or more methods, as shown below:
trait Summary { fn summarize(&self) -> String; fn default_method(&self) -> String { String::from("This is a default method.") } } struct Note { author: String, date: String, content: String, } impl Summary for Note { fn summarize(&self) -> String { String::from("This is note.") } } fn main() { let note = Note { author: String::from("someone"), date: String::from("2021-01-01"), content: String::from(""), }; println!("{}", note.default_method()); println!("{}", note.summarize()); } // output // This is a default method. // This is note.
Above, we set the default for the Summary feature_ Method provides a default method, so you can still use it even if the Note type does not implement this method. It is worth noting that we cannot use Note's member variables in the default method, such as default_ self.author is used in method because it is not guaranteed that all types that implement the Summary attribute have author as a member variable.
Use properties as parameter types
The following function can accept variables of any type that implements the Summary attribute as parameters:
fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize()); }
Note that the & impl Summary used here indicates that a reference needs to be passed when passing parameters. If it is an impl Summary, ownership will be transferred.
Attribute binding syntax
Consider the following function signature:
fn notify(item1: &impl Summary, item2: &impl Summary, item3: &impl Summary, item4: &impl Summary) { // do something }
It looks redundant, and can be written as follows:
fn notify<T: Summary>(item1: &T, item2: &T, item3: &T, item4: &T) { // do something }
This method is only applicable to Summary, which is a feature. If the Summary is of other types, an error will be reported, as follows:
fn sum<T: u32>(x: T, y: T, z: T) -> u32 { x + y + z } fn main() { println!("{}", sum(1, 2, 3)); } // Error message // | // 1 | fn sum<T: u32>(x: T, y: T, z: T) -> u32 { // | ^^^ not a trait
Types satisfying multiple properties
If we want the parameter type to satisfy multiple characteristics, we can use the + operator, as shown below:
fn notify(item: &(impl Summary + Display)) { // do something }
Or use the attribute binding syntax:
fn notify<T: Summary + Display>(item: &T) { // do something }
Keyword where
Consider the following function signatures:
fn some_func<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 { // do something }
You can use where to override:
fn some_func<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug, { // dome something }
The latter is more readable than the former.
Implement methods only for types that meet certain conditions
First, define the generic structure pair < T > and the new method, as follows:
use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Pair<T> { Pair { x, y } } }
We expect to implement the display method for type T that meets the display feature, as follows:
impl<T: Display> Pair<T> { fn display(&self) { println!("{{ x: {}, y: {} }}", self.x, self.y); } }
That is, when t does not satisfy the display feature, its corresponding type pair < T > has no display method.
In addition, if we want to implement the ToString attribute for the type T satisfying the Display attribute, the code is as follows:
impl<T: Display> ToString for Pair<T> { fn to_string(&self) -> String { format!("Pair {{ x: {}, y: {} }}", self.x, self.y) } }
Ignoring other factors, if we want to implement the ToString feature for all pairs < T >, the code is as follows:
impl<T> ToString for Pair<T> { // ... }
Comparing the two, we can find that the constraint on T is carried out in impl < >, and the form is the same as the feature binding syntax.