В API тип ограничен дочерним трейтом: какие гарантии о базовом трейте следуют из такого bound?
Если тип удовлетворяет bound на дочерний трейт, Rust гарантирует, что он также реализует все его супер-трейты, то есть базовые трейты. Поэтому вызывающий код может использовать методы и ассоциированные элементы базового трейта без отдельного bound на него.
Это не означает автоматической реализации методов: реализация дочернего трейта обязана обеспечить выполнение требований супер-трейтов, а их методы обычно реализуются отдельно.
Трейты решают задачу описания общего поведения типов без привязки к конкретной иерархии классов. Однако одному трейту часто нужна другая возможность: например, форматирование объекта имеет смысл только для значения, которое можно сравнивать или преобразовывать.
Супер-трейты позволяют явно выразить такую зависимость. Вместо повторения нескольких bounds в каждом месте API зависимость объявляется один раз в определении дочернего трейта.
Предположим, библиотека вводит трейт для сериализации, но его реализация должна уметь сравнивать значения. Если зависимость не выражена через супер-трейт, каждый обобщённый метод библиотеки должен отдельно указывать оба требования.
Такой дизайн повышает риск неполных bounds: один участок кода может потребовать сериализацию, но забыть сравнение. В результате зависимость проявится позже в виде ошибки компиляции или вынудит дублировать ограничения в публичном API.
Если трейт объявлен как зависящий от другого трейта, bound на первый трейт транзитивно предоставляет bound на второй. В обобщённом коде это означает, что после проверки дочернего трейта разрешены операции, требующие базовый трейт.
Здесь T: Printable одновременно гарантирует T: Display. Метод print использует унаследованную возможность форматирования, а функция show может обращаться к Display без повторного указания этого bound.
При реализации Printable тип обязан реализовать Display; одной реализации Printable недостаточно. Супер-трейт не наследует автоматически готовую реализацию методов и не создаёт динамическую диспетчеризацию сам по себе.
Зависимость действует в направлении от дочернего трейта к супер-трейту: из T: Printable следует T: Display, но из T: Display не следует T: Printable. Если супер-трейт содержит требования, несовместимые с использованием через объект трейта, это может повлиять и на объектную пригодность дочернего трейта.
Главный компромисс — связность API. Супер-трейт делает контракт точнее и уменьшает дублирование bounds, но добавляет обязательное требование всем будущим реализациям дочернего трейта. Поэтому в супер-трейт следует выносить действительно необходимую семантическую зависимость, а не случайное удобство конкретной реализации.
В библиотеке есть трейт Renderable, который должен возвращать текстовое представление объекта. Рассматривались два варианта.
Первый — объявить Renderable независимо, а в каждой функции писать одновременно ограничения на Renderable и форматирование. Плюс этого подхода — слабая связанность трейтов; минус — дублирование и возможность непоследовательного API.
Второй — сделать Renderable зависимым от Display. Это централизует контракт: любой Renderable гарантированно форматируем. Минус — тип, которому нужна отрисовка, но не подходит семантика Display, больше не сможет реализовать этот трейт.
Для публичной библиотеки выбран второй вариант, потому что форматирование является обязательной частью смысла Renderable, а не деталью одной функции. В результате bounds у пользовательских функций стали короче, а нарушение контракта обнаруживается при реализации трейта.
Да, если дочерний трейт действительно объявлен с этим супер-трейтом. Bound на дочерний трейт в данном месте уже включает обязательства супер-трейта, поэтому отдельная запись базового bound не нужна. Дополнительный bound может быть допустим, но обычно он избыточен и ухудшает читаемость.
Нет. Получаемая гарантия — логическая зависимость требований, а не автоматически сгенерированный код. Для реализации дочернего трейта тип сначала должен удовлетворять супер-трейту, включая его обязательные методы; готовые default-методы супер-трейта могут быть использованы, но отсутствующие обязательные методы нужно реализовать.
В поддерживаемых случаях Rust допускает такое использование благодаря отношению супер-трейта, но это не следует понимать как универсальное неявное преобразование между любыми trait object. Объектность обоих трейтов, требования к Self, ограничения на методы и правила преобразования должны быть совместимы. Если дочерний или супер-трейт не может использоваться как trait object, соответствующее динамическое преобразование недоступно, хотя статические bounds продолжают работать.