Introduction
If you have programmed before, probably you have ever used or heard interface. But what is interface? Well, an interface is a description of the actions that an object can do. In Object Oriented Programming(OOP), an Interface is a description of all functions that an object must have in order to be an "X". In Java, we can write like this.
interface Drawable{
void draw();
}
class Rectangle implements Drawable{
public void draw(){
System.out.println("drawing rectangle");
}
}
class Circle implements Drawable{
public void draw(){
System.out.println("drawing circle");
}
}
class TestInterface1{
public static void main(String args[]){
//In real scenario, object is provided by method e.g. getDrawable()
Drawable d = new Circle();
d.draw();
}}
As you can see, there is one interface called Drawable and two classes: Rectangle, Circle. Drawable interface has one method which is draw(), then as a result of implementing Drawable interface, each class can have draw method and being able to implement specific function like printing "drawing rectangles" for Rectangle class, printing "drawing circle" for Circle class. Other OOP language can write an interface like the above. But how about Rust? Is there any similar way? Yes, Rust does have a concept of interface is called "Trait". In this blog, I will explain what is Trait and how to use it.
What are traits in Rust?
A trait in Rust is a group of methods that are defined for particular type, then allow us to share behavior across their types and facilitates code reuse. They also enable us to write functions that accept different types for their arguments, as long as the type implements the required behavior in the form of trait.
Let's see an example from Rust standard io library.
pub trait Write {
// Required methods
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
// Provided methods
fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize> { ... }
fn is_write_vectored(&self) -> bool { ... }
fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { ... }
fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { ... }
fn by_ref(&mut self) -> &mut Self
where Self: Sized { ... }
}
Write trait is one of popular trait in Rust. As you can see, to define a trait, we use the trait keyword instead of interface. There are many methods in Write trait. Each function plays an important role to write bytes array. A struct that have to control bytes array like File, TcpStream implement this write trait.
Another example from here
pub trait ToString {
// Required method
fn to_string(&self) -> String;
}
This "to_string" function is very useful when you want to convert to String type. For examle, you have a Person object or struct.
struct Person {
name: String,
age: u32,
}
when you want to print out this person's information, you can write like this
fn main () {
let person = Person { name: "Tom".to_string(), age: 32, };
println!("{} is a {} year old", person.name, person.age);
}
This is one way to print out this person's information. However, you can also write like this
impl ToString for Person {
fn to_string(&self) -> String{
return format!("{} is a {} year old", self.name, self.age);
}
}
fn main() {
let person = Person{name: "Tom".to_string(), age: 32,};
println!("{}", person.to_string());
}
*fomat!() is a macro that create a String using interpolation of runtime expression.
This is very useful when you have multiple person. If you implement ToString trait for person, you can call to_string() method from each person.
Implementing a trait in Rust
In previous section, I touched a bit of implementing a trait, but to be more specific, to implement a trait, you have to declare an impl block for the type you want to implement the trait for. The syntax is like this.
impl <trait> for <type> {}
Similar to implementing regular methods, you can write trait name after impl keyword. After trait name, use the for keyword in addition to name of the type you want to implement the trait for. You will need to implement all the methods that don't have default implementations.
trait WithName {
fn new(name: String) -> Self;
fn get_name(&self) -> &str;
fn print(&self) {
println!("My name is {}", self.get_name())
}
}
struct Name(String);
impl WithName for Name {
fn new(name: String) -> Self {
Name(name)
}
fn get_name(&self) -> &str {
&self.0
}
}
How to use traits in Rust
Traits as Parameters
You can use trait as function parameters to allow the function to accept any type that can do x, where x is some behavior defined by a trait. You can also use trait bounds to refine and restrict generics, like accept any type T that implements a specified trait.
Let's see an example.
use std::io::Write;
fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
out.write_all(b"hello world\n")?;
out.flush();
}
say_hello method take one parameter that type is &mut dyn Write. It means "parameter will take mutable reference that implement Write trait". After declaring say_hello method, you can use like
use std::fs::File;
let mut local_file = File::create("hello.txt")?;
say_hello(&mut local_file)?; // ok
let mut bytes = vec![];
say_hello(&mut bytes)?; // ok
First of all, you create hello.txt file, then pass to say_hello method. It is acceptable because File struct already implemented a Write trait, as I mentioned in previous chapter.
In addition, you can also write like this as a generic function
fn say_hello<W: Write>(out: &mut W) -> std::io::Result<()> {
out.write_all(b"hello world\n");
out.flush();
}
Pay attention to parameter, I used W instead of dyn Write. The generic type W specified as the type of the out parameters constrains the function such that the concrete type of the value passed as na argument for out must be the same.
You can also specify more than one trait bound. This is what's known as a trait combo. To do this, you add the traits together:
W: Trait1 + Trait2 + Trait3 ....
fn say_hello<W: Write + Debug>(out: &mut W) -> std::io::Result<()> {
out.write_all(b"hello world\n");
out.flush();
}
Returning Types that implement Traits
You can use traits as return types from functions.
use std::fmt::Debug;
fn impl_trait(n: u8) -> impl Debug {
todo!()
}
By using impl Debug for the return type, you specify that the impl_trait function returns some type that implements the Debug trait without naming the concrete type.
Conclusion
I explored the difference between other programming language like Java and Rust, the concept of trait, how to define and how to implement trait, and how to use it. Hopefully, you have learned something new. However, this is just introduction of trait in Rust, unfortunately. I will explain about trait object in next blog. Thank you for reading. See you in next blog.