A trait is a contract that defines a set of behaviors or properties that a type must implement. It can contain associated types, constants, function or method signatures, and overridable default implementations.
Definition
With No Associates
π‘ Mostly used to mark a type as having certain properties to allow in certain operations. Known as Marker Traits.
pub trait Sized { }
With the Declarations of Associates
trait Greet {
const PREFIX: &'static str;
type Item;
fn greet(&self) -> String;
}
With the Default Implementations of Associates
trait Greet {
const PREFIX: &'static str = "Hello";
fn greet(&self) -> String {
format!("{}!", String::from(Self::PREFIX))
}
}
With Supertraits
A trait must have to be implemented first before implementing the current trait.
pub trait Copy: Clone { } // π‘ Any type that copyable should be clonable
// π‘ Any type that clonable should be sized
pub trait Clone: Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) { ... }
}
π― trait Subtrait: Super or trait Subtrait: SupertraitA + SupertraitB
With Generic Types
// π‘ Convert from one type to another. If successful, retrun Type; else return associated Error
pub trait TryFrom<T>: Sized {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}
Trait Impls (Manual Implementation)
Manually implementing a shared behavior defined in a trait for a type via an impl block.
With Required Components
When a trait has only declarations of associated items, it’s required to implement all of them.
struct Person {
name: String
}
trait Greet {
const PREFIX: &'static str; // π‘ Required constant
fn greet(&self) -> String; // π‘ Required method
}
impl Greet for Person {
const PREFIX: &'static str = "Hello";
fn greet(&self) -> String {
format!("{} {}!", Self::PREFIX.to_owned(), self.name)
}
}
fn main() {
let steve = Person { name: "Steve".to_string() };
println!("{}", steve.greet()); // Hello Steve!
}
With Provided Components
When a trait has default values and default implementations, it’s possible to implement only some of them or override them.
struct Person {
name: String
}
trait Greet {
// π‘ Provided constant
const PREFIX: &'static str = "Hello";
// π‘ Provided method
fn greet(&self) -> String {
format!("{}!", String::from(Self::PREFIX))
}
}
impl Greet for Person {
// π‘ Overridden constant
const PREFIX: &'static str = "Good morning";
// π‘ Overridden method
fn greet(&self) -> String {
format!("{} {}!", Self::PREFIX.to_owned(), self.name)
}
}
fn main() {
let steve = Person { name: "Steve".to_string() };
println!("{}", steve.greet()); // Good morning Steve!
}
For Enum Types
#![allow(unused)]
enum Shape {
Circle(f64), // radius
Rectangle(u32, u32), // width, height
}
trait Area {
fn area(&self) -> f64;
}
impl Area for Shape {
fn area(&self) -> f64 {
match *self {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
Shape::Rectangle(width, height) => (width * height) as f64,
}
}
}
fn main() {
let circle = Shape::Circle(7.0);
let rect = Shape::Rectangle(5, 5);
println!("{:?}", circle.area());
println!("{:?}", rect.area());
}
For Generic Types
impl<T> Trait for Type<T>
#![allow(unused)]
struct Person<T> {
name: String,
age: T,
}
trait Greet {
fn greet(&self) -> String;
}
impl<T> Greet for Person<T> {
fn greet(&self) -> String {
format!("Hello {}!", self.name)
}
}
fn main() {
let steve = Person { name: "Steve".to_string(), age: 65 }; // π‘ age: i32
let bill = Person { name: "Bill".to_string(), age: 7.5 }; // π‘ age: f64
println!("{:?}", steve.greet()); // Hello Steve!
println!("{:?}", bill.greet()); // Hello Bill!
}
impl<T> Trait<T> for Type<T>
struct Point<T> {
x: T,
y: T,
}
trait IntoTuple<T> {
fn into_tuple(self) -> (T, T);
}
impl<T> IntoTuple<T> for Point<T> {
fn into_tuple(self) -> (T, T) {
(self.x, self.y)
}
}
fn main() {
let a = Point { x: 0, y: 1 }; // Point<i32>
let b = Point { x: "2.0", y: "2.2" }; // Point<&str>
println!("{:?}", a.into_tuple()); // (0, 1)
println!("{:?}", b.into_tuple()); // ("2.0", "2.2")
}
π¨βπ« Update the above implementation to impl<T, U> Trait<T> for Type<U>.
Derive and Auto Traits (Autogenerate Implementations)
Derive Traits
The compiler generates the trait implementation automatically for you, based on the derived attributes.
#[derive(Debug, Clone, PartialEq)]
struct Person {
name: String,
}
fn main() {
let steve = Person { name: "Steve".to_string() };
let bill = Person { name: "Bill".to_string() };
// Debug: π‘ allow debug print with {:?}
println!("{steve:?}"); // Person { name: "Steve" }
// Clone: π‘ allow duplicate data via .clone()
let gates = bill.clone();
// PartialEq: π‘ support equate the two instances via == and !=
println!("{}", steve == bill); // false
println!("{}", bill == gates); // true
}
π Check bellow Rust STD documentation pages, of these normal derive-traits.
| Trait | The functionality (implement to the type at compile-time) |
|---|---|
Debug | Enables debug-printing of the internal state with {:?} |
Default | Allows creating a default initial value with ::default() |
Clone | Allows creating a deep copy explicitly with .clone() |
PartialEq / Eq | Allows comparing instances with == and != operators |
PartialOrd / Ord | Allows comparing instances with <, <=, >, and >= operators |
Hash | Allows the type to be used as a key in a HashMap or HashSet |
π― Marker traits are the traits that have no associated constants, types, methods, etc. So, the compiler-generated implementation has only an empty impl Trait for Type { } block.
#![allow(unused)]
#[derive(Debug, Clone, Copy)] // π‘ Copy trait needs the Clone trait as a supertrait
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 0, y: 1 };
let b = a; // Copy: π‘ duplicate data. a and b are separate instances
println!("{a:?}");
println!("{b:?}");
}
// π¨βπ« Try remove Copy derive on Point and explisitly copy via .clone()
π Copy set the ability to duplicate via simply copying bits (pass by duplicating)
Auto Traits
Implicitly bound/ automatically implemented by the compiler without any keywords.
pub trait Sized { }
pub unsafe auto trait Sync { }
pub auto trait Unpin { }
#![allow(unused)]
#[derive(Debug)]
struct Point { // π‘ Sized, Send, Sync: (Automatic) Implicitly bound for most simple types in Rust
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 0, y: 1 };
std::thread::scope(|s| {
s.spawn(|| println!("{a:?}"));
s.spawn(|| println!("{a:?}"));
});
std::thread::spawn(move || println!("{a:?}")).join().unwrap();
}
| Trait | The properties (set to the type automatically) |
|---|---|
Sized | Able to determine the size at compile time |
Unpin | Safe to move in memory |
Send | Safe to move ownership across threads |
Sync | Safe to shared references between threads |
UnwindSafe | Safe to use in panic unwindings |
RefUnwindSafe | Safe to use shared references in panic unwinding |
π¨βπ« Sized/ Unsized
- βοΈ Most simple types in Rust have a fixed size known at compile time and automatically implement
Sizedmarker trait.- π― Unsized types (or dynamically sized types, DSTs) cannot be used directly as local variables, function parameters, or return values. They must always be placed behind an indirection, such as a reference (
&or&mut), aBox,Rc, orArc. Common examples include slices ([T],&str) and trait objects (dyn Trait).
- π Pointers to these types are known as “wide pointers” or “fat pointers” because they contain both a pointer to the data and additional metadata. Metadata: for slices - length (number of elements) and for trait objects - a pointer to the vtable/ virtual function table. Ex:
&[T],*mut str,Box<dyn Trait>- π‘ To allow a type to be unsized (relaxes the default Sized requirement), we can use the
?Sizedbound.
Static Dispatch
Monomorphization is a compiler optimization and implementation strategy that transforms polymorphic (generic) code into specialized, monomorphic (single-type) versions for each unique set of concrete types used in a program. This increases the binary size.
Dispatch is the process of deciding which implementation of a polymorphic function to execute, either statically at compile time or dynamically at runtime (via a lookup in a vtable).
Rust compiler generates highly optimized code blocks for each type used for a generic function. So, function calls are statically resolved at compile time and no runtime overhead.
Trait Bounds
Specify the type constraints by traits in generics (<T: Trait>) rather than using exact types. Allowed in a field types & function return and argument.
struct Point { x: i32, y: i32 }
impl Addr for Point {
fn addr(&self) -> String {
format!("{}, {}", self.x, self.y)
}
}
struct MapLocation(String, String); // latitude, longitude
impl Addr for MapLocation {
fn addr(&self) -> String {
format!("{}, {}", self.0, self.1)
}
}
// --- βοΈ main Trait & Type with Trait Bounds βοΈ
trait Addr {
fn addr(&self) -> String;
}
struct Delivery<T: Addr> {
location: T,
}
impl<T: Addr> Delivery<T> {
fn new(location: T) -> Self {
Self { location }
}
}
// --- βοΈ fn with Trait Bounds βοΈ
fn location_info<T: Addr>(location: T) {
println!("Latitude/ Longitude: {}", location.addr())
}
// ---
fn main() {
let ocean = Point { x: 35, y: 20 };
let tokyo = MapLocation("35.68951".to_string(), "139.69170".to_string());
let pkg1 = Delivery::new(ocean);
let pkg2 = Delivery::new(tokyo);
location_info(pkg1.location); // Latitude/ Longitude: 35, 20
location_info(pkg2.location); // Latitude/ Longitude: 35.68951, 139.69170
}
Opaque Types
Specify the type constraints via traits (:impl Trait) without specifying full generic syntax. But only allowed as a function return or an argument.
struct Point { x: i32, y: i32 }
impl Addr for Point {
fn addr(&self) -> String {
format!("{}, {}", self.x, self.y)
}
}
struct MapLocation(String, String); // latitude, longitude
impl Addr for MapLocation {
fn addr(&self) -> String {
format!("{}, {}", self.0, self.1)
}
}
trait Addr {
fn addr(&self) -> String;
}
// --- βοΈ Opaque Types are only allowed in function parameters
fn location_info(location: impl Addr) { // π‘ Argument-Position-Impl-Trait/ APIT
println!("Latitude/ Longitude: {}", location.addr())
}
fn tokyo_location() -> impl Addr { // π‘ Return-Position-Impl-Trait/ RPIT
MapLocation("35.68951".to_string(), "139.69170".to_string())
}
// ---
fn main() {
let ocean = Point { x: 35, y: 20 };
let tokyo = tokyo_location();
location_info(ocean); // Latitude/ Longitude: 35, 20
location_info(tokyo); // Latitude/ Longitude: 35.68951, 139.69170
}
Dynamic Dispatch
Dynamic dispatch is used when we need to handle multiple different concrete types (mix types) and resolve the trait implementation dynamically at runtime (because this is not possible with static dispatch).
π The compiler uses vtable (virtual method table) pointers. Instead of knowing the type at compile time, the program follows a pointer to a table that contains the addresses of the methods for that specific instance.
Trait Objects
struct Point { x: i32, y: i32 }
impl Addr for Point {
fn addr(&self) -> String {
format!("{}, {}", self.x, self.y)
}
}
struct MapLocation(String, String); // latitude, longitude
impl Addr for MapLocation {
fn addr(&self) -> String {
format!("{}, {}", self.0, self.1)
}
}
trait Addr {
fn addr(&self) -> String;
}
// --- βοΈ Trait Object, allow mix types and check dynamically at runtime
fn locations_info(locations: &[Box<dyn Addr>]) { // π‘ `dyn Addr` is unsized (DST) and needs a pointer
for location in locations {
println!("Latitude/ Longitude: {}", location.addr())
}
}
// ---
fn main() {
let ocean = Point { x: 35, y: 20 };
let tokyo = MapLocation("35.68951".to_string(), "139.69170".to_string());
let locations: Vec<Box<dyn Addr>> = vec![Box::new(ocean), Box::new(tokyo)];
locations_info(&locations);
// Latitude/ Longitude: 35, 20
// Latitude/ Longitude: 35.68951, 139.69170
}
Blanket Impls (One-to-Many Manual Implementation)
Blanket impls are a special kind of trait implementation that applies to all types that have a certain type constraint/bound.
A Simple Blanket Impl
#![allow(unused)]
// --- π‘ Two types with Greet implentation
struct Person { name: String }
impl Greet for Person {}
struct Stranger {}
impl Greet for Stranger {}
trait Greet {
const PREFIX: &'static str = "Hello";
fn greet(&self) -> String {
format!("{}!", String::from(Self::PREFIX))
}
}
// --- New Farewell trait
trait Farewell {
const PREFIX: &'static str = "Goodbye";
fn farewell(&self) -> String {
format!("{}!", String::from(Self::PREFIX))
}
}
// βοΈ Any type that implements Greet gets this Farewell implementation
impl<T: Greet> Farewell for T {} // π‘or impl<T> Farewell for T where T: Greet {}
// ---
fn main() {
let steve = Person { name: "Steve".to_string() };
let stranger = Stranger {};
println!("{}", steve.greet()); // Hello!
println!("{}", stranger.greet()); // Hello!
println!("{}", steve.farewell()); // Goodbye!
println!("{}", stranger.farewell()); // Goodbye!
}
Useful Blanket Impls in STD
use std::fmt;
struct Person {
name: String,
}
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Person {{ name: {}}}", self.name) // π‘ {{ }} escaping curly braces
}
}
fn main() {
let steve = Person {
name: "Steve".to_string(),
};
println!("{steve}"); // Person { name: Steve}
println!("{}", steve.to_string()); // Person { name: Steve}
}
#![allow(unused)]
use std::convert::From;
#[derive(Debug)]
struct Person {
name: String,
}
impl From<&str> for Person {
fn from(name: &str) -> Self {
Person {
name: name.to_string(),
}
}
}
fn main() {
let steve = Person::from("Steve");
let bill: Person = "Bill".into();
println!("{:?}", steve); // Person { name: "Steve" }
println!("{:?}", bill); // Person { name: "Bill" }
}
π¨βπ« Try to implement TryFrom and check the blanket implementation it provides.
π¨βπ« Before going to the next…
- π― Check Trait implementation coherence and Orphan rules.