How to store different types in one Vec
Short answer: Vec<T> holds exactly one type, so the way to store different types in it is to make
them one type: an enum with a variant per kind, each variant carrying that kind’s data. Reading a value
back out is a match.
Each block below is a complete src/main.rs; the ones that print run with cargo run, the failing one
with cargo build. Rust 1.99.0, edition 2024. The project name doesn’t matter — nothing printed depends
on it.
The error you came for
Section titled “The error you came for”Push two different types into one Vec and the compiler stops at the second push:
fn main() { let mut items = Vec::new(); items.push(1); items.push("hello");}error[E0308]: mismatched types --> src/main.rs:4:16 | 3 | items.push(1); | ----- - this argument has type `{integer}`... | | | ... which causes `items` to have type `Vec<{integer}>` 4 | items.push("hello"); | ---- ^^^^^^^ expected integer, found `&str` | | | arguments to this method are incorrect |note: method defined here --> /Users/rango/.rustup/toolchains/1.99.0-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:995:12 |995 | pub fn push(&mut self, value: T) { | ^^^^
For more information about this error, try `rustc --explain E0308`.The first push pins the whole Vec to Vec<{integer}>. Every element of a Vec has the same type,
and by the time the string arrives the compiler has already committed to that one.
The fix: one enum, data in each variant
Section titled “The fix: one enum, data in each variant”An enum is one type with several variants, and each variant can carry its own data:
enum Item { Number(i32), Text(String),}
fn main() { let mut items = Vec::new(); items.push(Item::Number(3)); items.push(Item::Text("hi".to_string()));
for item in &items { match item { Item::Number(n) => println!("number {n}"), Item::Text(s) => println!("text {s}"), } }}number 3text hiNow the element type is Item, and the first push sets it the same way it does for any inferred
Vec — no annotation needed. Each value comes back out through a match, and that is the point as much
as the cost: the compiler checks the match covers every variant, so adding a variant later flags every
place that reads one.
The same idea with named fields
Section titled “The same idea with named fields”Variants can hold more than one value, with names:
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 },}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, }}
fn main() { let shapes = vec![ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.0 }, ];
for shape in &shapes { println!("area: {}", area(shape)); }}area: 12.566370614359172area: 12area takes a reference and match borrows the fields out of it, so the Vec stays usable after the
call. That is the shape of code the enum buys you: one function per behaviour, one arm per variant.
Where the enum stops
Section titled “Where the enum stops”An enum is a closed set: you list the variants, and every match has to say what happens for each
one. When the set of types isn’t known where the code is written — types that arrive from a plugin or a
config file — the same Vec wants a Box<dyn Trait> instead, which moves the per-type behaviour into
impls instead of arms. That is a different trade-off and belongs to its own page.
Related
Section titled “Related”- How to tell the compiler the type of an empty Vec —
the inference that fills in the element type when you write
Vec::new() - Which collection should you use? — the decision that comes before the element type
Next in this chapter: how to use itertools for iterator helpers.