Считается ли плохой практикой реализация 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 для типов, которые являются новыми типами и не являются умными указателями:

  1. actix_web::web::Json<T> является кортежной структурой (T,)и он реализует Deref<Target=T>.

  2. bstr::BString имеет одно поле напечатано Vec<u8>и он реализует Deref<Target=Vec<u8>>.

Так что, может быть, это нормально, если не злоупотреблять, например, имитировать многоуровневую иерархию наследования. Я также заметил, что в двух приведенных выше примерах либо ноль общедоступных методов, либо только один into_innerметод, который возвращает внутреннее значение. Тогда кажется хорошей идеей сохранить минимальное количество методов типа оболочки.

Хороший ответ также предлагается на сайте Rust Design Patterns :: Deref polymorphism, который я рекомендую всем прочитать. Я не буду копировать его здесь, потому что он довольно большой, но все согласны с тем, что это обычно плохой шаблон.

Другие вопросы по тегам