Когда уместно использовать связанный тип по сравнению с универсальным типом?

109

В этом вопросе возникла проблема, которую можно было решить, изменив попытку использования параметра универсального типа на связанный тип. Это вызвало вопрос «Почему здесь более уместен связанный тип?», Который заставил меня захотеть узнать больше.

В RFC, который представил связанные типы, говорится:

Этот RFC разъясняет сопоставление признаков по:

  • Обработка всех параметров типа признака как входных типов и
  • Предоставление связанных типов, которые являются типами вывода .

RFC использует структуру графа в качестве мотивирующего примера, и это также используется в документации , но я признаю, что не в полной мере оцениваю преимущества связанной версии типа над версией с параметризацией типа. Главное, что distanceметоду не нужно заботиться о Edgeтипе. Это хорошо, но кажется немного поверхностной причиной вообще наличия связанных типов.

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

Когда при написании кода мне следует выбирать связанный тип вместо параметра универсального типа, а когда делать наоборот?

Шепмастер
источник

Ответы:

76

Теперь это затронуто во втором издании языка программирования Rust . Однако давайте немного углубимся в детали.

Начнем с более простого примера.

Когда уместно использовать метод черты?

Существует несколько способов обеспечения позднего связывания :

trait MyTrait {
    fn hello_word(&self) -> String;
}

Или:

struct MyTrait<T> {
    t: T,
    hello_world: fn(&T) -> String,
}

impl<T> MyTrait<T> {
    fn new(t: T, hello_world: fn(&T) -> String) -> MyTrait<T>;

    fn hello_world(&self) -> String {
        (self.hello_world)(self.t)
    }
}

Игнорируя любую стратегию реализации / производительности, оба приведенных выше отрывка позволяют пользователю динамически указывать, как hello_worldему себя вести.

Единственное отличие (семантически) заключается в том, что traitреализация гарантирует, что для данного типа, Tреализующего trait, hello_worldвсегда будет одно и то же поведение, тогда как structреализация позволяет иметь другое поведение для каждого экземпляра.

Уместно ли использование метода или нет, зависит от варианта использования!

Когда целесообразно использовать связанный тип?

Подобно traitвышеприведенным методам, связанный тип представляет собой форму позднего связывания (хотя это происходит при компиляции), позволяя пользователю traitуказать для данного экземпляра, какой тип следует заменить. Это не единственный способ (отсюда вопрос):

trait MyTrait {
    type Return;
    fn hello_world(&self) -> Self::Return;
}

Или:

trait MyTrait<Return> {
    fn hello_world(&Self) -> Return;
}

Эквивалентны позднему связыванию вышеперечисленных методов:

  • первый обеспечивает, что для данного Selfсуществует единственный Returnсвязанный
  • второй, вместо этого, позволяет реализовать MyTraitдля Selfдля несколькихReturn

Какая форма более уместна, зависит от того, имеет ли смысл обеспечивать единство или нет. Например:

  • Deref использует связанный тип, потому что без уникальности компилятор сошёл бы с ума во время вывода
  • Add использует связанный тип, потому что его автор думал, что с учетом двух аргументов будет логический тип возврата

Как вы можете видеть, хотя Derefэто очевидный вариант использования (техническое ограничение), случай Addменее ясен: может быть, имеет смысл i32 + i32уступить либо то, i32либо другое в Complex<i32>зависимости от контекста? Тем не менее, автор высказал свое мнение и решил, что перегрузка типа возвращаемого значения для дополнений не требуется.

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

Матье М.
источник
4
Попробую немного упростить: trait/struct MyTrait/MyStructпозволяет ровно одно impl MyTrait forили impl MyStruct. trait MyTrait<Return>допускает несколько impls, потому что он общий. Returnможет быть любого типа. Общие структуры такие же.
Пол-Себастьян Маноле
2
Я считаю, что ваш ответ намного проще для понимания, чем ответ из «Язык программирования Rust»
drojf
«первый обеспечивает, чтобы для данного Я был связан единственный Возвращение». Это верно в прямом смысле слова, но, конечно, можно обойти это ограничение, создав подклассы с универсальным признаком. Возможно , единственность может быть только предложение, а не исполнение
Joel
37

Связанные типы - это механизм группировки , поэтому их следует использовать, когда есть смысл группировать типы вместе.

GraphЧерта введена в документации является примером этого. Вы хотите, чтобы a Graphбыл общим, но когда у вас есть конкретный тип Graph, вы не хотите, чтобы типы Nodeили Edgeбольше менялись. Конкретный Graphобъект не захочет изменять эти типы в рамках одной реализации, а фактически хочет, чтобы они всегда были одинаковыми. Они сгруппированы вместе или, можно даже сказать, связаны .

Стив Клабник
источник
5
Мне потребовалось время, чтобы понять. На мой взгляд, это больше похоже на определение нескольких типов одновременно: Edge и Node не имеют смысла вне графика.
tafia