skip to content
Mehdi Mehdikhani
Table of Contents

Traits are Rust’s way of defining shared behavior across different types. They’re like interfaces in other languages, but more powerful - you can implement traits for types you didn’t even write.

What Traits Actually Are

A trait defines a set of methods that types can implement:

trait Drawable {
fn draw(&self);
// Default implementation is optional
fn area(&self) -> f64 {
0.0 // Default for shapes without area
}
}
struct Circle {
radius: f64,
}
impl Drawable for Circle {
fn draw(&self) {
println!("Drawing a circle with radius {}", self.radius);
}
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}

Any type that implements Drawable can be drawn and has an area.

Generic Functions with Traits

Traits really shine with generics - you can write functions that work with any type implementing a specific trait:

fn draw_shape<T: Drawable>(shape: &T) {
shape.draw();
println!("Area: {:.2}", shape.area());
}
// Alternative syntax
fn draw_shapes(shapes: &[impl Drawable]) {
for shape in shapes {
shape.draw();
}
}
// Multiple trait bounds
fn compare_and_draw<T>(a: &T, b: &T)
where
T: Drawable + PartialEq + std::fmt::Debug
{
println!("Comparing: {:?} vs {:?}", a, b);
if a == b {
println!("Shapes are equal.");
}
a.draw();
b.draw();
}

When I Actually Use Traits

Most of the time, I use traits for:

  1. Defining common behavior: Making different types work the same way:
trait Processor {
type Input;
type Output;
type Error;
fn process(&self, input: Self::Input) -> Result<Self::Output, Self::Error>;
}
struct JsonProcessor;
struct XmlProcessor;
impl Processor for JsonProcessor {
type Input = String;
type Output = serde_json::Value;
type Error = serde_json::Error;
fn process(&self, input: String) -> Result<Self::Output, Self::Error> {
serde_json::from_str(&input)
}
}
impl Processor for XmlProcessor {
type Input = String;
type Output = String; // Simplified - would use proper XML type
type Error = String;
fn process(&self, input: String) -> Result<Self::Output, Self::Error> {
// XML parsing logic
if input.starts_with('<') {
Ok(format!("Parsed: {}", input))
} else {
Err("Not valid XML".to_string())
}
}
}
// Generic function that works with any processor
fn handle_data<P: Processor>(processor: P, data: P::Input) {
match processor.process(data) {
Ok(result) => println!("Processed successfully"),
Err(error) => eprintln!("Processing failed: {:?}", error),
}
}
  1. Building extensible systems: Plugin-like architecture:
trait EventHandler {
fn can_handle(&self, event_type: &str) -> bool;
fn handle(&self, event: &str) -> Result<(), String>;
}
struct LogHandler;
struct EmailHandler;
struct DatabaseHandler;
impl EventHandler for LogHandler {
fn can_handle(&self, event_type: &str) -> bool {
event_type.starts_with("log_")
}
fn handle(&self, event: &str) -> Result<(), String> {
println!("LOG: {}", event);
Ok(())
}
}
impl EventHandler for EmailHandler {
fn can_handle(&self, event_type: &str) -> bool {
event_type == "user_registered" || event_type == "password_reset"
}
fn handle(&self, event: &str) -> Result<(), String> {
println!("Sending email for: {}", event);
// Email sending logic
Ok(())
}
}
struct EventSystem {
handlers: Vec<Box<dyn EventHandler>>,
}
impl EventSystem {
fn new() -> Self {
Self { handlers: Vec::new() }
}
fn register_handler<T: EventHandler + 'static>(mut self, handler: T) -> Self {
self.handlers.push(Box::new(handler));
self
}
fn dispatch(&self, event_type: &str, event_data: &str) {
for handler in &self.handlers {
if handler.can_handle(event_type) {
if let Err(e) = handler.handle(event_data) {
eprintln!("Handler failed: {}", e);
}
}
}
}
}
  1. Implementing standard traits: Making your types work with Rust ecosystem:
