The contract and the proof
You are building a game engine. You have a Circle, a Square, and a Triangle. You want a function render_all that takes a list of shapes and draws them. In JavaScript, you would iterate over the list, check if each object has a draw method, and call it. Rust refuses this approach. Rust demands a contract. You must declare which types can be drawn and what that drawing looks like before the code runs. That contract is a trait. Implementing a trait is how you prove your type fulfills the contract.
A trait defines a set of behaviors that a type can have. Think of a trait as a certification exam. The trait lists the requirements: "To be certified as Display, you must provide a fmt method." Your struct is the candidate. The impl block is the proof. You write the impl to show the compiler, "Yes, my User struct can actually format itself. Here is the code."
Minimal implementation
The syntax connects a trait to a type using the impl keyword and the for separator. The for is mandatory. It tells the compiler you are implementing a trait, not just adding methods directly to the type.
/// A trait for anything that can be summarized.
trait Summary {
/// Returns a short summary of the item.
fn summarize(&self) -> String;
}
/// A news article with a headline and author.
struct NewsArticle {
headline: String,
author: String,
}
/// Implement Summary for NewsArticle.
impl Summary for NewsArticle {
fn summarize(&self) -> String {
// Combine author and headline for the summary.
format!("By {}: {}", self.author, self.headline)
}
}
The compiler checks the impl block against the trait definition. Every method in the trait must appear in the impl with the exact same signature. If you miss a method, change the return type, or alter the arguments, the code fails to compile. The for keyword binds the contract to the type. Without it, you are writing inherent methods, and the trait remains unfulfilled.
Default methods and opt-in behavior
Real-world traits often include default implementations. Default methods provide shared logic that types can use without rewriting code. Types can still override the default if they need custom behavior.
/// A trait for types that can be converted to a URL slug.
trait ToSlug {
/// Convert the value to a lowercase, hyphenated string.
fn to_slug(&self) -> String;
/// Check if the value is empty before converting.
/// Default implementation assumes non-empty is valid.
fn is_valid_slug(&self) -> bool {
// Reuse to_slug to check length.
!self.to_slug().is_empty()
}
}
struct BlogPost {
title: String,
}
impl ToSlug for BlogPost {
fn to_slug(&self) -> String {
// Replace spaces with hyphens and lowercase.
self.title.to_lowercase().replace(' ', "-")
}
}
fn main() {
let post = BlogPost { title: "Rust Traits".to_string() };
// Call the method defined in the impl.
println!("{}", post.to_slug());
// Call the default method from the trait.
println!("{}", post.is_valid_slug());
}
Default methods reduce boilerplate without sacrificing flexibility. The type provides the core data transformation, and the trait supplies the utility logic. Default methods let you share logic without forcing every type to repeat it.
Generic implementations and bounds
Traits work seamlessly with generics. You can implement a trait for a generic type, often with bounds that restrict the implementation to types that satisfy certain conditions.
/// A trait for logging values.
trait Loggable {
fn log(&self);
}
struct Wrapper<T> {
value: T,
}
// Implement Loggable for Wrapper<T> only when T is also Loggable.
impl<T: Loggable> Loggable for Wrapper<T> {
fn log(&self) {
println!("Wrapper logging...");
self.value.log();
}
}
struct SimpleValue;
impl Loggable for SimpleValue {
fn log(&self) {
println!("SimpleValue logged");
}
}
fn main() {
let wrapped = Wrapper { value: SimpleValue };
wrapped.log();
}
The bound T: Loggable ensures the implementation only exists when the inner type can also log. The compiler verifies the requirements before accepting the impl. Bounds keep your implementations honest and prevent you from calling methods that might not exist.
Associated types
Some traits need the implementor to choose a concrete type. Associated types let the trait define a placeholder that the impl fills in. This keeps the trait interface clean while allowing flexibility.
/// A trait for containers that hold items of a specific type.
trait Container {
/// The type of item stored in the container.
type Item;
/// Get a reference to the item.
fn get(&self) -> &Self::Item;
}
struct Box<T>(T);
impl<T> Container for Box<T> {
// Specify the associated type.
type Item = T;
fn get(&self) -> &T {
&self.0
}
}
Associated types let the implementor pick the concrete type, keeping the trait interface clean. The compiler uses the associated type to resolve method signatures and ensure type safety across the trait boundary.
Supertraits
A trait can require that a type also implements another trait. This is called a supertrait. When you implement the child trait, you must also implement the parent trait.
/// Base trait for anything with a name.
trait Named {
fn name(&self) -> &str;
}
/// Trait for things that are named and can be described.
trait Describable: Named {
fn describe(&self) -> String;
}
struct Book {
title: String,
}
// Must implement Named because Describable requires it.
impl Named for Book {
fn name(&self) -> &str {
&self.title
}
}
impl Describable for Book {
fn describe(&self) -> String {
format!("Book: {}", self.name())
}
}
The : Named syntax in the trait definition enforces the dependency. You cannot implement Describable without first implementing Named. Supertraits build hierarchies of behavior and guarantee that base functionality exists.
Pitfalls and compiler errors
The compiler enforces trait contracts strictly. Common errors arise from mismatches between the trait definition and the implementation.
If you forget to implement a required method, the compiler rejects the code with E0046 (missing items in implementation). If you add a method that is not in the trait, you get E0407 (method is not a member of trait). You cannot add extra methods inside a trait impl. Those belong in an inherent impl block.
trait Summary {
fn summarize(&self) -> String;
}
struct Article;
// E0407: method `extra` is not a member of trait `Summary`
impl Summary for Article {
fn summarize(&self) -> String { "Summary".to_string() }
fn extra() {} // Error here
}
Lifetime mismatches trigger E0195 (lifetime bound not satisfied). If the trait method has a lifetime parameter, the impl must match it exactly.
The orphan rule prevents implementing a foreign trait on a foreign type. You cannot implement Display for String in your own crate. Both the trait and the type must be local to your crate, or at least one of them must be. This rule prevents two crates from disagreeing on how a type behaves. If you need to add a trait to a foreign type, wrap it in a newtype.
Convention aside: For standard traits like Debug, Clone, PartialEq, and Eq, the community expects you to use #[derive] unless you have a specific reason to customize. Manual implementations of derived traits are rare and usually signal a mistake or a very specific optimization. Use #[derive] to keep your code concise and maintainable.
The orphan rule protects the ecosystem. Work around it with newtypes, don't fight it.
When to implement a trait
Choose the implementation style based on ownership and reuse needs.
Use impl Trait for Type when you own the type and want to add shared behavior that other types might also have. Use #[derive] when the trait has a standard implementation like Debug or Clone and you don't need custom logic. Use a newtype wrapper when you need to implement a foreign trait on a foreign type, like adding Serialize to a third-party struct. Use inherent impl blocks for methods that are unique to the type and don't fit a shared interface. Use generic implementations with bounds when the behavior depends on properties of the type parameters.
Match the implementation to the ownership and reuse pattern. Pick the tool that aligns with your design goals.