generics - ¿Cómo puedo evitar que un efecto dominó cambie una estructura concreta a genérica?
rust traits (2)
Tengo una estructura de configuración que se ve así:
struct Conf {
list: Vec<String>,
}
La implementación estaba poblando internamente al miembro de la
list
, pero ahora he decidido que quiero delegar esa tarea a otro objeto.
Así que tengo:
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct Conf<T: Sized + ListBuilder> {
list: Vec<String>,
builder: T,
}
impl<T> Conf<T>
where
T: Sized + ListBuilder,
{
fn init(&mut self) {
self.builder.build(&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
}
}
Parece que funciona bien, pero ahora en
todas partes
donde uso
Conf
, tengo que cambiarlo:
fn do_something(c: &Conf) {
// ...
}
se convierte
fn do_something<T>(c: &Conf<T>)
where
T: ListBuilder,
{
// ...
}
Dado que tengo muchas de esas funciones, esta conversión es dolorosa, especialmente porque a la mayoría de los usos de la clase
Conf
no le importa
ListBuilder
, es un detalle de implementación.
Me preocupa que si agrego otro tipo genérico a
Conf
, ahora tengo que volver y agregar otro parámetro genérico en todas partes.
Hay alguna manera de evitar esto?
Sé que podría usar un cierre para el generador de listas, pero tengo la restricción añadida de que mi Conftruct debe ser
Clone
, y la implementación real del generador es más compleja y tiene varias funciones y un cierto estado en el generador, lo que hace que Un enfoque de cierre difícil de manejar.
Puede usar el
objeto de rasgo
Box<dyn ListBuilder>
para ocultar el tipo del constructor.
Algunas de las consecuencias son el despacho dinámico (las llamadas al método de
build
pasarán por una tabla de funciones virtuales), la asignación de memoria adicional (objeto de rasgo en recuadro) y algunas
restricciones sobre el rasgo
ListBuilder
.
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct Conf {
list: Vec<String>,
builder: Box<dyn ListBuilder>,
}
impl Conf {
fn init(&mut self) {
self.builder.build(&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
}
}
Si bien los tipos genéricos pueden "infectar" el resto de su código, ¡es exactamente por eso que son beneficiosos! El conocimiento del compilador sobre qué tan grande y específicamente qué tipo se utiliza le permite tomar mejores decisiones de optimización.
Dicho esto, ¡puede ser molesto! Si tiene una pequeña cantidad de tipos que implementan su rasgo, también puede construir una enumeración de esos tipos y delegar a las implementaciones secundarias:
struct FromUser;
impl ListBuilder for FromUser { /**/ }
struct FromFile;
impl ListBuilder for FromFile { /**/ }
enum MyBuilders {
User(FromUser),
File(FromFile),
}
impl ListBuilder for MyBuilders {
fn build(&self, list: &mut Vec<String>) {
use MyBuilders::*;
match *self {
User(ref u) => u.build(list),
File(ref f) => f.build(list),
}
}
}
Ahora el tipo concreto sería
Conf<MyBuilders>
, que puede usar un alias de tipo para ocultar.
He usado esto con buenos resultados cuando quería poder inyectar implementaciones de prueba en el código durante la prueba, pero tenía un conjunto fijo de implementaciones que se usaron en el código de producción.