Handing Different Types in a Collection

I just finished chapter 17.3 of the Rust book, discussing how to implement the state pattern in Rust (using trait objects), and what a more native Rust implementation would look like (using different types).

In the example given for the more Rust-like solution, you're writing software to run a blog, and you have different structs to represent the different possible states of the posts (a draft post struct, an in-review post struct, etc) all with their own implementation.

This all made sense, except for one problem.

In an OOP language like C#, the way you'd store your bag of different types of posts is you'd create a List<PostBaseType> and you could iterate through different types of posts, filtering and performing different operations based on the post type (via downcasting).

The example in the Rust book doesn't seem to cover this - if we split posts into different disparate types, how do we bind them together/create a collection (so we can have a vec of all post-like items)? And once we have that collection, how do we then get back the original types to perform operations against the types (for example, if we want to only see 'pending review' posts, we then will want to have the option to approve one - how do we go from whatever is in the vector to the appropriate concrete type)?

My gut response is to use an enum, but the chapter (when covering the OOP-like solution using trait objects) included a comment that said that enums may be more verbose than using trait objects, so I am wondering if there's a more elegant way?

Or to put it another way, what is the Rust-way to handle a bag of different related types, and to get back the thing you need after you add them into a collection?

I would consider enums and trait objects mutually exclusive since traits specify shared functionality while Enums kinda like mean this or that.

1 Like

An enum would indeed be a typical way when you know the set of implementing types. Matching an enum isn't really worse ergonomically than attempting a downcast (to a particular type) or having a chain of attempted downcasts (to handle all possible types), and affords more compile-time checks ("did I forget a type?"). Using an enum can involve more boilerplate when it comes to forwarding implementations of traits to each variant. There are crates aimed at reducing the boilerplate.

You may also utilize other patterns, like perhaps methods on your collection that return iterators over the pending posts only.

An alternative is to have a collection that stores each type separately.

3 Likes

I believe that answers my question, thank you!

One follow up - you mention "when you know the set of implementing types". I noticed Box has a downcast operation. In cases where I didn't have a set of known concrete types, would that be the correct Rust-way to go about it?

Sometimes. The mechanisms that the language/std provides for emulated downcasting are trait Any and struct TypeId. Those are only available for types that meet a 'static bound; in particular Any itself has a 'static bound. So one of the downsides is that you need to either have Any as a supertrait,[1] or to use TypeId and some unsafe to supply your own downcasting for 'static types.[2]

The 'static bound may not be as onerous as it sounds for a very specific trait,[3] if you're using Box<dyn Trait> in your fields anyway -- because that's really a Box<dyn Trait + 'static>. But I still feel it's more common to just avoid the need to downcast. Especially if your trait is public and/or serves a purpose beyond "I'm going to store this in a lifetime-less struct". For example, functions and the like that accept a T: Trait often don't care if the T is 'static or not, so why impose that bound globally. Perhaps it's also just less common because unlike many OO-centric languages, this isn't real up/downcasting -- there is no actual trait supertype that encompasses all implementors as subtypes, there is no "everything is a class" paradigm, etc.

A way to avoid the need to downcast while still using dyn Trait is to encapsulate all the functionality/information you need within the capabilities of the trait. For instance, let's say the book example wanted to add the ability to externally query what the current state is. This could be achieved via the Any trait, but it could also be accomplished by adding a method that returns a fieldless enum representing the state. The latter approach would also afford consumers the benefit of being able to use exhaustive matching and so on.


  1. and apply some manual upcasting for now, but that will be a stable coercion some day ↩︎

  2. without necessarily imposing a 'static bound on implementers of the trait ↩︎

  3. like the example in the book, or any crate-private trait I suppose ↩︎

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.