Ask for dynamic dispatch in Rust way


#1

I want to write a card game, every card in this game is a Rust struct Holder, Holder can hold many Features(Feature is a trait), and I think my left main work is write many Structs to impl the trait Feature.
Since card is flexable, so I assemble features for each Holder in program run time.
I write a simple codes to illustrate it:

fn main() {
	let mut holder = Holder { features : vec![] };
	let red = Box::new(RedFeature { red_value : 0});
	let blue = Box::new(BlueFeature { blue_value1 : "".to_string(), blue_value2 : 1});

	holder.features.push(red);
	holder.features.push(blue);

	//here I want to find RedFeature from a holder, and use its value and its methods
}

struct Holder {
	features : Vec<Box<Feature>>
}

trait Feature {

}

struct RedFeature {
	red_value : i32
}

impl Feature for RedFeature {

}

struct BlueFeature {
	blue_value1 : String,
	blue_value2 : i32
}

impl Feature for BlueFeature {

}

This issue is, when I want to use these card(Holder), I must cast to its type in order to use its unique field and methods, that sounds not a Rust way, please help me.


#2

NOTE: Can someone proof read this? I’m still kinda new…

The following function will use dynamic dispatch:

fn do_something_with_feature(feature: Box<Feature>){
    feature.use_feature_trait_method();
}

Does that answer your question? You can only use functions defined in trait Feature since we can’t tell what type it is once it’s contained in Holder. The only thing we know is that it’s a Feature. In addition, this won’t work:

fn do_something_with_feature(feature: T) where T : Feature{
    feature.use_feature_trait_method();
}

Since Features pulled out of Holder don’t have a type known at compile time, and generics are statically bound.

------oops. Didn’t read your question properly.

It’s probably not a great idea to store something in a Box<Feature> when you still need to know its true type at some point. Putting a struct in a trait object erases your knowledge of the true type. But assuming you’re ok with doing so…this might work:

trait Feature{
    fn get_red(&self) ->  Option<&RedFeature>;
}
impl Feature for RedFeature{
    fn get_red(&self) -> Option<&RedFeature>{
          Some(&self)
    }
}
impl Feature for BlueFeature{
    fn get_red(&self) -> Option<&RedFeature>{
          None
    }
}

Then you can iterate through the Holder's Features and get a red item (by matching against Some/None).

Again, I want to stress that you should only put something in a collection of Box<Feature>s if you’re absolutely sure they can all be treated the same way.


#3

Thanks for reply.

Probably, I should have many feature structs, and follow your idea seems verbose.


#4

You can use an enum, assuming the set of features is known upfront. Each variant in the enum can have different structure and you can match on them. The alternative is full dynamic dispatch via a trait method, as @burns47 mentioned.


#5

@vitalyd makes a great point. I’m probably going to make a follow up question here in a little bit exploring the different use cases for enums vs traits. I’ve had to choose between the two in many cases and I never feel educated while doing so…lol

@luyang177 I’d follow @vitalyd’s advice if (like he said) the set of Features is known up front. Then you can write like this:

 enum Feature{
      Red{red_value : i32},
      Blue{blue_value1 : String, blue_value2 : i32}
 }    

struct Holder{
    features : Vec<Feature> //Box is no longer needed since Feature is now a type
}

fn do_something_with_red_features(holder: Holder){
    for feature in holder.features.iter(){
        if let Feature::Red{..} = *feature{
        }
    }
}

#6

That sounds great.
Only drawback is I have to write all features in a single enum, also means in single rs file, that not I expect


#7

Yes, the enum has to be in one place, but definitions of features’ internals can be elsewhere:

use red::RedFeatureStruct; // separate modules
use blue::BlueFeatureStruct;

enum Feature {
    RedVariant(RedFeatureStruct),
    BlueVariant(BlueFeatureStruct),
};

#8

You can also have it implemented in a more C or Java way by making your own way to identify the type and cast it, but of course the casts bypass safety of the type system.

edit: as DanielKeep points out, you can use Any for this.


#9

