Overview of Common Standard Traits

A guided tour of the most frequently encountered traits in Rust's standard library and how they shape a type's behavior

Rust encodes fundamental type behaviors directly into the type system using a collection of fine-grained traits. Rather than relying on hidden compiler magic for operations like copying, comparing, or printing, the standard library makes these capabilities explicit through traits that types can implement. This gives you, the programmer, full control—and the compiler gives clear error messages when a trait is missing.

Most of these traits can be automatically implemented for your own types using #[derive(...)] macros. The derived implementations do what you'd expect: comparing field-by-field, cloning each member in turn, and so on. The compiler also requires that every field of a struct or variant of an enum implements the trait in question before it can auto-generate the code.

Familiarity with the most common standard traits lets you scan a type definition and immediately understand what operations it supports.

The traits covered here:

This page gives an overview of each trait's purpose, when to use it, and the most important pitfalls. Deeper explanations, manual implementations, and advanced usage live in the dedicated pages under Utility Traits Deep Dive later in this chapter.

Debug

Debug is the trait for programmer-facing output. It defines what you see when you print a value with the {:?} format specifier or use the dbg! macro.

The primary job of Debug is to display the internal state of a type in a way that helps you understand what's happening during development. It makes no promises about formatting stability—the output format can change between compiler versions. Libraries should not attempt to parse Debug output.

Debug can be derived for almost any type whose fields all implement Debug. The derived implementation prints the type name followed by its fields and their values.

#[derive(Debug)]
struct SensorReading {
    temperature: f64,
    humidity: f64,
    timestamp: u64,
}
fn main() {
    let reading = SensorReading {
        temperature: 23.5,
        humidity: 0.64,
        timestamp: 1710300000,
    };
    println!("{:?}", reading);
}

Running this prints SensorReading { temperature: 23.5, humidity: 0.64, timestamp: 1710300000 }. Each field's Debug implementation handles its own formatting, so nested structs and standard library types all work automatically.

Derive coverage:

If every field in your struct or every variant in your enum implements Debug, adding #[derive(Debug)] is enough. You will get no compile errors—the trait is ready to use immediately.

Clone

Clone provides the ability to explicitly create a duplicate of a value. Calling .clone() runs user-defined code to produce a new, independent copy. For most types, this means allocating new memory for heap-allocated data like String or Vec.

Unlike C++ copy constructors, Rust never calls .clone() automatically. You always see the call site, which makes potentially expensive copies obvious.

The derive macro for Clone clones each field in order. This is the right behavior for the majority of types. You should avoid deriving Clone when a type represents unique ownership of a resource—like a file handle or a mutex guard—where duplicating the resource would break invariants.

#[derive(Clone, Debug)]
struct Config {
    host: String,
    port: u16,
}
fn main() {
    let original = Config {
        host: "localhost".to_string(),
        port: 8080,
    };
    let duplicate = original.clone();
    println!("{:?} and {:?}", original, duplicate);
}

Here, cloning the Config clones its String field by allocating a new buffer and copying the bytes. After cloning, original and duplicate are independent.

Clone is not a copy:

Using .clone() on a type that implements Copy compiles, but it is slower than a plain assignment. The compiler (and clippy) will warn you. When a type is Copy, just use assignment—no .clone() needed.

Copy

Copy is a marker trait—it has no methods. Implementing Copy tells the compiler that a bitwise copy of the type's memory representation always produces a valid, independent value. In C++ terms, a Copy type behaves like a "plain old data" (POD) type.

The practical effect is dramatic: assignment of a Copy type performs a bitwise copy and the original variable remains usable. Without Copy, assignment moves the value, and the original variable can no longer be accessed.

#[derive(Debug, Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}
fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = p1; // p1 is bitwise-copied, not moved
    println!("p1 = {:?}, p2 = {:?}", p1, p2); // both usable
}

Without the Copy derive, the same code would fail with "borrow of moved value". The trait fundamentally changes how the compiler treats assignments for that type.

Copy can only be derived if all fields are themselves Copy. Types like String, Vec, and anything that manages heap memory cannot be Copy because a bitwise copy would duplicate pointers without duplicating the pointed-to data, leading to double frees.

Copy and Drop are incompatible:

