#rust
#дженерики #Ржавчина #Трейты
Вопрос:
У меня есть структура конфигурации, которая выглядит следующим образом:
struct Conf {
list: Vec<String>,
}
Реализация заполняла list
элемент внутренне, но теперь я решил, что хочу делегировать эту задачу другому объекту. Итак, у меня есть:
trait ListBuilder {
fn build(amp;self, list: amp;mut Vec<String>);
}
struct Conf<T: Sized ListBuilder> {
list: Vec<String>,
builder: T,
}
impl<T> Conf<T>
where
T: Sized ListBuilder,
{
fn init(amp;mut self) {
self.builder.build(amp;mut self.list);
}
}
impl<T> Conf<T>
where
T: Sized ListBuilder,
{
pub fn new(lb: T) -> Self {
let mut c = Conf {
list: vec![],
builder: lb,
};
c.init();
c
}
}
Кажется, это работает нормально, но теперь везде, которые я использую Conf
, я должен это изменить:
fn do_something(c: amp;Conf) {
// ...
}
становится
fn do_something<T>(c: amp;Conf<T>)
where
T: ListBuilder,
{
// ...
}
Поскольку у меня много таких функций, это преобразование является болезненным, тем более что большинство вариантов использования Conf
класса не заботятся о ListBuilder
— это деталь реализации. Я обеспокоен тем, что если я добавлю другой универсальный тип в Conf
, теперь мне придется вернуться и добавить еще один универсальный параметр везде. Есть ли какой-либо способ избежать этого?
Я знаю, что вместо этого я мог бы использовать замыкание для list builder, но у меня есть дополнительное ограничение, которым должна быть моя Conf
структура Clone
, а фактическая реализация builder более сложная и имеет несколько функций и некоторое состояние в builder, что делает подход закрытия громоздким.
Комментарии:
1. Привязка не требуется
Sized
; это значение по умолчанию.
Ответ №1:
Хотя может показаться, что универсальные типы «заражают» остальной код, именно поэтому они полезны! Знания компилятора о том, насколько велик и конкретно какой тип используется, позволяют ему принимать лучшие решения по оптимизации.
При этом это может раздражать! Если у вас есть небольшое количество типов, которые реализуют вашу черту, вы также можете создать перечисление этих типов и делегировать дочерним реализациям:
enum MyBuilders {
User(FromUser),
File(FromFile),
}
impl ListBuilder for MyBuilders {
fn build(amp;self, list: amp;mut Vec<String>) {
use MyBuilders::*;
match self {
User(u) => u.build(list),
File(f) => f.build(list),
}
}
}
// Support code
trait ListBuilder {
fn build(amp;self, list: amp;mut Vec<String>);
}
struct FromUser;
impl ListBuilder for FromUser {
fn build(amp;self, list: amp;mut Vec<String>) {}
}
struct FromFile;
impl ListBuilder for FromFile {
fn build(amp;self, list: amp;mut Vec<String>) {}
}
Теперь конкретным типом будет Conf<MyBuilders>
, который вы можете скрыть с помощью псевдонима типа.
Я использовал это с хорошим эффектом, когда хотел иметь возможность внедрять тестовые реализации в код во время тестирования, но имел фиксированный набор реализаций, которые использовались в производственном коде.
Ящик enum_dispatch помогает создать этот шаблон.
Ответ №2:
Вы можете использовать объект trait Box<dyn ListBuilder>
, чтобы скрыть тип конструктора. Некоторые из последствий — это динамическая отправка (вызовы build
метода будут проходить через таблицу виртуальных функций), дополнительное выделение памяти (упакованный объект признака) и некоторые ограничения на признак ListBuilder
.
trait ListBuilder {
fn build(amp;self, list: amp;mut Vec<String>);
}
struct Conf {
list: Vec<String>,
builder: Box<dyn ListBuilder>,
}
impl Conf {
fn init(amp;mut self) {
self.builder.build(amp;mut self.list);
}
}
impl Conf {
pub fn new<T: ListBuilder 'static>(lb: T) -> Self {
let mut c = Conf {
list: vec![],
builder: Box::new(lb),
};
c.init();
c
}
}
Комментарии:
1. Спасибо, это было в значительной степени то, что мне было нужно. Моему ListBuilder также потребовалось реализовать Clone, что я решил с помощью метода, описанного здесь: users.rust-lang.org/t /…