Don’t use transmute for this. Just use Any to do downcasting.


#10

So Feature is also allocate in the heap, cause it is in Vec?


#11

Vec’s storage is on the heap but the Features are embedded inline in that storage and are contiguous. A Vec<Box> would be a Vec storing pointers to Features, with each Feature being somewhere on the heap as well.


#12

Thanks, that’s clear.


#14

@vitalyd @burns47 @kornel
I follow your advice, and write codes below:

pub enum Feature {
	HealthPointType(HealthPoint),
	AttackPointType(AttackPoint)
}

fn get_single_feature(features : &mut Vec<Feature>) -> Option<&mut HealthPoint> {
	let mut result = None;
	let mut count = 0;
	for i in features.iter_mut() {
		match *i {
			Feature::HealthPointType(ref mut v) => {
				result = Some(v);
				count += 1;
				if count > 1 {
					panic!("more than one");
				}
			},
			_ => {}
		}
	}

	if count == 1 {
		result
	} else {
		None
	}
}

It can work correctly, but when I want fetch a specific Feature(like HealthPoint) from Vec, I have to write a function like get_single_feature, so I have many Features, I don’t think that’s a good idea to write many functions like get_single_feature, that’s not DRY.
I attampt to write a generic function, but failed at the line

Feature::HealthPointType(ref mut v) => {

Please help me.


#15

I don’t think a generic version will be possible because the enum variants aren’t their own type, they’re members/values of the enum. You could maybe write a macro that generates a function per variant so you don’t need to repeat the same boilerplate for each variant.


#16
macro_rules! get_single_feature {
	($feature_name : ident, $features : ident) => {

		fn get_single_feature($features : &mut Vec<Feature>) -> Option<&mut $feature_name> {
			let mut result = None;
			let mut count = 0;
			for i in features.iter_mut() {
				match *i {
					Feature::$feature_nameType(ref mut v) => {
						result = Some(v);
						count += 1;
						if count > 1 {
							panic!("more than one");
						}
					},
					_ => {}
				}
			}

			if count == 1 {
				result
			} else {
				None
			}
		}

	}
}

And compiler said: expected expression, found keyword fn


#17

I don’t see a problem with fn, but when I try to compile that code it fails on:

That’s because it reads it as $feature_nameType variable, not $feature_name + Type. You’ll probably need to pass that name separately to the macro as an input.


#18
struct Red;
struct Green;

// Any trait allows dynamic type checks
use std::any::Any;

// Every Feature also implements Any trait
trait Feature : Any {}

impl Feature for Red {}
impl Feature for Green {}


trait FeatureFinder {
    // Type searched by this function will depend on what type is required
    fn find<T: Feature>(&self) -> Option<&T>;
}

// Implement searching on a Vec
// it works on Box<Feature>, because we want Vec to be storing pointers to variable-sized objects,
// rather than objects inline.
impl FeatureFinder for Vec<Box<Any>> {
    fn find<T: Feature>(&self) -> Option<&T> {
        // filter_map takes every element, and when it returns None, the item is skipped
        // downcast_ref returns None if type isn't T
        // next() gets the first matching element (or None if nothing matches)
        self.iter().filter_map(|f| f.downcast_ref()).next()
    }
}

fn main() {
    let features: Vec<Box<Any>> = vec![Box::new(Red), Box::new(Green)];

    // Note that find() magically works! It picks the type from the variable.
    let red: &Red = features.find().expect("red should be in there");
    let green: &Green = features.find().expect("green should be in there, too");
}

BTW: you’ll probably want to write find_mut() that uses iter_mut() to be able to modify features found. Or use Rc<Feature> if you’re going to be using them a lot outside of the vec.

edit: I’ve corrected the code. Features need to be stored as Box<Any>, not Box<Feature> . I’m not sure why.

It’s also possible to use HashMap to enforce that there are no duplicate items (and the search is marginally faster for many features): https://gist.github.com/anonymous/7f3ee0af8cb407dddcc973d3ede5f2a1


A key/value data structure keyed by Trait?