trait
Traits are similar to, but not identical to, interfaces in TS. For example, attributes and methods can be defined in interfaces, whereas methods can only be defined in traits.
Define the trait
Use the trait keyword followed by the trait name, with curly braces to identify the method that should be implemented for the type that has the trait:
trait Summary {
// A trait can contain multiple methods: each method signature occupies a single line and ends with a semicolon.
fn summarize(&self) - >String;
}
Copy the code
Implement traits for types
In rust, impl is followed by the trait name, followed by the for keyword and the type name. In rust, the Summary trait is implemented for NewsArticle and Tweet.
struct NewsArticles {
headlline: String,
content: String,
author: String,
location: String
}
// Implement Summary for NewsArticles
impl Summary for NewsArticles {
fn summarize(&self) - >String {
// self represents the specific type of the method being called, much like this in TS
format!("{} - {} - {} - {}".self.headlline, self.author, self.location, self.content)
}
}
let news = NewsArticles {
headlline: String::from("headline"),
content: String::from("content"),
author: String::from("author"),
location: String::from("location")};// Call summarize()
println!("{}", news.summarize()); // headline-author-location-content
struct Tweet {
username: String,
content: String,
reply: String,
retweet: String,}// Implement Summary for tweets
impl Summary for Tweet {
fn summarize(&self) - >String {
format!("{} - {} - {} - {}".self.username, self.content, self.reply, self.retweet)
}
}
let tweet = Tweet {
username: String::from("username"),
content: String::from("content"),
reply: String::from("reply"),
retweet: String::from("retweet"),};// Call summarize()
println!("{}", tweet.summarize()) // username-content-reply-retweet
Copy the code
Implement custom traits for external constructs:
// Implement custom traits for the standard library
impl Summary for String {
fn summarize(&self) - >String {
"impl Summary for String".to_string()
}
}
// Call the summarize() method implemented for the string
println!("{}".String::from("abc").summarize()) // impl Summary for String
Copy the code
Traits that implement the external library for the structure we defined:
use std::fmt;
// Implement the Display trait in the standard library to print types on the console
// Instead of focusing on the details of the code, we need to know that traits in the standard library can implement our custom types
impl fmt::Display for Tweet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {}, {}, {})".self.username, self.content, self.reply, self.retweet)
}
}
// Display can print tweets without adding {:? } Enable Debug mode
println!("{}", tweet); // (username, content, reply, retweet)
Copy the code
Implement external traits for external constructs:
// This implements the Display trait in the library for the Vec types in the library
impl<T> std::fmt::Display for std::vec::Vec<T> {
// Error only when traits or one of the types are defined in our library,
// To implement the corresponding trait for the type.
}
Copy the code
The above error occurs because rust has an orphan rule: it is so named because its parent type is not defined in the current library. This rule is also part of program coherence, which ensures that what someone else has written doesn’t break your code and vice versa. Without this rule, two libraries could separately implement the same trait for the same type, and Rust would not be able to determine which version to use.
The default implementation
The trait method signature can be implemented by default, reducing the need for duplicate code writing for multiple types of methods:
trait User {
fn user(&self) - >String {
String::from("unknow")}}// Implement the User trait for NewsArticles
impl User for NewsArticles {
// Since user has a default implementation, there is no need to implement it here
}
println!("{}", news.user()) // unknow
Copy the code
You can also call other methods in the same trait in the default implementation, even if they don’t have a default implementation:
trait User {
fn user(&self) - > &String;
fn info(&self) - > &String {
// User () does not have a default implementation, but can be called because the structure implements the User trait
// The user() method must be implemented
self.user()
}
}
impl User for NewsArticles {
// The user() method is implemented here
fn user(&self) - > &String{&self.author
}
}
println!("{}", news.info()) // author
Copy the code
Use traits to constrain parameters
Using the IMPL keyword, a function requires that the parameter implement a trait:
fn notify(item: impl Summary) { // Request item implements Summary
println!("{}", item.summarize())
}
notify(news) // headline-author-location-content
Copy the code
The above is just a syntactic sugar. The full “trait constraint” is implemented using generics:
// The generic parameter is followed by the trait to be implemented
fn notify<T: Summary>(item: T) {
println!("{}", item.summarize())
}
Copy the code
The IMPl Trait syntax sugar is more suitable for short examples, while the Trait constraint is more suitable for complex situations, such as multiple parameters, where the Trait constraint is more concise:
// impl Trait
fn notify(item: impl Summary, item2: impl Summary) {}
/ / trait constraints
fn notify<T: Summary>(item: T, item2: T) {}
Copy the code
Allow multiple traits to limit parameters:
// Using the plus sign to require item requires both Summary and Display traits to be implemented
fn notify<T: Summary + std::fmt::Display>(item: T) {
println!("{}", item.summarize())
}
Copy the code
Simplify trait restrictions using the WHERE syntax:
fn some_function<T: Summary + std::fmt::Display, U: Debug + Clone>(param1: T, param2: U) {}
// Equivalent to:
fn some_function<T, U>(param1: T, param2: U) where
T: Summary + std::fmt::Display,
U: Summary + Clone {
// ...
}
Copy the code
Restrict return value types
You can also use traits to require that the return value implements a trait:
// Require the return value to implement the Summary trait
fn notify(item: String) - >impl Summary {
Tweet {
username: item,
content: String::from("content"),
reply: String::from("reply"),
retweet: String::from("retweet"),
}
}
notify("str".to_string());
Copy the code
In particular, Tweet and NewArticles both implement Summary but fail to compile:
fn notify(switch: bool) - >impl Summary {
if switch {
NewsArticles {
headlline: String::from("headline"),
content: String::from("content"),
author: String::from("author"),
location: String::from("location"),}}else {
// Error: If and else types are incompatible due to the way traits work
// How to use trait objects to store different types of values
Tweet {
username: String::from("username"),
content: String::from("content"),
reply: String::from("reply"),
retweet: String::from("retweet"),
}
}
}
notify(true);
Copy the code
Use traits to solve the largest function problem in generic sections
Recall the largest function in the previous section, which ignored the error. Instead, let’s use a trait to solve the problem.
// Find the maximum value in the array
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest { // This is an error because T can be of any type and not all types can be compared
largest = item
}
}
largest
}
println!("{}", largest(&['a'.'b'.'c'.'d']))
println!("{}", largest(&[1.2.3.3]))
Copy the code
Use trait constraints to solve:
// The T type is required to implement PartialOrd and Copy traits
// PartialOrd is used for comparison.
// Copy is used to Copy values, not transfer ownership
fn largest<T: std::cmp::PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0]; // Copy is required because, for example, when arguments are all strings, member ownership cannot be removed
for &item in list.iter() {
if item > largest { STD :: CMP ::PartialOrd is required because not all data is comparable
largest = item
}
}
largest
}
println!("{}", largest(&['a'.'b'.'c'.'d'])); // d
println!("{}", largest(&[1.2.3.3])) / / 3
Copy the code
Use traits to implement methods conditionally
When implementing a method for a structure, it can be implemented conditionally. For example, a method is implemented for a structure only when the structure meets the trait restriction:
#[derive(Debug)]
struct Pair<T> {
x: T,
y: T,
}
// Implement a method with the generic type T
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {
x,y
}
}
}
let pair = Pair::new(100.50);
println!("{:? }", pair); // Pair { x: 100, y: 50 }
// Implement a method that requires structure member types
// The T type is required to implement the Display and PartialOrd traits
// Only Pair
that implements both traits,
// the cmp_display method is implemented
impl<T: std::fmt::Display + std::cmp::PartialOrd> Pair<T> {
fn cmp_display(&self) - >bool {
if self.x > self.y {
println!("larger:{}".self.x)
} else {
println!("smaller:{}".self.y)
}
}
}
pair.cmp_display(); // larger:100
Copy the code
Overlay implementation (Blanketimplementation)
This feature really blows up for all type implementations that satisfy trait constraints:
// Define a Log trait
trait Log {
fn log(&self);
}
// Note that for T directly implements the log method for all types that implement Display
impl<T: std::fmt::Display> Log for T {
fn log(&self) {
println!("{}".self)}}2.log(); / / 2
news.log() // error: news does not implement Display
Copy the code
You can implement all types without trait restrictions:
trait LogAlwaysOne {
fn log_one(&self) {
println!("{}".1)}}// Implement the LogAlwaysOne trait for all types
impl<T> LogAlwaysOne for T {}
// For example, the log_one() method can be called for any of the following types
1.log_one(); / / 1
'1'.log_one(); / / 1
"1".log_one(); / / 1
news.log_one(); / / 1
Copy the code