Moving multiple fields out of an opaque struct

Let's say I have some struct with private fields:

pub struct MyStruct {
    foo: Foo,
    bar: Bar,
}

impl MyStruct {
    pub fn into_foo(self) -> Foo {
        self.foo
    }
    
    pub fn foo(&self) -> &Foo {
        &self.foo
    }

    pub fn into_bar(self) -> Bar {
        self.bar
    }
    
    pub fn bar(&self) -> &Bar {
        &self.bar
    }
}

I can move or borrow fields from the struct, and that's all fine. But what if I want to move multiple fields?

fn do_stuff(my_struct: MyStruct) {
    let foo = my_struct.into_foo(); // now the whole struct is moved
    do_stuff_with_foo(foo);
    let bar = my_struct.into_bar(); // so this is a compiler error
    do_stuff_with_bar(bar);
}

I can clone, which is often suboptimal (and my fields might not implement Clone anyway!), or I can do something like this:

impl MyStruct {
    pub fn consume(self) -> (Foo, Bar) {
        (self.foo, self.bar)
    }
}

This works, but it's got some issues - if several of my fields have the same type it gets confusing, and if this is public API then adding a new field can be a breaking change.

Was wondering if anyone knows any better patterns (or even crates!) that help with this.

Thanks!

Is there any particular reason the fields are private? If you want them to be effectively public, just make them public.

If there is a good reason for privacy (e.g. the type upholds an invariant which can't be enforced otherwise), then mirror the fields to be extracted in a separate struct with public, named fields, and return that from your consume() function. (Which would probably be called into_inner() or something like that, idiomatically.)

6 Likes

Yeah, in a lot of situations public fields will do, but I was mostly thinking about the case where there's an invariant to be upheld.

I like your suggestion, it can even be public API without the risk of breaking changes on future field additions if you just make the helper struct non_exhaustive. It's a bit of boilerplate, but nothing catastrophic (and this isn't necessarily a super common use case).

Thanks!

To add to @H2CO3's suggestion of into_inner(), such a consume() function is often called into_parts() or into_raw_parts(), e.g.:

(Interestingly, the http and hyper examples all have custom Parts structs with private fields.)

1 Like

isn't it generally a good practice and good future-proofing to force consumers to get fields via methods though? that way later on you are free to change the private field names, and/or switch to computing the value on the fly when the field is requested, and all without breaking the outward api?

No, in practice, it doesn't help much. In particular, if you write accessors (both getters and setters) for a field, then:

  • You provide the exact same capabilities (reading and writing) as you would if you simply made the field public.
  • It is no better protection against breakage. If you need to rename a field, that is because its purpose changed or because you found a better name for it. In either case, you would need to change the accessor function names as well – it would be very confusing not to do so, in the first case because the accessor would have a name that actively misleads the user, and in the second case simply because the names don't match up.

The good reasons for keeping fields private and writing read-only accessors are:

  • when your field must be private (for eg. one of the reasons mentioned above)
  • or when you want to ensure that it is immutable.

Writing read-write accessors IMO is extremely niche and hard to warrant due to the way idiomatic Rust design works. In particular, Rust code doesn't usually follow the "object-oriented" encapsulation pattern.

It is highly discouraged in Rust to conflate "active", stateful, complex "objects" with pure data. Complex objects usually have all-private fields, and their methods are much more useful and richer than mere getters. They usually don't expose any setters so as to ensure internal invariants. Meanwhile, pure data values don't need any sort of encapsulation, and have all public fields.

Instead of mixing the two, it is a much cleaner approach to ensure that invalid states are unrepresentable. If you type your pure data values rigorously, you can enforce that whatever you get out or put in your system using an arbitrary value that the span of all fields can store will be logically consistent with your domain logic.

3 Likes

In some languages it can sometimes help, but note that if you're trying to return a reference -- with is particularly common in Rust -- you usually can't change to computing it on the fly later because you have to return a reference to something.

1 Like

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.