Traits in Rust are similar to interfaces in other languages. When a type implements a trait, that type will behave as the trait describes.
pub trait Shape {
fn area(&self) -> f32; // Traits declarations have ';' as opposed to {} blocks
}
pub struct Circle {
radius: f32,
}
impl Shape for Circle {
fn area(&self) -> f32 {
self.radius * 3.14159
}
}
pub struct Rectable {
width: f32,
height: f32,
}
impl Shape for Rectable {
fn area(&self) -> f32 {
self.width * self.height
}
}
fn main() {
let cir = Circle { radius: 12.25 };
println!("{}", cir.area());
}
Associated types act as a type placeholder for traits in method signatures. When the trait is implemented, the associated type is replaced with the concrete type.
struct Point {x: i32, y: i32}
trait Contains {
// Declare placeholder types
type A;
type B;
fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
}
impl Contains for Point {
// Specify types during implementation
type A = i32;
type B = i32;
fn contains(&self, num1: &Self::A, num2: &Self::B) -> bool {
(&self.x == num1) && (&self.y == num2)
}
// This would also be a valid signature:
//fn contains(&self, num1: &i32, num2: &i32) -> bool {}
}
fn main() {
let num1 = 2;
let num2 = 3;
let points = vec![Point{x: 1, y: 2}, Point{x: 2, y: 3}];
for point in points {
match point.contains(&num1, &num2) {
true => println!("Does contain point!"),
false => println!("Does NOT contain point!"),
}
}
}
You can use traits to conditionally implement methods to a generic struct. 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} // Notice Self vs self
}
}
impl<T: Display> Pair<T> {
fn print(&self) {
println!("{}, {}", self.x, self.y);
}
}
fn main() {
let p = Pair::new(5, 4);
p.print();
}
In the above example, you might notice the capital 'S' in Self
. Capital Self
can be used to refer to the type that is being implemented as opposed to self
which refers to the calling object.
A default generic type can be given for traits.
There are two main purposes for this:
use std::ops::Add;
struct Feet(f32);
struct Inches(f32);
/* // Add trait definition
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
*/
// Use default
impl Add for Feet {
type Output = Feet;
fn add(self, other: Feet) -> Feet {
Feet(self.0 + other.0)
}
}
impl Add<Inches> for Feet {
type Output = Feet;
fn add(self, other: Inches) -> Feet {
Feet(self.0 + (other.0 / 12.0))
}
}
To have a default implementation for a trait, use a {}
block.
Default implementations can also call other functions.
Example:
pub trait Shape {
fn area(&self) -> f32 {
0.0
}
}
The drop
trait is similar to the deconstructor in C++. When a variable that has resources on the heap, it needs to implement the drop
trait so when the variable leaves its scope, the resources are deallocated. If the drop
trait in implemented, the copy
trait cannot be.
Supertraits allow a trait to require the implementation of another trait. Example:
use std::ops::Add;
#[derive(Debug, PartialEq, Copy, Clone)]
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 }
}
}
// Supertrait - Has ':' with trait requirements
trait Print: std::fmt::Debug {
fn print(&self) {
println!("*** {:?} *** ", self);
}
}
impl Print for Point {}
fn main() {
let p1 = Point{x: 1, y: 2};
p1.print();
}
Traits can also be parameters. These functions are similar to generics, but restrict the types to types that implement the trait. Example:
pub trait Shape {
fn area(&self) -> f32;
}
pub trait TwoD {}
pub struct Circle {
radius: f32,
}
impl Shape for Circle {
fn area(&self) -> f32 {
self.radius * 3.14159
}
}
impl TwoD for Circle {}
// Traits as parameters - short hand
pub fn print_area(s: &impl Shape) {
println!("{}", s.area());
}
// Traits as parameters - the long way
pub fn print_area2<T: Shape>(s: &T) {
println!("{}", s.area());
}
// Multiple Traits
pub fn print_area3<T: Shape + TwoD>(s: &T) {
println!("{}", s.area());
}
// where can be used to make trait bounds look pretty
pub fn print_areas<T, U>(s: &T, r: &U)
where T: Shape + TwoD,
U: Shape
{
println!("{}", s.area());
println!("{}", r.area());
}
// Return type that implements trait
pub fn get_area(s: &str) -> impl Shape {
if s == "Circle" {
return Circle { radius: 0.0 };
}
Circle { radius: 0.0 } // Imagine other Shapes being here
}
fn main() {
let cir = Circle { radius: 12.25 };
print_area3(&cir);
print_areas(&cir, &cir);
}