Verás la versión en español de mi respuesta abajo, la cual fue creada usando traducción automática.
No hablo español, así que estoy utilizando una traducción automática para entender mejor tu pregunta:
I can’t speak Spanish, so I’m making a machine translation so I can understand your question better:
I need to store a list of trait objects, but the trait has a generic. However, when I create the animals variable, it tells me that I must specify the type. I need to do something where the type can be various, depending on the implementation, such that the type information is specified in the implementation.
trait Animal<T> {
fn speak(&self) -> String;
fn get_associated_value(&self) -> T;
}
enum Species {
Dog,
Cat,
}
struct Dog {
name: String,
age: i32,
species: Species,
}
impl Animal<i32> for Dog {
fn speak(&self) -> String {
format!("Woof, my name is {} and I am {} years old", self.name, self.age)
}
fn get_associated_value(&self) -> i32 {
self.age
}
}
struct Cat {
name: String,
color: String,
species: Species,
}
impl Animal<String> for Cat {
fn speak(&self) -> String {
format!("Meow, my name is {} and my color is {}", self.name, self.color)
}
fn get_associated_value(&self) -> String {
self.color.clone()
}
}
fn main() {
let mut animals: Vec<Box<dyn Animal>> = Vec::new();
animals.push(Box::new(Dog {
name: "Rex".to_string(),
age: 5,
species: Species::Dog,
}));
animals.push(Box::new(Cat {
name: "Misha".to_string(),
color: "black".to_string(),
species: Species::Cat,
}));
for animal in animals {
println!("{} has an associated value of {}", animal.speak(), animal.get_associated_value());
}
}
Answer (English)
There are two problems with abstracting over Animals
like you are trying to do. A type parameter like for Animals<T>
allows the type T
not only to depend on the implementation, as you noted, too, but it even allows multiple implementations with different parameters. I.e. you can create something like
impl Animal<i32> for Dog {
fn speak(&self) -> String {
format!("Woof, my name is {} and I am {} years old", self.name, self.age)
}
fn get_associated_value(&self) -> i32 {
self.age
}
}
// This does not create a conflict / overlap
impl Animal<String> for Dog {
fn speak(&self) -> String {
format!("Woof, my name is {} and I am a Dog", self.name)
}
fn get_associated_value(&self) -> String {
self.name.clone()
}
}
What you might be wanting here is instead an associated type:
trait Animal {
type Value; // The associated type declaration.
fn speak(&self) -> String;
fn get_associated_value(&self) -> Self::Value;
}
impl Animal for Dog {
type Value = i32;
fn speak(&self) -> String {
format!("Woof, my name is {} and I am {} years old", self.name, self.age)
}
fn get_associated_value(&self) -> Self::Value {
self.age
}
}
impl Animal for Cat {
type Value = String;
fn speak(&self) -> String {
format!("Meow, my name is {} and my color is {}", self.name, self.color)
}
fn get_associated_value(&self) -> Self::Value {
self.color.clone()
}
}
(playground)
An associated type really means that “the type information is specified in the implementation”, i.e. the implementation chooses the type.
It still doesn’t fully address the trait object issue though. For dyn Animal
even like this, one still needs to actually write dyn Animal<Value = …something…>
.
To hide that information, you need to re-introduce some common interface. In this case, your use-case seems to be that printing the type should be possible, so maybe something like Box<dyn Display>
could work. If you don't want to modify your trait further, you can adapt it with a different trait trait objects of which you'll then use:
use std::fmt;
trait GeneralAnimalWithDisplayValue {
// different name can help avoid ambiguities when used
fn general_speak(&self) -> String;
fn general_get_associated_value(&self) -> Box<dyn fmt::Display>;
}
impl<T> GeneralAnimalWithDisplayValue for T
where
T: Animal,
T::Value: fmt::Display + 'static,
{
fn general_speak(&self) -> String {
self.speak()
}
fn general_get_associated_value(&self) -> Box<dyn fmt::Display> {
Box::new(self.get_associated_value())
}
}
fn main() {
let mut animals: Vec<Box<dyn GeneralAnimalWithDisplayValue>> = Vec::new();
animals.push(Box::new(Dog {
name: "Rex".to_string(),
age: 5,
species: Species::Dog,
}));
animals.push(Box::new(Cat {
name: "Misha".to_string(),
color: "black".to_string(),
species: Species::Cat,
}));
for animal in animals {
println!("{} has an associated value of {}", animal.general_speak(), animal.general_get_associated_value());
}
}
(playground)
Alternatively, one could even make a helper Wrapper
and use the original Animal
trait, but with Value = Box<dyn fmt::Display>
:
use std::fmt;
struct Wrapper<T>(T);
impl<T: Animal> Animal for Wrapper<T>
where
T::Value: fmt::Display + 'static,
{
type Value = Box<dyn fmt::Display>;
fn speak(&self) -> String {
self.0.speak()
}
fn get_associated_value(&self) -> Self::Value {
Box::new(self.0.get_associated_value())
}
}
fn main() {
let mut animals: Vec<Box<dyn Animal<Value = Box<dyn fmt::Display>>>> = Vec::new();
animals.push(Box::new(Wrapper(Dog {
name: "Rex".to_string(),
age: 5,
species: Species::Dog,
})));
animals.push(Box::new(Wrapper(Cat {
name: "Misha".to_string(),
color: "black".to_string(),
species: Species::Cat,
})));
for animal in &animals {
println!("{} has an associated value of {}", animal.speak(), animal.get_associated_value());
}
}
(playground)
Alternatively, going even further into the direction of “specific helper trait for this specific use-case” you can skip the need for boxing into Box<dyn fmt::Display>
and instead put the implementation of the for
loop body into a trait:
use std::fmt;
trait AnimalHelperTrait {
fn main_loop_body(&self);
}
impl<T> AnimalHelperTrait for T
where
T: Animal,
T::Value: fmt::Display,
{
fn main_loop_body(&self) {
println!("{} has an associated value of {}", self.speak(), self.get_associated_value());
}
}
fn main() {
let mut animals: Vec<Box<dyn AnimalHelperTrait>> = Vec::new();
animals.push(Box::new(Dog {
name: "Rex".to_string(),
age: 5,
species: Species::Dog,
}));
animals.push(Box::new(Cat {
name: "Misha".to_string(),
color: "black".to_string(),
species: Species::Cat,
}));
for animal in animals {
animal.main_loop_body();
}
}
(playground)
Also, as an unrelated note, I think your species: Species
field seems rather useless, given the type itself already describes whether it’s a Cat
or Dog
.
Respuesta (Español)
Hay dos problemas al intentar abstraer sobre Animales
como lo estás intentando hacer. Un parámetro de tipo como para Animales<T>
permite que el tipo T
no solo dependa de la implementación, como también notaste, sino que incluso permite múltiples implementaciones con diferentes parámetros. Es decir, puedes crear algo como:
impl Animal<i32> for Perro {
fn hablar(&self) -> String {
format!("Guau, mi nombre es {} y tengo {} años", self.nombre, self.edad)
}
fn obtener_valor_asociado(&self) -> i32 {
self.edad
}
}
// Esto no crea un conflicto / solapamiento
impl Animal<String> for Perro {
fn hablar(&self) -> String {
format!("Guau, mi nombre es {} y soy un Perro", self.nombre)
}
fn obtener_valor_asociado(&self) -> String {
self.nombre.clone()
}
}
Lo que podrías querer aquí es en cambio un tipo asociado:
trait Animal {
type Valor; // La declaración del tipo asociado.
fn hablar(&self) -> String;
fn obtener_valor_asociado(&self) -> Self::Valor;
}
impl Animal for Perro {
type Valor = i32;
fn hablar(&self) -> String {
format!("Guau, mi nombre es {} y tengo {} años", self.nombre, self.edad)
}
fn obtener_valor_asociado(&self) -> Self::Valor {
self.edad
}
}
impl Animal for Gato {
type Valor = String;
fn hablar(&self) -> String {
format!("Miau, mi nombre es {} y mi color es {}", self.nombre, self.color)
}
fn obtener_valor_asociado(&self) -> Self::Valor {
self.color.clone()
}
}
(playground)
Un tipo asociado realmente significa que “en la implementación este la información del tipo”, es decir, la implementación elige el tipo.
Aún así, esto no aborda completamente el problema de los objetos de tipo trait. Para dyn Animal
incluso así, todavía se necesita escribir dyn Animal<Valor = …algo…>
.
Para ocultar esa información, necesitas reintroducir alguna interfaz común. En este caso, tu caso de uso parece ser que sería posible imprimir el tipo, así que tal vez algo como Box<dyn Display>
podría funcionar. Si no quieres modificar tu trait más, puedes adaptarlo con un diferente trait cuyos objetos de trait entonces usarás:
use std::fmt;
trait AnimalGeneralConValorMostrable {
// un nombre diferente puede ayudar a evitar ambigüedades cuando se usa
fn hablar_general(&self) -> String;
fn obtener_valor_asociado_general(&self) -> Box<dyn fmt::Display>;
}
impl<T> AnimalGeneralConValorMostrable for T
where
T: Animal,
T::Valor: fmt::Display + 'static,
{
fn hablar_general(&self) -> String {
self.hablar()
}
fn obtener_valor_asociado_general(&self) -> Box<dyn fmt::Display> {
Box::new(self.obtener_valor_asociado())
}
}
fn main() {
let mut animales: Vec<Box<dyn AnimalGeneralConValorMostrable>> = Vec::new();
animales.push(Box::new(Perro {
nombre: "Rex".to_string(),
edad: 5,
especie: Especie::Perro,
}));
animales.push(Box::new(Gato {
nombre: "Misha".to_string(),
color: "negro".to_string(),
especie: Especie::Gato,
}));
for animal in animales {
println!("{} tiene un valor asociado de {}", animal.hablar_general(), animal.obtener_valor_asociado_general());
}
}
(playground)
Alternativamente, incluso podrías hacer un Wrapper
auxiliar y usar el trait Animal
original, pero con Valor = Box<dyn fmt::Display>
:
use std::fmt;
struct Wrapper<T>(T);
impl<T: Animal> Animal for Wrapper<T>
where
T::Valor: fmt::Display + 'static,
{
type Valor = Box<dyn fmt::Display>;
fn hablar(&self) -> String {
self.0.hablar()
}
fn obtener_valor_asociado(&self) -> Self::Valor {
Box::new(self.0.obtener_valor_asociado())
}
}
fn main() {
let mut animales: Vec<Box<dyn Animal<Valor = Box<dyn fmt::Display>>>> = Vec::new();
animales.push(Box::new(Wrapper(Perro {
nombre: "Rex".to_string(),
edad: 5,
especie: Especie::Perro,
})));
animales.push(Box::new(Wrapper(Gato {
nombre: "Misha".to_string(),
color: "negro".to_string(),
especie: Especie::Gato,
})));
for animal in &animales {
println!("{} tiene un valor asociado de {}", animal.hablar(), animal.obtener_valor_asociado());
}
}
(playground)
Alternativamente, yendo aún más lejos en la dirección de "trait auxiliar específico para este caso de uso específico", puedes omitir la necesidad de encajar en Box<dyn fmt::Display>
e en lugar de ello poner la implementación del cuerpo del bucle for
en un trait:
use std::fmt;
trait TraitAuxiliarAnimal {
fn main_cuerpo_del_bucle(&self);
}
impl<T> TraitAuxiliarAnimal for T
where
T: Animal,
T::Valor: fmt::Display,
{
fn main_cuerpo_del_bucle(&self) {
println!("{} tiene un valor asociado de {}", self.hablar(), self.obtener_valor_asociado());
}
}
fn main() {
let mut animales: Vec<Box<dyn TraitAuxiliarAnimal>> = Vec::new();
animales.push(Box::new(Perro {
nombre: "Rex".to_string(),
edad: 5,
especie: Especie::Perro,
}));
animales.push(Box::new(Gato {
nombre: "Misha".to_string(),
color: "negro".to_string(),
especie: Especie::Gato,
}));
for animal in animales {
animal.main_cuerpo_del_bucle();
}
}
(playground)
Además, como una nota no relacionada, creo que tu campo especie: Especie
parece bastante inútil, dado que el tipo en sí ya describe si es un Gato
o un Perro
.