Skip to content

S14: Deref used to imitate inheritance

S14Severity: Medium

A wrapper type that auto-dereferences to a "base class".

Clippy: no lint finds this. Look for it in review.

“Employee extends Person”, written with Deref and DerefMut:

pub struct Person {
pub name: String,
pub email: String,
}
impl Person {
pub fn greeting(&self) -> String {
format!("Dear {}", self.name)
}
}
/// "Employee extends Person".
pub struct Employee {
pub person: Person,
pub salary_cents: u64,
}
impl Deref for Employee {
type Target = Person;
fn deref(&self) -> &Person {
&self.person
}
}
impl DerefMut for Employee {
fn deref_mut(&mut self) -> &mut Person {
&mut self.person
}
}
pub fn welcome(employee: &Employee) -> String {
format!("{} <{}>", employee.greeting(), employee.email) // looks like inherited members
}

In welcome, employee.greeting() and employee.email look like members of Employee. They are members of Person, reached through deref().

  • It is the closest thing to extends. The agent wants Employee to have every field and method of Person. Deref makes the method call syntax find them.
  • It removes compile errors quickly. After a refactoring, code that called person.greeting() on an Employee fails. One impl Deref fixes every call at once.
  • It appears in blog posts as a “trick” for inheritance in Rust.
  • Employee is still not a Person. Deref coercion works for one reference at a time: &Employee becomes &Person in a function call. It does not work for collections, generics, or trait bounds. A &Vec<Employee> is not a &[Person]:
Does not compile: E0308
use smells::s14_deref_for_inheritance::worse::{Employee, Person};
fn mail_all(people: &[Person]) -> usize { people.len() }
let staff: Vec<Employee> = Vec::new();
mail_all(&staff); // error E0308: expected `&[Person]`, found `&Vec<Employee>`
  • Traits are not inherited. If Person implements Display or Serialize, Employee does not. Generic code that needs those traits rejects it.
  • The reader cannot see where a member comes from. employee.email does not tell you that the field is in another struct. With several levels, finding a method means following each Deref.
  • DerefMut gives away the inner value. Any caller can change the Person through an &mut Employee, including fields that Employee wants to control.
  • The documentation says not to. The Deref documentation says to implement it only when a value of the type “transparently behaves like a value of the target type”, and users will not be surprised by deref coercion. An Employee has a salary, so it does not behave like a Person.
  1. WorseDeref and DerefMut from Employee to Person, so that Person members look inherited.

    pub struct Person {
    pub name: String,
    pub email: String,
    }
    impl Person {
    pub fn greeting(&self) -> String {
    format!("Dear {}", self.name)
    }
    }
    /// "Employee extends Person".
    pub struct Employee {
    pub person: Person,
    pub salary_cents: u64,
    }
    impl Deref for Employee {
    type Target = Person;
    fn deref(&self) -> &Person {
    &self.person
    }
    }
    impl DerefMut for Employee {
    fn deref_mut(&mut self) -> &mut Person {
    &mut self.person
    }
    }
    pub fn welcome(employee: &Employee) -> String {
    format!("{} <{}>", employee.greeting(), employee.email) // looks like inherited members
    }
  2. BetterComposition: employee.person is a named field. Shared behavior is a small trait, Contact.

    pub struct Person {
    pub name: String,
    pub email: String,
    }
    impl Person {
    pub fn greeting(&self) -> String {
    format!("Dear {}", self.name)
    }
    }
    pub struct Employee {
    pub person: Person, // composition: an employee has a person
    pub salary_cents: u64,
    }
    pub fn welcome(employee: &Employee) -> String {
    let person = &employee.person; // the reader sees where the data comes from
    format!("{} <{}>", person.greeting(), person.email)
    }
    /// Code that works for any "has a contact" type takes a trait, not a "base class".
    pub trait Contact {
    fn email(&self) -> &str;
    }
    impl Contact for Employee {
    fn email(&self) -> &str {
    &self.person.email
    }
    }

The “better” welcome names employee.person, so the reader sees where the data comes from. Code that must work for employees, customers, and suppliers takes &impl Contact, and each type says how it provides an email.

Deref is correct for smart pointers and thin wrappers whose value is the target, with an extra guarantee:

/// A smart pointer or a thin wrapper that **is** its target, with extra
/// guarantees: `Deref` is what the trait is for. Read-only, no `DerefMut`.
pub struct NonEmptyList(Vec<String>);
impl NonEmptyList {
pub fn new(first: String, rest: Vec<String>) -> Self {
let mut items = vec![first];
items.extend(rest);
Self(items)
}
pub fn first(&self) -> &str {
&self.0[0] // never empty, so this cannot panic
}
}
impl Deref for NonEmptyList {
type Target = [String];
fn deref(&self) -> &[String] {
&self.0
}
}

NonEmptyList is a list of strings that is never empty. Every slice method (len, iter, contains) is correct for it, so Deref to [String] is honest. It has no DerefMut, because a caller must not be able to remove the last item. Box, Rc, String (to str), and Vec (to a slice) use Deref the same way.

  • No Clippy lint reports this pattern.
  • Search for impl Deref for and impl DerefMut for. For each one, ask: is Target a smart pointer target or a wrapped value with the same meaning? Or is it a “base class” with other data next to it?
  • A struct that has other fields next to its Deref target (salary_cents next to person) is almost always the smell.
Remove the Deref and DerefMut implementations that make Employee look like
a subclass of Person. Access the inner struct through its field
(employee.person.name). If several types need the same behavior, define a
small trait for that behavior and implement it for each type. Keep Deref
only for smart pointers or wrappers that are their target. Run the tests.

Related: S13: Trait hierarchy that copies a Java class hierarchy, Traits vs interfaces, and Smart pointers.