Rust enforces that you cannot implement Copy for a type that also implements Drop. The reasoning: Drop runs custom cleanup code that must execute exactly once per value. Bitwise copies would confuse that tracking. If your type needs a destructor, it must not be Copy.

PartialEq and Eq

PartialEq allows you to compare two values with == and !=. The "partial" in the name means the comparison is allowed to be a partial equivalence relation: every value can be compared, but the relation x == x might not always hold. The classic example is floating-point NaNNaN == NaN is false.

Eq is a marker trait that extends PartialEq and guarantees a full equivalence relation. It adds no methods; it just tells the compiler and users that x == x is always true for this type. Most types can implement Eq. The main exception is floating-point numbers, which implement PartialEq but not Eq.

Both traits can be derived when all fields support the corresponding comparison. The derived PartialEq compares fields in order and short-circuits on the first inequality. Derived Eq requires that all fields be Eq.

#[derive(Debug, PartialEq, Eq)]
enum Status {
    Active,
    Inactive,
    Suspended,
}
fn main() {
    let a = Status::Active;
    let b = Status::Active;
    let c = Status::Inactive;
    assert!(a == b);
    assert!(a != c);
}

Having both traits on a type means == behaves as most people expect: reflexive, symmetric, and transitive.

Always derive Eq when you can:

If your type's comparison is reflexive—meaning any value is equal to itself—derive Eq alongside PartialEq. It doesn't cost anything and improves the type's contract for generic code that requires Eq.

PartialOrd and Ord

PartialOrd enables the <, >, <=, and >= operators. Like its equality counterpart, it describes a partial order: some pairs of values may not be comparable. Floating-point types again provide the canonical example—NaN is not comparable to any number, so 3.0 < NaN is false and 3.0 > NaN is also false.

Ord is a marker trait that guarantees a total order: for any two values a and b, exactly one of a < b, a == b, or a > b is true. Most types with a natural ordering (integers, strings, enums without explicit discriminants) can implement both.

The derive macros generate lexicographic comparison: fields are compared in declaration order, and the first difference determines the result. This only works when every field implements the corresponding ordering trait.

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Version {
    major: u32,
    minor: u32,
    patch: u32,
}
fn main() {
    let v1 = Version { major: 2, minor: 1, patch: 0 };
    let v2 = Version { major: 2, minor: 0, patch: 3 };
    assert!(v1 > v2); // compares major first, then minor
}

Ord types can be used as keys in BTreeMap and are compatible with methods like sort() on slices. The total order property ensures those data structures behave correctly.

Hash

Hash allows a type to produce a stable hash value—a fixed-size integer summary of its contents. Hash-based collections like HashMap and HashSet use this trait to store and retrieve values efficiently.

The Hash trait requires a single method, hash(), that feeds the type's fields into a provided Hasher. The derive macro calls hash() on each field in order, which is correct for any struct or enum whose fields all implement Hash.

Two values that are equal according to Eq must produce the same hash. If they don't, hash-based collections will break. The derive macro enforces this automatically because it hashes the same fields that PartialEq compares.

use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, Hash)]
struct UserId(u32);
fn main() {
    let mut scores = HashMap::new();
    scores.insert(UserId(42), "Alice");
    scores.insert(UserId(7), "Bob");
    println!("{:?}", scores.get(&UserId(42))); // Some("Alice")
}

Deriving Hash is almost always what you want. Manual implementation is needed only when you want to hash a subset of fields or when the hash must be consistent with a custom equality implementation.

Default

Default provides a no-argument constructor: Default::default() returns a "sensible" default value of the type. For numeric types, that's zero. For collections, it's empty. For Option, it's None.

The derive macro creates a default by calling Default::default() on each field. This requires that every field type has a Default implementation of its own.

#[derive(Default, Debug)]
struct SearchOptions {
    query: String,         // defaults to ""
    case_sensitive: bool,  // defaults to false
    max_results: usize,    // defaults to 0
}
fn main() {
    let opts = SearchOptions::default();
    println!("{:?}", opts);
    // SearchOptions { query: "", case_sensitive: false, max_results: 0 }
}

Default is especially useful with the struct update syntax, where you can override a few fields and default the rest:

let opts = SearchOptions {
    query: "rust traits".to_string(),
    ..Default::default()
};

This pattern avoids long constructor boilerplate and clearly communicates which fields are being set explicitly.

