generics rust traits

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.