24 days from Node.js to Rust
preface
Rust designed Traits for code reuse. This is very similar to JavaScript mixins, which are a pattern of adding methods to objects, usually using object.assign (), for example:
const utilityMixin = {
prettyPrint() {
console.log(JSON.stringify(this.null.2)); }};class Person {
constructor(first, last) {
this.firstName = first;
this.lastName = last; }}function mixin(base, mixer) {
Object.assign(base.prototype, mixer);
}
mixin(Person, utilityMixin);
const author = new Person("Jarrod"."Overson");
author.prettyPrint();
Copy the code
You can also use mixins in TypeScript, but it’s a little more complicated
Rust Traits are very similar to JavaScript mixins in that they are collections of methods, and there are plenty of documents out there that compare structs and Traits to object inheritance. Ignore those, they just complicate things
You just have to remember,Traits
It’s just a bunch of methods
The body of the
Perfect the TrafficLight
In the previous article, we added the get_state() method to the TrafficLight structure, and now it’s time to add functionality to each type of light. The first light we’ll add is the home light. Do not need to consider too many functions, as long as can turn on and off
Not surprisingly, we can do the following:
#[derive(Debug)]
struct HouseLight {
on: bool,}impl Display for HouseLight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Houselight is {}".if self.on { "on" } else { "off"}}})impl HouseLight {
pub fn new() - >Self {
Self { on: false}}pub fn get_state(&self) - >bool {
self.on
}
}
Copy the code
Next, we implement a generic print_state() function that we want to print the states of all lights:
fn print_state(light: ???) {}Copy the code
How do we do that? We can’t list all types like we do in TypeScript:
function print(light: TrafficLight | HouseLight) {... }Copy the code
In this example, we don’t care what type of data is being passed in, we just want its name and state, and Traits come in handy. Okay
Traits
Traits are defined starting with a trait keyword and have a similar structure to impL, containing several methods, except that methods in Traits can have no function body
Note: The function body is optional; if it is written, it corresponds to the default implementation, and others can choose to override the default implementation
Now implement a Traits called Light and add a get_name() method:
trait Light {
fn get_name(&self) - > &str;
}
Copy the code
When we implement Traits, we use the impl keyword, just as we did with struct. Now we write it in the format impl [trait] for [struct] :
impl Light for HouseLight {
fn get_name(&self) - > &str {
"House light"}}impl Light for TrafficLight {
fn get_name(&self) - > &str {
"Traffic light"}}Copy the code
Now we can implement print_state() as an impl [trait] :
fn print_state(light: &impl Light) {
println!("{}", light.get_name());
}
Copy the code
If you wanted to add the get_state() method to a trait, we ran into a problem because each light has a different type of state, so we printed them in debug format. When you hear this, your first reaction might be something like this:
trait Light {
fn get_name(&self) - > &str;
fn get_state(&self) - >impl std::fmt::Debug;
}
Copy the code
Rust will declare impl traits not allowed outside of function and method return types:
error[E0562]: `impl Trait` not allowed outside of function and method return types
--> crates/day-10/traits/src/main.rs:17:27
|
17 | fn get_state(&self) -> impl std::fmt::Debug;
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0562`.
Copy the code
But we’re just going to treat it as a return value, so how do we do that?
impl vs dyn
The key to using Traits here is whether Rust knows the specific type at compile time, whether it should use dyn or impl if you want to use Traits using dyn. The reason we can’t use IMPL STD :: FMT ::Debug here is that each specific implementation returns a different type, whereas using DYn sacrifices performance for flexibility. Once a value is described as dyN, it loses type information and is essentially binary data pointing to a trait method
So we changed the code as follows:
trait Light {
fn get_name(&self) - > &str;
fn get_state(&self) - > &dyn std::fmt::Debug;
}
impl Light for HouseLight {
fn get_name(&self) - > &str {
"House light"
}
fn get_state(&self) - > &dyn std::fmt::Debug{&self.on
}
}
impl Light for TrafficLight {
fn get_name(&self) - > &str {
"Traffic light"
}
fn get_state(&self) - > &dyn std::fmt::Debug{&self.color
}
}
Copy the code
Note: Rust must know the size of each variable at compile time, but dyn [trait] is an exception because it is not any concrete type and does not have a known size
Now our complete code looks like this:
use std::fmt::Display;
fn main() {
let traffic_light = TrafficLight::new();
let house_light = HouseLight::new();
print_state(&traffic_light);
print_state(&house_light);
}
fn print_state(light: &impl Light) {
println!("{}'s state is : {:? }", light.get_name(), light.get_state());
}
trait Light {
fn get_name(&self) - > &str;
fn get_state(&self) - > &dyn std::fmt::Debug;
}
impl Light for HouseLight {
fn get_name(&self) - > &str {
"House light"
}
fn get_state(&self) - > &dyn std::fmt::Debug{&self.on
}
}
impl Light for TrafficLight {
fn get_name(&self) - > &str {
"Traffic light"
}
fn get_state(&self) - > &dyn std::fmt::Debug{&self.color
}
}
impl std::fmt::Display for TrafficLight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Traffic light is {}".self.color)
}
}
#[derive(Debug)]
struct TrafficLight {
color: TrafficLightColor,
}
impl TrafficLight {
pub fn new() - >Self {
Self {
color: TrafficLightColor::Red,
}
}
pub fn turn_green(&mut self) {
self.color = TrafficLightColor::Green
}
}
#[derive(Debug)]
enum TrafficLightColor {
Red,
Yellow,
Green,
}
impl Display for TrafficLightColor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let color_string = match self {
TrafficLightColor::Green => "green",
TrafficLightColor::Red => "red",
TrafficLightColor::Yellow => "yellow"};write!(f, "{}", color_string)
}
}
#[derive(Debug)]
struct HouseLight {
on: bool,}impl Display for HouseLight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Houselight is {}".if self.on { "on" } else { "off"}}})impl HouseLight {
pub fn new() - >Self {
Self { on: false}}}Copy the code
The printed result is:
[snipped]
Traffic light's state is : Red
House light's state is : false
Copy the code
Now that our code is getting bigger and bigger, is it time to learn how to split code
read
- The Rust Book: ch 10.02
- Rust by Example: Traits
- Common Rust Traits
- The Rust Reference: Traits
conclusion
Traits are so ubiquitous in the Rust world that it’s worth digging into them. You can read Rust code on Github, or take a look at the entire library implementation. While there may be a single correct way to implement a feature in some languages, there is no equivalent in Rust, where there are 1000 different ways to implement a feature. It is very important to read other people’s code and not be a closed book when learning Rust
Note: The fact that there are 1,000 ways to implement a feature makes Rust a spiritual successor to Perl
In the next article, we’ll cover the module system. You can pick it up pretty quickly, but since you’re probably moving from Node.js, and node.js has the simplest module system I’ve ever seen, you’ll probably have some bumps in the beginning, but you’ll get the hang of it pretty quickly after overcoming a few bumps
More and more
- Rust tutorial (1) From NVM to Rust
- Rust tutorial (2) from NPM to Cargo for the front end
- Configure Visual Studio Code in Rust (3)
- Rust tutorial (4) Hello World
- Rust Tutorial for the front end (5) Borrowing & Ownership
- Part 1 of Rust Tutorial (6) String
- Rust Tutorial (7)
- Rust Tutorial (8)
- Rust Tutorial (9)
- Rust tutorial for the front end (10) from Mixins to Traits
- Rust Tutorial (11) Module for the front-end