bookworm-smart-assistant/skills/rust-engineer/references/traits.md

8.0 KiB

Traits, Generics, and Type System

Basic Trait Definition

// Simple trait
trait Drawable {
    fn draw(&self);
}

// Trait with default implementation
trait Describable {
    fn describe(&self) -> String {
        String::from("No description available")
    }
}

// Implementing traits
struct Circle {
    radius: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Drawing circle with radius {}", self.radius);
    }
}

impl Describable for Circle {
    fn describe(&self) -> String {
        format!("A circle with radius {}", self.radius)
    }
}

Associated Types

// Associated types vs generic parameters
trait Container {
    type Item;

    fn add(&mut self, item: Self::Item);
    fn get(&self, index: usize) -> Option<&Self::Item>;
}

impl Container for Vec<i32> {
    type Item = i32;

    fn add(&mut self, item: i32) {
        self.push(item);
    }

    fn get(&self, index: usize) -> Option<&i32> {
        self.get(index)
    }
}

// Iterator trait (standard library example)
trait MyIterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Generic Traits and Bounds

// Generic trait with multiple bounds
fn print_info<T>(item: &T)
where
    T: std::fmt::Display + std::fmt::Debug,
{
    println!("Display: {}", item);
    println!("Debug: {:?}", item);
}

// Generic struct with trait bounds
struct Pair<T: PartialOrd> {
    first: T,
    second: T,
}

impl<T: PartialOrd> Pair<T> {
    fn new(first: T, second: T) -> Self {
        Self { first, second }
    }

    fn larger(&self) -> &T {
        if self.first > self.second {
            &self.first
        } else {
            &self.second
        }
    }
}

// Blanket implementation
trait MyTrait {
    fn do_something(&self);
}

impl<T: std::fmt::Display> MyTrait for T {
    fn do_something(&self) {
        println!("Value: {}", self);
    }
}

Trait Objects (Dynamic Dispatch)

// Static dispatch (monomorphization)
fn static_dispatch<T: Drawable>(item: &T) {
    item.draw();
}

// Dynamic dispatch (trait objects)
fn dynamic_dispatch(item: &dyn Drawable) {
    item.draw();
}

// Storing trait objects
struct Canvas {
    shapes: Vec<Box<dyn Drawable>>,
}

impl Canvas {
    fn new() -> Self {
        Self { shapes: Vec::new() }
    }

    fn add_shape(&mut self, shape: Box<dyn Drawable>) {
        self.shapes.push(shape);
    }

    fn draw_all(&self) {
        for shape in &self.shapes {
            shape.draw();
        }
    }
}

// Object safety: traits must meet criteria
trait ObjectSafe {
    fn method(&self);  // OK: takes &self
}

trait NotObjectSafe {
    fn generic<T>(&self);  // NOT OK: generic method
    fn by_value(self);     // NOT OK: takes self by value
}

Derive Macros

// Standard derive macros
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct User {
    id: u64,
    name: String,
}

// Deriving more traits
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Point {
    x: i32,
    y: i32,
}

// Custom derive with serde
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Config {
    host: String,
    port: u16,
}

Advanced Trait Patterns

// Extension trait pattern
trait StringExt {
    fn truncate_to(&self, max_len: usize) -> String;
}

impl StringExt for str {
    fn truncate_to(&self, max_len: usize) -> String {
        if self.len() <= max_len {
            self.to_string()
        } else {
            format!("{}...", &self[..max_len])
        }
    }
}

// Sealed trait pattern (prevent external implementation)
mod sealed {
    pub trait Sealed {}
}

pub trait MySealed: sealed::Sealed {
    fn method(&self);
}

struct MyType;
impl sealed::Sealed for MyType {}
impl MySealed for MyType {
    fn method(&self) {
        println!("Implemented");
    }
}

// Supertraits
trait Printable {
    fn print(&self);
}

trait Loggable: Printable {  // Supertrait: must also impl Printable
    fn log(&self) {
        self.print();  // Can call supertrait methods
    }
}

Associated Constants

trait Config {
    const MAX_SIZE: usize;
    const DEFAULT_TIMEOUT: u64;
}

struct ServerConfig;

impl Config for ServerConfig {
    const MAX_SIZE: usize = 1024;
    const DEFAULT_TIMEOUT: u64 = 30;
}

fn use_config<T: Config>() {
    println!("Max size: {}", T::MAX_SIZE);
}

Generic Associated Types (GATs)

// GATs allow generics in associated types
trait LendingIterator {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

struct WindowsMut<'data, T> {
    data: &'data mut [T],
    index: usize,
}

impl<'data, T> LendingIterator for WindowsMut<'data, T> {
    type Item<'a> = &'a mut [T] where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>> {
        if self.index >= self.data.len() {
            return None;
        }

        let start = self.index;
        self.index += 2;

        Some(&mut self.data[start..start.min(self.data.len())])
    }
}

Marker Traits

use std::marker::{PhantomData, Send, Sync};

// Send: type can be transferred across thread boundaries
// Sync: type can be shared between threads (&T is Send)

// Custom marker trait
trait Trusted {}

struct TrustedData<T> {
    data: T,
    _marker: PhantomData<T>,
}

impl<T: Trusted> TrustedData<T> {
    fn new(data: T) -> Self {
        Self {
            data,
            _marker: PhantomData,
        }
    }
}

Operator Overloading

use std::ops::{Add, Mul};

#[derive(Debug, Clone, Copy)]
struct Vector2D {
    x: f64,
    y: f64,
}

impl Add for Vector2D {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

impl Mul<f64> for Vector2D {
    type Output = Self;

    fn mul(self, scalar: f64) -> Self {
        Self {
            x: self.x * scalar,
            y: self.y * scalar,
        }
    }
}

// Usage
let v1 = Vector2D { x: 1.0, y: 2.0 };
let v2 = Vector2D { x: 3.0, y: 4.0 };
let v3 = v1 + v2;
let v4 = v1 * 2.5;

From/Into Conversion Traits

struct UserId(u64);

impl From<u64> for UserId {
    fn from(id: u64) -> Self {
        UserId(id)
    }
}

// Into is automatically implemented
fn accept_user_id(id: impl Into<UserId>) {
    let user_id = id.into();
    println!("User ID: {}", user_id.0);
}

// TryFrom for fallible conversions
use std::convert::TryFrom;

impl TryFrom<i64> for UserId {
    type Error = &'static str;

    fn try_from(value: i64) -> Result<Self, Self::Error> {
        if value < 0 {
            Err("User ID cannot be negative")
        } else {
            Ok(UserId(value as u64))
        }
    }
}

Const Traits (Nightly)

// Const trait implementations (requires nightly)
#![feature(const_trait_impl)]

#[const_trait]
trait ConstAdd {
    fn add(self, other: Self) -> Self;
}

impl const ConstAdd for i32 {
    fn add(self, other: Self) -> Self {
        self + other
    }
}

const fn compute() -> i32 {
    5.add(10)  // Can use in const context
}

Best Practices

  • Prefer associated types when there's one clear type per implementation
  • Use generic parameters when multiple types might be used simultaneously
  • Keep traits small and focused (single responsibility)
  • Use extension traits to add functionality to existing types
  • Document trait requirements and invariants
  • Use marker traits for compile-time guarantees
  • Prefer static dispatch for performance, dynamic dispatch for flexibility
  • Use #[derive] when possible instead of manual implementations
  • Implement standard traits (Debug, Clone, etc.) for better ecosystem integration
  • Use sealed traits to prevent external implementations when needed