В библиотечном API проверьте, достаточно ли bound трейта для вызова clone у ассоциированного типа без повторения ограничения:
trait Container {
type Item: Clone;
fn item(&self) -> Self::Item;
}
fn duplicate<C: Container>(container: C) -> (C::Item, C::Item) {
let value = container.item();
(value.clone(), value)
}
Скомпилируется ли функция duplicate?
Да, duplicate скомпилируется. Ограничение type Item: Clone является частью контракта трейта, поэтому из bound C: Container компилятор выводит, что C::Item: Clone.
Повторять C::Item: Clone в сигнатуре duplicate не требуется. Это отличается от ситуации, когда трейт вообще не устанавливает ограничение для своего ассоциированного типа.
Ассоциированные типы позволяют трейту описывать тип результата, однозначно связанный с конкретной реализацией. Ограничение на ассоциированный тип помещает обязательное свойство результата непосредственно в контракт трейта, а не заставляет каждый вызывающий код формулировать его заново.
Такой подход решает проблему распределённых ограничений: без него разные функции могли бы по-разному предполагать свойства Item, хотя сам трейт уже требует их для всех реализаций.
В duplicate тип C известен только через bound C: Container. На первый взгляд компилятору может быть неизвестно, реализует ли C::Item трейт Clone, поскольку конкретная реализация Container ещё не выбрана.
Если bound ассоциированного типа не считается частью гарантии Container, вызов value.clone() был бы отклонён. Тогда пришлось бы повторять ограничение в каждой обобщённой функции, работающей с C::Item.
Запись:
означает, что любая корректная реализация Container обязана выбрать для Item тип, реализующий Clone. Поэтому при наличии C: Container доступно и следствие C::Item: Clone.
В функции value имеет тип C::Item. Вызов value.clone() создаёт копию этого значения, а исходное value затем перемещается во вторую позицию кортежа. Это корректно, потому что вызов clone не перемещает исходное значение.
Ограничение можно записать и на уровне where:
Смысл тот же: реализация трейта допустима только при выполнении ограничения для Item. Однако bound непосредственно в объявлении ассоциированного типа обычно лучше выражает, что свойство относится именно к этому типу.
Важно отличать такой контракт от ограничения в отдельной функции. Если объявить type Item; без Clone, то C: Container не даст права вызвать clone у C::Item; bound придётся добавить отдельно, например C::Item: Clone.
Это ограничение относится к типу, выбранному реализацией, а не к каждому отдельному значению. Конкретный C может иметь только один согласованный Item, и для него это свойство проверяется при компиляции реализации.
Предположим, библиотека предоставляет обобщённый адаптер, который получает результат из хранилища и сохраняет его копию для повторного использования. Если протокол хранилища всегда требует копируемый результат, разумно объявить type Item: Clone в самом трейте.
Вариант с ограничением только на метод адаптера слабее: другие методы могли бы возвращать некопируемые значения, а пользователи получали бы несогласованный контракт и ошибки только при использовании конкретной операции. Вариант с отдельным generic-параметром вместо ассоциированного типа дал бы возможность одной реализации работать с несколькими типами результата, но это изменило бы модель API и могло бы усложнить вывод типов.
Выбранный вариант с bound у ассоциированного типа оправдан, если у каждой реализации ровно один тип результата и он всегда должен быть Clone. В результате функции, принимающие C: Container, получают эту гарантию автоматически, а некорректные реализации отклоняются ещё при компиляции.
C::Item: Clone в каждой функции, использующей C: Container?Нет, если Clone указан в контракте ассоциированного типа трейта. Bound C: Container предоставляет гарантию C::Item: Clone. Повторение ограничения иногда используют для читаемости или при более сложных проекциях типов, но для приведённого случая оно избыточно.
Тогда гарантия будет локальной для этого метода. Например, если Clone требуется лишь методу duplicate, его можно выразить через where Self::Item: Clone; наличие C: Container само по себе не позволит другим функциям вызывать clone у C::Item.
Это полезно, когда свойство нужно не всем реализациям или не всем операциям. Bound на объявлении Item сильнее: он становится обязательным условием существования реализации трейта.
C::Item: Clone гарантированным, если C — trait object?Само наличие bound у ассоциированного типа не делает ассоциированный тип доступным у dyn Container. Для trait object обычно требуется явно зафиксировать значение ассоциированного типа, например dyn Container<Item = String>, если такой объект допустим с учётом остальных требований объектной безопасности.
Следовательно, bound Item: Clone гарантирует свойство выбранного ассоциированного типа, но не устраняет отдельные ограничения trait object и не сообщает компилятору, какой именно тип скрывается за Item.