Проверьте, скомпилируется ли вызов обобщённой функции для Box<dyn Describe>, и объясните результат.
trait Describe {
fn describe(&self) -> &'static str;
}
struct Item;
impl Describe for Item {
fn describe(&self) -> &'static str { "item" }
}
fn inspect<T: Describe>(value: T) {
println!("{}", value.describe());
}
fn main() {
let boxed: Box<dyn Describe> = Box::new(Item);
inspect(boxed);
}
Код не скомпилируется: при вызове inspect(boxed) параметр T выводится как Box<dyn Describe>, но реализация Describe для Box<dyn Describe> автоматически не появляется. Реализация трейта для Item не распространяется на контейнер Box<Item> или Box<dyn Describe>.
Разыменование, используемое при обычном вызове метода, не заменяет проверку generic-bound. Чтобы передавать такие значения, нужно явно работать с ссылкой на объект трейта и разрешить T быть несSized.
Generics в Rust предназначены для описания операций над конкретными типами с проверкой на этапе компиляции и обычно со статической диспетчеризацией. Trait objects решают другую задачу: позволяют скрыть конкретный тип за динамическим интерфейсом и выбирать реализацию во время выполнения.
Контейнер вроде Box<T> владеет значением T, но это не означает автоматического переноса всех реализаций трейтов с T на Box<T>. Такое автоматическое наследование создало бы неоднозначности и нарушило бы контроль над тем, какие типы действительно реализуют трейт.
Вызов функции с bound T: Describe проверяет именно тип, выведенный для T. В данном случае это не Item и не dyn Describe, а Box<dyn Describe>.
Если разработчик ожидает, что компилятор автоматически разыменует Box для удовлетворения generic-bound, код будет отвергнут. При этом выражение boxed.describe() может работать благодаря auto-deref, что делает ошибку особенно неочевидной.
У функции есть сигнатура:
Для вызова с переменной boxed компилятор выбирает T = Box<dyn Describe>. Требование становится эквивалентным Box<dyn Describe>: Describe. Но пользовательская реализация существует только для Item:
Она не создаёт автоматически реализацию Describe для Box<Item> или Box<dyn Describe>. Вызов метода через точку устроен иначе: механизм поиска метода может разыменовать Box и найти метод у содержащегося объекта. Это правило поиска метода не меняет множества реализаций трейта, доступных для проверки generic-bound.
Один из вариантов исправления — принимать ссылку и разрешить несSized-типы:
Здесь T выводится как dyn Describe, а ?Sized отменяет неявное требование Sized. Другой вариант — определить явную forwarding-реализацию для конкретного контейнера, если это допустимо для API:
После этого исходный вызов станет корректным, но такая реализация должна быть осознанной: она добавляет конкретное поведение для оболочки, а не общее правило для всех возможных контейнеров.
Допустим, функция логирования принимает T: Describe, а клиентская часть системы хранит обработчики как Box<dyn Describe>. Простое добавление Box не сделает его подходящим аргументом для generic-функции.
Можно выбрать один из вариантов:
fn inspect<T: Describe + ?Sized>(&T): решение универсально для ссылок, Box, Arc и других владельцев, но функция работает с заимствованием;&*boxed: не требует новых реализаций, однако немного раскрывает детали разыменования у вызывающего кода;Для библиотеки обычно выбирают первый вариант, если функции не нужно владеть объектом. Он совместим и с обычными значениями, и с trait objects, а динамическая диспетчеризация сохраняется только там, где вызывающий действительно передал dyn Describe.
Почему inspect(&boxed) и inspect(&*boxed) — не одно и то же?
Вариант inspect(&boxed) выводит T как Box<dyn Describe>, потому что передаётся ссылка на сам Box. Для функции fn inspect<T: Describe + ?Sized>(&T) это всё равно требует Box<dyn Describe>: Describe, которого нет. Вариант inspect(&*boxed) сначала разыменовывает Box, поэтому тип аргумента становится &dyn Describe, а T — dyn Describe.
Можно ли решить проблему добавлением T: ?Sized к исходной функции, не меняя тип аргумента?
Нет. ?Sized позволяет подставить несSized-тип, например dyn Describe, но функция всё ещё принимает значение T по значению. Trait object нельзя передать как обычное значение без указателя или ссылки, поскольку его размер не известен статически. Нужно изменить аргумент на &T, Box<T>, Arc<T> или другой подходящий владеющий тип.
Почему компилятор не создаёт автоматически общий forwarding-bound для всех Box<T>?
Потому что реализация трейта — отдельное утверждение о конкретном типе, а не свойство, автоматически наследуемое оболочками. Общее правило вроде impl<T: Describe + ?Sized> Describe for Box<T> могло бы конфликтовать с другими реализациями и не всегда однозначно определяло бы семантику методов. Поэтому forwarding должен быть предоставлен библиотекой или разработчиком явно, если он действительно нужен.