Display

Display is the trait for user-facing output. It controls what appears when you use the {} format specifier in macros like println!, format!, and write!. While Debug is for developers, Display is for end users.

The critical difference from Debug: Display cannot be derived. The compiler cannot guess how you want your type to look to a user, so you must implement it manually. This is an intentional design choice—formatting for humans requires judgment.

use std::fmt;
struct EmailAddress {
    local: String,
    domain: String,
}
impl fmt::Display for EmailAddress {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}@{}", self.local, self.domain)
    }
}
fn main() {
    let addr = EmailAddress {
        local: "user".to_string(),
        domain: "example.com".to_string(),
    };
    println!("Contact: {}", addr);
}

Any type that implements Display automatically gets a ToString implementation, which gives you the .to_string() method. This is the idiomatic way to convert a value to an owned String for display purposes.

Don't confuse Debug and Display:

Using {} on a type that only implements Debug produces a compile error. If you see "MyType doesn't implement std::fmt::Display", either implement Display or use {:?} instead. Most production code implements both—Debug for logging and development, Display for human-readable output.

Drop

Drop is the destructor trait. Its single method, drop(), is called automatically when a value goes out of scope. This gives you a hook to release resources, close connections, or perform any other cleanup.

Most Rust code never needs to implement Drop manually. Ownership and the standard library's RAII types (like File, TcpStream, MutexGuard) handle resource management automatically. You reach for Drop when you're building your own low-level resource wrapper.

struct Connection {
    id: u32,
    active: bool,
}
impl Drop for Connection {
    fn drop(&mut self) {
        if self.active {
            println!("Closing connection {}", self.id);
            // actual cleanup logic here
        }
        self.active = false;
    }
}
fn main() {
    let conn = Connection { id: 42, active: true };
    // conn goes out of scope here → drop() is called
}

The compiler guarantees drop() runs even if the function panics, so it's safe to place critical cleanup in this method. Values are dropped in reverse order of declaration, so a struct's fields are dropped after the struct's own drop() method runs.

Send and Sync

Send and Sync are auto traits—marker traits that the compiler automatically implements for types when it is safe to do so. You almost never derive or implement them manually. They govern what can safely cross thread boundaries in concurrent programs.

  • Send: a type is Send if ownership of values of this type can be transferred to another thread. Almost every type in Rust is Send. Notable exceptions are Rc<T> (which uses non-atomic reference counting) and raw pointers.
  • Sync: a type is Sync if shared references (&T) can be accessed from multiple threads simultaneously. A type is Sync when all its fields are Sync. Mutex<T> is Sync if T is Send because the mutex ensures exclusive access.

These traits appear constantly in the signatures of concurrency primitives. std::thread::spawn requires that the closure it runs be Send. Arc<T> requires T: Send + Sync to be shared across threads safely.

use std::rc::Rc;
use std::sync::Arc;
use std::thread;
fn main() {
    let shared = Rc::new(42);
    // thread::spawn(move || {
    //     println!("{}", shared); // ❌ Rc<i32> is not Send
    // });
    let shared = Arc::new(42);
    thread::spawn(move || {
        println!("{}", shared); // ✅ Arc<i32> is Send
    }).join().unwrap();
}

If a type contains only Send and Sync fields, the compiler automatically marks it as Send and Sync. Raw pointers and types like Cell/RefCell explicitly opt out of these traits because they allow interior mutability without synchronization.

Manual unsafe impls:

Manually implementing Send or Sync requires unsafe because you are promising the compiler that your type is thread-safe. Only do this when building safe abstractions over raw synchronization primitives. For all normal application code, rely on the automatic implementations.

How these traits work together

A typical data structure in Rust derives several of these traits at once, creating a type that is printable, comparable, cloneable, and usable in collections:

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
enum Color {
    #[default]
    Black,
    Red,
    Green,
    Blue,
}

This single line gives Color a rich set of behaviors: it can be debug-printed, cloned and copied by value, compared for equality and ordering, used as a key in hash maps, and defaulted to Black. The compiler guarantees that all these implementations are consistent with each other.

The standard traits are designed to be orthogonal and composable. You rarely need all of them, but knowing what each one means lets you choose exactly the capabilities your type should expose.


This overview introduced the traits that appear in nearly every Rust codebase.