Traits
A trait describes behavior that a type can provide. It is a collection of method signatures and, optionally, default method implementations.
#![allow(unused)]
fn main() {
trait Describe {
fn describe(&self) -> String;
}
}
This trait says that any type implementing Describe must provide a
describe method. The method borrows self and returns an owned String.
Traits let code depend on capabilities instead of one specific type.
Implement a trait for a type
Use an impl Trait for Type block:
trait Describe {
fn describe(&self) -> String;
}
struct User {
name: String,
}
impl Describe for User {
fn describe(&self) -> String {
format!("user: {}", self.name)
}
}
fn main() {
let user = User {
name: String::from("Ferris"),
};
println!("{}", user.describe());
}
The trait declares the required interface. The implementation supplies the
behavior for User.
Another type can implement the same trait differently:
trait Describe {
fn describe(&self) -> String;
}
struct User {
name: String,
}
struct Server {
address: String,
}
impl Describe for User {
fn describe(&self) -> String {
format!("user: {}", self.name)
}
}
impl Describe for Server {
fn describe(&self) -> String {
format!("server: {}", self.address)
}
}
fn main() {
let user = User {
name: String::from("Ferris"),
};
let server = Server {
address: String::from("127.0.0.1"),
};
println!("{}", user.describe());
println!("{}", server.describe());
}
The caller uses one method name while each type keeps its own implementation.
Default methods
A trait can provide a method body:
trait Describe {
fn name(&self) -> &str;
fn describe(&self) -> String {
format!("item: {}", self.name())
}
}
struct User {
name: String,
}
impl Describe for User {
fn name(&self) -> &str {
&self.name
}
}
fn main() {
let user = User {
name: String::from("Ferris"),
};
println!("{}", user.describe());
}
User must implement name, but it receives describe automatically. An
implementation can override a default method when it needs different behavior.
Default methods are useful when most types should share an implementation and the required behavior can be expressed through other methods in the trait.
Trait bounds constrain generics
An unconstrained generic function knows almost nothing about its type parameter. A trait bound states which behavior the function needs:
trait Describe {
fn describe(&self) -> String;
}
fn print_description<T: Describe>(value: &T) {
println!("{}", value.describe());
}
struct User { name: String }
impl Describe for User {
fn describe(&self) -> String { self.name.clone() }
}
fn main() {
let user = User { name: String::from("Ferris") };
print_description(&user);
}
T: Describe means that the function accepts any T implementing Describe.
Inside the function, Rust allows methods promised by that trait.
Without the bound, the method call fails:
#![allow(unused)]
fn main() {
fn print_description<T>(value: &T) {
println!("{}", value.describe());
}
}
The compiler cannot assume that every possible T has a describe method.
where clauses improve longer bounds
Bounds can follow the type parameter:
use std::fmt::{Debug, Display};
fn show<T: Display + Debug>(value: T) {
println!("display: {value}");
println!("debug: {value:?}");
}
fn main() {
show(42);
}
The + means that T must implement both traits.
A where clause expresses the same requirement more clearly when a signature
has several parameters:
use std::fmt::{Debug, Display};
fn show_pair<T, U>(first: T, second: U)
where
T: Display + Debug,
U: Display,
{
println!("first: {first} ({first:?})");
println!("second: {second}");
}
fn main() {
show_pair(42, "hello");
}
Use whichever form is easier to read. They have the same meaning.
impl Trait in parameters
impl Trait is a shorter way to accept a value implementing a trait:
use std::fmt::Display;
fn show(value: impl Display) {
println!("{value}");
}
fn main() {
show(42);
show("hello");
}
For a single parameter, this is similar to:
use std::fmt::Display;
fn show<T: Display>(value: T) {
println!("{value}");
}
fn main() {}
Named type parameters are necessary when the signature must express a relationship. These two parameters must have the same concrete type:
use std::fmt::Display;
fn show_same<T: Display>(first: T, second: T) {
println!("{first} {second}");
}
fn main() {
show_same(10, 20);
}
By contrast, two separate impl Display parameters may have different
concrete types:
use std::fmt::Display;
fn show_different(first: impl Display, second: impl Display) {
println!("{first} {second}");
}
fn main() {
show_different(10, "twenty");
}
impl Trait in return position
A function can hide its concrete return type while promising a trait:
fn numbers() -> impl Iterator<Item = i32> {
1..=3
}
fn main() {
for number in numbers() {
println!("{number}");
}
}
The caller knows that the result is an iterator yielding i32 values. It does
not need to name the range’s concrete type.
The function must still return one concrete type on every path:
#![allow(unused)]
fn main() {
fn numbers(reverse: bool) -> impl Iterator<Item = i32> {
if reverse {
(1..=3).rev()
} else {
1..=3
}
}
}
The two branches return different iterator types. Both implement Iterator,
but return-position impl Trait does not mean “any implementing type at
runtime.” It hides one concrete type chosen by the function.
Trait objects choose implementations at runtime
A trait object, written dyn Trait, can refer to values of different concrete
types through one interface:
trait Describe {
fn describe(&self) -> String;
}
struct User;
struct Server;
impl Describe for User {
fn describe(&self) -> String {
String::from("user")
}
}
impl Describe for Server {
fn describe(&self) -> String {
String::from("server")
}
}
fn print_all(items: &[&dyn Describe]) {
for item in items {
println!("{}", item.describe());
}
}
fn main() {
let user = User;
let server = Server;
let items: [&dyn Describe; 2] = [&user, &server];
print_all(&items);
}
&dyn Describe contains a pointer to a value and metadata used to find that
value’s trait methods. The concrete implementation is selected at runtime.
This is called dynamic dispatch.
Compare the main forms:
T: Trait named generic type; static dispatch
impl Trait unnamed concrete type; usually static dispatch
dyn Trait concrete type chosen at runtime; dynamic dispatch
Generics are usually the simplest choice when a collection contains one concrete type. Trait objects are useful when one collection must contain different concrete types that share behavior.
Because dyn Trait does not have a compile-time size by itself, it appears
behind a pointer such as &dyn Trait or Box<dyn Trait>.
Associated types name outputs of a trait
A trait can declare a type that each implementation chooses:
trait Source {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
struct Counter {
current: u32,
end: u32,
}
impl Source for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.end {
self.current += 1;
Some(self.current)
} else {
None
}
}
}
fn main() {
let mut counter = Counter { current: 0, end: 2 };
println!("{:?}", counter.next());
println!("{:?}", counter.next());
println!("{:?}", counter.next());
}
Item is an associated type. The Counter implementation chooses u32, so
Counter::next returns Option<u32>.
The standard Iterator trait uses this pattern. Each iterator implementation
has one associated Item type.
Supertraits require another trait
A trait can require implementations to provide another trait first:
use std::fmt::Display;
trait Labeled: Display {
fn label(&self) -> String {
format!("value: {self}")
}
}
impl Labeled for i32 {}
fn main() {
println!("{}", 42.label());
}
Labeled: Display means every Labeled type must also implement Display.
That allows the default label method to format self.
Traits follow coherence rules
Rust prevents conflicting trait implementations. In general, you may implement a trait when your crate defines the trait or defines the target type.
This is allowed because the type is local:
use std::fmt;
struct UserId(u64);
impl fmt::Display for UserId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "user-{}", self.0)
}
}
fn main() {
println!("{}", UserId(42));
}
Implementing an external trait for an external type is not allowed:
#![allow(unused)]
fn main() {
use std::fmt;
impl fmt::Display for Vec<i32> {
fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
Ok(())
}
}
}
Both Display and Vec belong to the standard library. If every crate could
add that implementation, two dependencies could make incompatible choices.
The restriction keeps trait resolution consistent.
A small wrapper type, often called a newtype, gives your crate a local type that can implement the trait.
Common standard-library traits
These traits appear throughout ordinary Rust code:
Debugformats developer-facing output with{:?}.Displayformats user-facing output with{}.Clonecreates an explicit duplicate.Copypermits implicit copying instead of moving.PartialEqandEqdefine equality.Defaultconstructs a default value.Iteratorproduces a sequence of values.FromandIntoconvert between types.AsRefandBorrowprovide borrowed views.ReadandWritedescribe byte-oriented I/O.
Derive standard traits when their generated behavior matches the meaning of your type. Write an implementation when the behavior needs a deliberate choice.
When reading a bound, translate it into a capability. T: Clone + Debug means
the function may duplicate T explicitly and format it for diagnostics. The
bound is not decoration; it explains what the implementation is allowed to do.