#[derive(Debug, Clone, PartialEq)] // Derive common traits
struct User {
id: u32,
name: String,
email: String,
}
// Custom Display implementation
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} <{}>", self.name, self.email)
}
}
// Custom ordering
impl Ord for User {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.name.cmp(&other.name)
}
}
impl PartialOrd for User {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for User {}
// Now User works with sorting, printing, etc.
let mut users = vec![
User { id: 1, name: "Charlie".to_string(), email: "c@example.com".to_string() },
User { id: 2, name: "Alice".to_string(), email: "a@example.com".to_string() },
User { id: 3, name: "Bob".to_string(), email: "b@example.com".to_string() },
];
users.sort(); // Works because we implemented Ord
for user in &users {
println!("{}", user); // Works because we implemented Display
}
  1. Iterator patterns: Creating custom iterators:
struct Counter {
current: usize,
max: usize,
}
impl Counter {
fn new(max: usize) -> Self {
Self { current: 0, max }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let current = self.current;
self.current += 1;
Some(current)
} else {
None
}
}
}
// Now it works with for loops and iterator methods
let counter = Counter::new(5);
let squares: Vec<usize> = counter
.map(|x| x * x)
.filter(|&x| x > 5)
.collect();
println!("Squares > 5: {:?}", squares); // [9, 16]

Associated Types vs Generic Parameters

Sometimes you want associated types instead of generic parameters:

// Generic trait - can implement multiple times for same type
trait From<T> {
fn from(value: T) -> Self;
}
// Associated type trait - only one implementation per type
trait Iterator {
type Item; // Associated type
fn next(&mut self) -> Option<Self::Item>;
}
// When to use which?
trait Convert<T> { // Generic - User can convert from many types
fn convert_from(value: T) -> Self;
}
trait AsBytes { // Associated type - User has one byte representation
type Bytes;
fn as_bytes(&self) -> &Self::Bytes;
}

Real World Example

Here’s how I use traits for a configuration system:

use std::collections::HashMap;
trait ConfigSource {
type Error;
fn load(&self) -> Result<HashMap<String, String>, Self::Error>;
fn name(&self) -> &str;
}
struct FileConfigSource {
path: String,
}
struct EnvConfigSource {
prefix: String,
}
struct RemoteConfigSource {
url: String,
}
impl ConfigSource for FileConfigSource {
type Error = std::io::Error;
fn load(&self) -> Result<HashMap<String, String>, Self::Error> {
let content = std::fs::read_to_string(&self.path)?;
let mut config = HashMap::new();
for line in content.lines() {
if let Some((key, value)) = line.split_once('=') {
config.insert(key.trim().to_string(), value.trim().to_string());
}
}
Ok(config)
}
fn name(&self) -> &str {
&self.path
}
}
impl ConfigSource for EnvConfigSource {
type Error = String;
fn load(&self) -> Result<HashMap<String, String>, Self::Error> {
let mut config = HashMap::new();
for (key, value) in std::env::vars() {
if key.starts_with(&self.prefix) {
let config_key = key.strip_prefix(&self.prefix)
.unwrap()
.to_lowercase();
config.insert(config_key, value);
}
}
Ok(config)
}
fn name(&self) -> &str {
"environment"
}
}
struct ConfigManager {
sources: Vec<Box<dyn ConfigSource<Error = Box<dyn std::error::Error>>>>,
}
impl ConfigManager {
fn new() -> Self {
Self { sources: Vec::new() }
}
fn add_source<S>(&mut self, source: S)
where
S: ConfigSource + 'static,
S::Error: std::error::Error + 'static,
{
// Wrap the source to have uniform error type
struct SourceWrapper<T>(T);
impl<T> ConfigSource for SourceWrapper<T>
where
T: ConfigSource,
T::Error: std::error::Error + 'static,
{
type Error = Box<dyn std::error::Error>;
fn load(&self) -> Result<HashMap<String, String>, Self::Error> {
self.0.load().map_err(|e| Box::new(e) as Box<dyn std::error::Error>)
}
fn name(&self) -> &str {
self.0.name()
}
}
self.sources.push(Box::new(SourceWrapper(source)));
}
fn load_all(&self) -> HashMap<String, String> {
let mut config = HashMap::new();
for source in &self.sources {
match source.load() {
Ok(source_config) => {
println!("Loaded {} keys from {}", source_config.len(), source.name());
config.extend(source_config);
},
Err(e) => {
eprintln!("Failed to load from {}: {}", source.name(), e);
}
}
}
config
}
}

Trait Objects and Dynamic Dispatch

When you need to store different types implementing the same trait:

trait Animal {
fn make_sound(&self);
fn name(&self) -> &str;
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn make_sound(&self) { println!("Woof!"); }
fn name(&self) -> &str { &self.name }
}
impl Animal for Cat {
fn make_sound(&self) { println!("Meow!"); }
fn name(&self) -> &str { &self.name }
}
// Store different animals together
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: "Rex".to_string() }),
Box::new(Cat { name: "Whiskers".to_string() }),
];
for animal in &animals {
println!("{} says:", animal.name());
animal.make_sound();
}

Traits make Rust’s type system flexible while maintaining safety. They’re key to writing generic, reusable code that doesn’t sacrifice performance.