If your list has to work with any type
You can use the Any trait or better yet another object-safe trait which has the functionality you need from items in the list.
enum Node {
Nil,
Node {
value: Box<dyn std::any::Any>,
next: Box<Node>,
},
}
impl std::fmt::Debug for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Node::Nil => write!(f, "Nil"),
Node::Node { value, next } => {
if let Some(value) = value.downcast_ref::<f64>() {
write!(f, "Node({value}) -> {next:?}")
} else if let Some(value) = value.downcast_ref::<i32>() {
write!(f, "Node({value}) -> {next:?}")
} else if let Some(value) = value.downcast_ref::<&str>() {
write!(f, "Node({value:?}) -> {next:?}")
} else {
write!(f, "Node(<unknown type>) -> {next:?}")
}
}
}
}
}
fn main() {
let list = Node::Node {
value: Box::new(56.7),
next: Box::new(Node::Node {
value: Box::new("hello"),
next: Box::new(Node::Node {
value: Box::new(77),
next: Box::new(Node::Nil),
}),
}),
};
dbg!(list);
}
will output something like [src/main.rs:39] list = Node(56.7) -> Node("hello") -> Node(77) -> Nil to stderr.
Pros
- Works with any object (implementing the trait).
- Every
Node has the size of two Boxes no matter the types you put into the list.
Cons
- Using
downcast* can become very awkward fast, and there isn't really much you can do with a dyn Any otherwise.
- Necessary additional indirection of
Box.
If you know the types that end up in the list
You can use an enum with every possible type:
#[derive(Debug)]
enum DynamicType {
Float(f64),
Int(i32),
Str(&'static str),
}
enum Node {
Nil,
Node {
value: DynamicType,
next: Box<Node>,
},
}
impl std::fmt::Debug for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Node::Nil => write!(f, "Nil"),
Node::Node { value, next } => write!(f, "Node({value:?}) -> {next:?}"),
}
}
}
fn main() {
use DynamicType::*;
let list = Node::Node {
value: Float(56.7),
next: Box::new(Node::Node {
value: Str("hello"),
next: Box::new(Node::Node {
value: Int(77),
next: Box::new(Node::Nil),
}),
}),
};
dbg!(list);
}
Pros
- You can
match and know instantly what type you're dealing with.
- No indirection, the data is stored directly in the
Nodes.
Cons
- If the
DynamicType contains a variety of sizes you might end up wasting a lot of space on Nodes.
- You have to know every type to put in up front.