I often use the newtype pattern, but I am tired of writing my_type.0.call_to_whatever(...)
. I am tempted to implement the Deref
trait because it permits writing simpler code since I can use my newtype as if it were the underlying type in some situations, e.g.:
use std::ops::Deref;
type Underlying = [i32; 256];
struct MyArray(Underlying);
impl Deref for MyArray {
type Target = Underlying;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let my_array = MyArray([0; 256]);
println!("{}", my_array[0]); // I can use my_array just like a regular array
}
Is this a good or bad practice? Why? What can be the downsides?
the rules regarding
Deref
andDerefMut
were designed specifically to accommodate smart pointers. Because of this,Deref
should only be implemented for smart pointers to avoid confusion.ā
std::ops::Deref
I think it's a bad practice.
since I can use my newtype as if it were the underlying type in some situations
That's the problem ā it can be implicitly used as the underlying type whenever a reference is. If you implement DerefMut
, then it also applies when a mutable reference is needed.
You don't have any control over what is and what is not available from the underlying type; everything is. In your example, do you want to allow people to call as_ptr
? What about sort
? I sure hope you do, because they can!
About all you can do is attempt to overwrite methods, but they still have to exist:
impl MyArray {
fn as_ptr(&self) -> *const i32 {
panic!("No, you don't!")
}
}
Even then, they can still be called explicitly (<[i32]>::as_ptr(&*my_array);
).
I consider it bad practice for the same reason I believe that using inheritance for code reuse is bad practice. In your example, you are essentially inheriting from an array. I'd never write something like the following Ruby:
class MyArray < Array
# ...
end
This comes back to the is-a and has-a concepts from object-oriented modeling. Is MyArray
an array? Should it be able to be used anywhere an array can? Does it have preconditions that the object should uphold that a consumer shouldn't be able to break?
but I am tired of writing
my_type.0.call_to_whatever(...)
Like in other languages, I believe the correct solution is composition over inheritance. If you need to forward a call, create a method on the newtype:
impl MyArray {
fn call_to_whatever(&self) { self.0.call_to_whatever() }
}
The main thing that makes this painful in Rust is the lack of delegation. A hypothetical delegation syntax could be something like
impl MyArray {
delegate call_to_whatever -> self.0;
}
While waiting for first-class delegation, we can use crates like delegate or ambassador to help fill in some of the gaps.
So when should you use Deref
/ DerefMut
? I'd advocate that the only time it makes sense is when you are implementing a smart pointer.
Speaking practically, I do use Deref
/ DerefMut
for newtypes that are not exposed publicly on projects where I am the sole or majority contributor. This is because I trust myself and have good knowledge of what I mean. If delegation syntax existed, I wouldn't.