Считается ли плохой практикой реализация Deref для новых типов?
Я часто использую шаблон нового типа, но я устал писать my_type.0.call_to_whatever(...)
, Я испытываю желание реализовать Deref
черта, потому что это позволяет писать более простой код, так как я могу использовать свой новый тип, как если бы он был базовым типом в некоторых ситуациях, например:
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
}
Это хорошая или плохая практика? Зачем? Какие могут быть минусы?
3 ответа
Я думаю, что это плохая практика.
так как я могу использовать свой новый тип, как если бы он был базовым типом в некоторых ситуациях
В этом проблема - он может быть неявно использован в качестве базового типа всякий раз, когда есть ссылка. Если вы реализуете DerefMut
, то это также применяется, когда требуется изменяемая ссылка.
Вы не имеете никакого контроля над тем, что есть и что недоступно из базового типа; все. В вашем примере, вы хотите, чтобы люди могли звонить as_ptr
? Как насчет sort
? Я очень надеюсь, что вы делаете, потому что они могут!
Все, что вы можете сделать, это попытаться переписать методы, но они все еще должны существовать:
impl MyArray {
fn as_ptr(&self) -> *const i32 {
panic!("No, you don't!")
}
}
Даже тогда их все равно можно вызывать явно (<[i32]>::as_ptr(&*my_array);
).
Я считаю это плохой практикой по той же причине, по которой я считаю, что использование наследования для повторного использования кода - плохая практика. В вашем примере вы по существу наследуете от массива. Я бы никогда не написал что-то вроде следующего Ruby:
class MyArray < Array
# ...
end
Это возвращается к концепции " есть и есть" из объектно-ориентированного моделирования. Является MyArray
массив? Должен ли он быть в состоянии использовать везде, где может массив? Есть ли у него предпосылки, что объект должен поддерживать то, что потребитель не сможет сломать?
но я устал писать
my_type.0.call_to_whatever(...)
Как и в других языках, я считаю, что правильное решение - это композиция, а не наследование. Если вам нужно переадресовать вызов, создайте метод для нового типа:
impl MyArray {
fn call_to_whatever(&self) { self.0.call_to_whatever() }
}
Главное, что делает это болезненным в Rust, это отсутствие делегирования. Синтаксис гипотетического делегирования может быть что-то вроде
impl MyArray {
delegate call_to_whatever -> self.0;
}
Так, когда вы должны использовать Deref
/ DerefMut
? Я бы сказал, что единственное время, когда это имеет смысл, это когда вы используете умный указатель.
Говоря практически, я использую Deref
/ DerefMut
для новых типов, которые не публикуются в проектах, где я являюсь единственным или мажоритарным спонсором. Это потому, что я доверяю себе и хорошо знаю, что имею в виду. Если бы существовал синтаксис делегирования, я бы не стал.
Вопреки принятому ответу, я обнаружил, что некоторые популярные ящики реализуют
Deref
для типов, которые являются новыми типами и не являются умными указателями:
actix_web::web::Json<T>
является кортежной структурой(T,)
и онреализует Deref<Target=T>
.bstr::BString
имеет одно поле напечатаноVec<u8>
и онреализует Deref<Target=Vec<u8>>
.
Так что, может быть, это нормально, если не злоупотреблять, например, имитировать многоуровневую иерархию наследования. Я также заметил, что в двух приведенных выше примерах либо ноль общедоступных методов, либо только один
into_inner
метод, который возвращает внутреннее значение. Тогда кажется хорошей идеей сохранить минимальное количество методов типа оболочки.
Хороший ответ также предлагается на сайте Rust Design Patterns :: Deref polymorphism, который я рекомендую всем прочитать. Я не буду копировать его здесь, потому что он довольно большой, но все согласны с тем, что это обычно плохой шаблон.