Vincent P
Vincent P

Reputation: 699

Storing trait objects in structs

I'm working on a parser that has a bunch of different nodes (~6 node kinds right now, more later) and I'm a bit lost as how to interact with the node items (all nodes implement a Node trait) so I need to use Box<Node> everywhere.

ListNode looks like:

pub struct ListNode {
    kind: NodeKind,
    position: usize,
    pub nodes: Vec<Box<Node>>
}

but I can't derive Clone because Node doesn't implement it and whenever I try to get for example the kind of node in a test like so:

#[test]
fn test_plain_string() {
    let mut parser = Parser::new("plain_string", "Hello world");
    parser.parse();
    assert_eq!(1, parser.root.nodes.len());
    let ref node = parser.root.nodes[0];
    let kind = node.get_kind();
    assert_eq!(kind, NodeKind::Text);
}

I get borrowing and sizing errors like so for example:

src/parser.rs:186:20: 186:24 error: cannot move out of borrowed content [E0507]
src/parser.rs:186         let kind = node.get_kind();
                                     ^~~~
src/parser.rs:186:20: 186:24 error: cannot move a value of type nodes::Node + 'static: the size of nodes::Node + 'static cannot be statically determined [E0161]
src/parser.rs:186         let kind = node.get_kind();

Something similar to this test is in the tests.

How am I supposed to access trait objects or this approach flawed in Rust? Is it possible to implement traits to a trait (like Debug) to a trait or do I have implement Debug manually for each struct embedding a Node?

Upvotes: 3

Views: 2576

Answers (1)

Francis Gagn&#233;
Francis Gagn&#233;

Reputation: 65657

The error comes from the fact that the get_kind method on Node expects self by value, rather than &self. Passing self by value means that the method takes ownership of the object (and therefore drops it and the end of the method), which is not necessary here. In general, you should use &self by default, then change to &mut self if you need to mutate the object, or self if you need to consume the object (e.g. because you need to move one of the object's fields elsewhere and you don't want to clone it).

By the way, I noticed that you implemented ToString for your structs. However, the documentation for ToString says:

This trait is automatically implemented for any type which implements the Display trait. As such, ToString shouldn't be implemented directly: Display should be implemented instead, and you get the ToString implementation for free.


Instead of using a trait, you could also consider using an enum, especially if the types of nodes are known in advance and you don't need extensibility. Also, if later on you need to determine the type of a node, it'll be more natural to do with an enum (just do pattern matching).

It appears that you need all of your node types to have a kind and a position attribute. Depending on how you're going to use these nodes, you might find it more useful to define a struct with these fields plus an enum for the type-specific fields.

struct Node {
    //kind: NodeKind, // redundant!
    position: usize,
    specific: SpecificNode,
}

enum SpecificNode {
    List(Vec<Box<Node>>),
    Text(String),
    VariableBlock(Box<Node>),
    Identifier(String),
    Int(i32),
    Float(f32),
    Bool(bool),
}

Hmm, the SpecificNode enum looks a lot like your NodeKind enum, doesn't it? In fact, the kind field becomes redundant, because you can determine the kind by looking at the variant of the specific field.

Now, instead of implementing methods separately on each type of node, you only define them once, though each method will usually need to pattern match self in order to access the data of any variant.

If you have methods that apply only to one particular variant (e.g. a method that only makes sense on a List), with the enum approach, you'd be able to invoke it on any type of node, as there are no distinct types for each enum variant (as of Rust 1.6; there are discussions about introducing this feature). However, your code is likely to work on an abstract Node most of the time, and you'd have to verify what kind of node you have before calling the method anyway, so you might as well move that logic directly into those methods.

Upvotes: 3

Related Questions