S14: Deref used to imitate inheritance
What it looks like
Section titled “What it looks like”“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().
Why AI agents produce it
Section titled “Why AI agents produce it”- It is the closest thing to
extends. The agent wantsEmployeeto have every field and method ofPerson.Derefmakes the method call syntax find them. - It removes compile errors quickly. After a refactoring, code that called
person.greeting()on anEmployeefails. Oneimpl Dereffixes every call at once. - It appears in blog posts as a “trick” for inheritance in Rust.
Why it is a problem
Section titled “Why it is a problem”Employeeis still not aPerson. Deref coercion works for one reference at a time:&Employeebecomes&Personin a function call. It does not work for collections, generics, or trait bounds. A&Vec<Employee>is not a&[Person]:
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
PersonimplementsDisplayorSerialize,Employeedoes not. Generic code that needs those traits rejects it. - The reader cannot see where a member comes from.
employee.emaildoes not tell you that the field is in another struct. With several levels, finding a method means following eachDeref. DerefMutgives away the inner value. Any caller can change thePersonthrough an&mut Employee, including fields thatEmployeewants to control.- The documentation says not to. The
Derefdocumentation 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. AnEmployeehas a salary, so it does not behave like aPerson.
The better version
Section titled “The better version”Worse
DerefandDerefMutfromEmployeetoPerson, so thatPersonmembers 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}BetterComposition:
employee.personis 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 personpub salary_cents: u64,}pub fn welcome(employee: &Employee) -> String {let person = &employee.person; // the reader sees where the data comes fromformat!("{} <{}>", 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.
When it is acceptable
Section titled “When it is acceptable”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.
How to detect it
Section titled “How to detect it”- No Clippy lint reports this pattern.
- Search for
impl Deref forandimpl DerefMut for. For each one, ask: isTargeta 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
Dereftarget (salary_centsnext toperson) is almost always the smell.
What to tell your agent
Section titled “What to tell your agent”Remove the Deref and DerefMut implementations that make Employee look likea subclass of Person. Access the inner struct through its field(employee.person.name). If several types need the same behavior, define asmall trait for that behavior and implement it for each type. Keep Derefonly 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.