Ожидаемый трейт A, найденный &A при попытке упаковать трейт-объект

Я пытаюсь создать трейт, который может либо извлекать (и возвращать ссылку) объект трейта другого трейта, либо создавать его (и возвращать его коробочную версию), оставляя выбор разработчику (что означает, что мне нужно чтобы ограничить время жизни возвращаемого объекта временем жизни производителя). Однако я сталкиваюсь с ошибками:

use std::borrow::Borrow;
use std::collections::HashMap;

trait A { 
    fn foobar(&self) {
        println!("!"); 
    } 
}

trait ProducerOrContainer {
    fn get_a<'a>(&'a self, name: &'a str) -> Option<Box<dyn A + 'a>>;
}

impl<'b, B: Borrow<A>> ProducerOrContainer for HashMap<&'b str, B> {
    fn get_a<'a>(&'a self, name: &'a str) -> Option<Box<dyn A + 'a>> {
        self.get(name).map(|borrow| Box::new(borrow.borrow()))
    }
}

Ошибка:

error[E0308]: mismatched types
  --> src/main.rs:20:9
   |
20 |         self.get(name).map(|borrow| Box::new(borrow.borrow()))
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected trait A, found &A
   |
   = note: expected type `std::option::Option<std::boxed::Box<dyn A + 'a>>`
              found type `std::option::Option<std::boxed::Box<&dyn A>>`

Что меня озадачивает, потому что я ожидал, что &A тоже будет A. Я пробовал impl<'a> A for &'a A, но и это не помогает. Есть ли способ исправить это?


person Michail    schedule 16.09.2018    source источник
comment
A реализация признака не означает, что &A реализует. Иногда может показаться, что это правда из-за поведения авто-ссылки и авто-размещения в Rust. Например, он автоматически разыщет переменную, чтобы вы могли ввести foo.bar() вместо (*foo).bar().   -  person Peter Hall    schedule 16.09.2018
comment
Зачем вообще нужно возвращать Box<&dyn A>? Бокс ссылки не кажется очень полезным.   -  person Peter Hall    schedule 16.09.2018
comment
Я заметил, что name: &'a str здесь излишне ограничен, потому что возвращаемая ссылка не привязана к name. Возможно, у вас есть другие реализации ProducerOrContainer, в которых возвращаемое значение является производным от name, но если нет, избавление от 'a будет более гибким.   -  person trentcl    schedule 16.09.2018


Ответы (2)


... который может либо извлекать (и возвращать ссылку на) трейт-объект другого трейта, либо создавать его (и возвращать его упакованную версию).

С этим требованием Box работать не будет. Box владеет своими данными, но иногда у вас есть заимствованные данные, которые вы не можете переместить.

В стандартной библиотеке есть тип под названием Cow, который является абстракцией того, является ли значение заимствованным или принадлежащим. Однако здесь он может быть не совсем подходящим для вас, потому что он не позволит вам владеть данными как Box, а также требует, чтобы ваш тип данных реализовывал ToOwned.

Но мы можем взять ваше требование и смоделировать его непосредственно как enum:

enum BoxOrBorrow<'a, T: 'a + ?Sized> {
    Boxed(Box<T>),
    Borrowed(&'a T),
}

И сделайте его эргономичным в использовании, внедрив Deref:

use std::ops::Deref;

impl<'a, T> Deref for BoxOrBorrow<'a, T> {
    type Target = T;
    fn deref(&self) -> &T {
        match self {
            BoxOrBorrow::Boxed(b) => &b,
            BoxOrBorrow::Borrowed(b) => &b,
        }
    }
}

Это позволяет обрабатывать пользовательский тип BoxOrBorrow как любую другую ссылку — вы можете разыменовать его с помощью * или передать любой функции, которая ожидает ссылку на T.

Вот как будет выглядеть ваш код:

trait ProducerOrContainer {
    fn get_a<'a>(&'a self, name: &'a str) -> Option<BoxOrBorrow<'a, dyn A + 'a>>;
}

impl<'b, B: Borrow<dyn A>> ProducerOrContainer for HashMap<&'b str, B> {
    fn get_a<'a>(&'a self, name: &'a str) -> Option<BoxOrBorrow<'a, dyn A + 'a>> {
        self.get(name)
            .map(|b| BoxOrBorrow::Borrowed(b.borrow()))
    }
}
person Peter Hall    schedule 16.09.2018

Вы можете компилировать исходный код, реализовав A для &'_ dyn A и добавив явное приведение:

self.get(name).map(|borrow| Box::new(borrow.borrow()) as Box<dyn A>)

Закрытие не является сайтом принуждения. Компилятор просматривает содержимое замыкания, чтобы увидеть возвращаемое значение, и заключает, что он возвращает Box<&'a dyn A>. Но само замыкание не может быть преобразовано из "функции, возвращающей Box<&'a dyn A>" в "функцию, возвращающую Box<dyn A + 'a>", потому что эти типы структурно различны. Вы добавляете приведение, чтобы сообщить компилятору, что вы хотите, чтобы замыкание возвращало Box<dyn A> в первую очередь.

Но это немного глупо. Boxссылка здесь совершенно не нужна, а приведение ее к Box<dyn A> просто добавляет еще один уровень косвенности для вызывающей стороны. Было бы лучше вернуть тип, который инкапсулирует идею «либо упакованный тип-объект, или ссылка на типаж-объект», как ответ Питера Холла описывает.


В будущей версии Rust с универсальными связанными типами («GAT») можно будет сделать возвращаемый тип ассоциированным типом ProducerOrContainer, что-то вроде следующего:

trait ProducerOrContainer {
    type Result<'a>: A;
    fn get_a<'a>(&'a self, name: &'a str) -> Option<Result<'a>>;
}

С этим определением черты каждый тип, реализующий ProducerOrContainer, может выбирать, какой тип он возвращает, поэтому вы можете выбрать Box<dyn A> для одних impl и &'a dyn A для других. Однако в текущей версии Rust (1.29) это невозможно.

person trentcl    schedule 16.09.2018