patterns/anti_patterns/deref.md
Jaxel Rojas fb57f21ec1
Fix syntax error on code snippet (#246)
* Fix syntax error on code snippet

* Added comma to struct definition on code snippet

* cargo fmt, remove ignore flag

Co-authored-by: Marco Ieni <11428655+MarcoIeni@users.noreply.github.com>
2021-03-10 13:40:13 +01:00

4.1 KiB

Deref polymorphism

Description

Abuse the Deref trait to emulate inheritance between structs, and thus reuse methods.

Example

Sometimes we want to emulate the following common pattern from OO languages such as Java:

class Foo {
    void m() { ... }
}

class Bar extends Foo {}

public static void main(String[] args) {
    Bar b = new Bar();
    b.m();
}

We can use the deref polymorphism anti-pattern to do so:

use std::ops::Deref;

struct Foo {}

impl Foo {
    fn m(&self) {
        //..
    }
}

struct Bar {
    f: Foo,
}

impl Deref for Bar {
    type Target = Foo;
    fn deref(&self) -> &Foo {
        &self.f
    }
}

fn main() {
    let b = Bar { f: Foo {} };
    b.m();
}

There is no struct inheritance in Rust. Instead we use composition and include an instance of Foo in Bar (since the field is a value, it is stored inline, so if there were fields, they would have the same layout in memory as the Java version (probably, you should use #[repr(C)] if you want to be sure)).

In order to make the method call work we implement Deref for Bar with Foo as the target (returning the embedded Foo field). That means that when we dereference a Bar (for example, using *) then we will get a Foo. That is pretty weird. Dereferencing usually gives a T from a reference to T, here we have two unrelated types. However, since the dot operator does implicit dereferencing, it means that the method call will search for methods on Foo as well as Bar.

Advantages

You save a little boilerplate, e.g.,

impl Bar {
    fn m(&self) {
        self.f.m()
    }
}

Disadvantages

Most importantly this is a surprising idiom - future programmers reading this in code will not expect this to happen. That's because we are abusing the Deref trait rather than using it as intended (and documented, etc.). It's also because the mechanism here is completely implicit.

This pattern does not introduce subtyping between Foo and Bar like inheritance in Java or C++ does. Furthermore, traits implemented by Foo are not automatically implemented for Bar, so this pattern interacts badly with bounds checking and thus generic programming.

Using this pattern gives subtly different semantics from most OO languages with regards to self. Usually it remains a reference to the sub-class, with this pattern it will be the 'class' where the method is defined.

Finally, this pattern only supports single inheritance, and has no notion of interfaces, class-based privacy, or other inheritance-related features. So, it gives an experience that will be subtly surprising to programmers used to Java inheritance, etc.

Discussion

There is no one good alternative. Depending on the exact circumstances it might be better to re-implement using traits or to write out the facade methods to dispatch to Foo manually. We do intend to add a mechanism for inheritance similar to this to Rust, but it is likely to be some time before it reaches stable Rust. See these blog posts and this RFC issue for more details.

The Deref trait is designed for the implementation of custom pointer types. The intention is that it will take a pointer-to-T to a T, not convert between different types. It is a shame that this isn't (probably cannot be) enforced by the trait definition.

Rust tries to strike a careful balance between explicit and implicit mechanisms, favouring explicit conversions between types. Automatic dereferencing in the dot operator is a case where the ergonomics strongly favour an implicit mechanism, but the intention is that this is limited to degrees of indirection, not conversion between arbitrary types.

See also