Comparison of these techniques

The Rust community tends to prefer using enumerations to address one-variable-multiple-types problems. In terms of runtime cost, a simple enumeration is maximally efficient, and efficiency is important to Rust programmers.

However, there is a downside to using enumerations, which is that the match expressions (or similar) that decide how to handle a particular enumeration value and associated data might be spread throughout the source code of the program. If we discover a need to add or remove an enumeration value, or change an enumeration value's parameters, we have to find and change every one of those match expressions.

If we decide to add a Reverse value to the Drive enumeration, the match expressions have to be changed:

The compiler will point out each match expression that needs to be updated, but it won't catch places where an if let expression would need similar changes (because if let is allowed to handle only some of the possibilities), so this can be a significant problem.

Trait objects on the other hand let us keep all the related code close together, by making the behavior for different data types actually be part of the data type. They also allow us to write code that works with data types we haven't even created yet, but they are less efficient, because the computer needs to maintain and use the hidden trait object structure while the program is running.

We might think we could get the best of both worlds by creating an enumeration and then implementing functions on it that contain match expressions and handle each of the enumeration values differently, and to an extent, we can. However, if those functions work by picking another enumeration value-specific function and calling that, we've just recreated trait objects all over again, but less efficiently.

If we try something like the following to avoid trait objects, we're better off just using trait objects:

pub enum LikeATraitObject {
Integer(i32),
Float(f32),
Bool(bool),
}

fn handle_integer(x: i32) {
println!("Integer {}", x);
}

fn handle_float(x: f32) {
println!("Float {}", x);
}

fn handle_bool(x: bool) {
println!("Bool {}", x);
}

impl LikeATraitObject {
pub fn handle(&self) {
match self {
LikeATraitObject::Integer(x) => { handle_integer(*x); }
LikeATraitObject::Float(x) => { handle_float(*x); }
LikeATraitObject::Bool(x) => { handle_bool(*x); }
}
}
}

That's not to say that such constructions are useless, because they're not. However, if the only reason for doing something like that is to avoid using trait object references, it's a mistake.

It may seem like Any is actually the best option, since it can store such a wide range of values and gives us full access to the stored value data, but usually one of the other choices is better. Using Any means we need to check for all of the possibilities in various places throughout the code, as we would with an enumeration and, unlike an enumeration, the compiler can't give us any help at all in finding places that need to change because, like a trait object reference, there's no defined list of possibilities. Any is, in many ways, the worst of both worlds. 

There are some problems that Any is the right choice for, though. If we really need to handle a collection of unrelated data types, we need Any.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.168.214