1

I know of the visibility rules in Rust and the typical 'constructor' patterns in Rust, e.g. via associated functions, commonly named as new or if all fields of the struct are public, via 'direct' construction, e.g. Struct {value: 0}.

Just wish to get a sense of what is the appropriate idiom to achieve the following:

Exposing a public struct with all public fields but restricting direct construction.

e.g.

pub struct MyStruct {
    pub val: u8,
}

impl MyStruct {
    pub fn new(val: u8) {
        MyStruct {val}
    }
}

Ok for external crate to do this:

use your_crate::MyStruct;

let your_struct = MyStruct::new(8);
println!("{}", your_struct.val);
//8

But not Ok for external crate (call it 'your_crate' vs. 'my_crate' in which MyStruct is defined) to do this:

use my_crate::MyStruct;

let your_struct = MyStruct {val: 8};
// above should return compile error

I am aware that there are two methods to achieve this.

  1. Using getter and setter methods and make fields private.
pub struct MyStruct {
    val: u8,
}

impl MyStruct {
    pub fn new(val: u8) {
        MyStruct {val}
    }

    pub fn get_val(&self) -> &u8 {
        self.val
    }
    
    pub fn set_val(&mut self, new_val: u8) {
        *self.val = new_val;
    }
}

This provides access to the fields indirectly via method calls instead of attribute access.

  1. Add a 'private' inconsequential field.
pub struct MyStruct {
    pub val: u8,
    _private: (),
}

This prevents direct construction but allows for direct attribute access and removes the need for implementing superfluous 'get' and 'set' methods.

My question is:

Which is the more appropriate idiom accepted by many in the community? Or what considerations should a library writer think through before deciding which idiom is more appropriate?

3
  • 1
    A key piece of what's missing in your question is "why". Why is it acceptable for all internal state to be exposed but yet mandatory that your "constructor" be called? Does the constructor perform some side effect?
    – eggyal
    Commented Mar 29, 2023 at 9:43
  • Thanks for your response, I think you have a point, I wish to preserve the invariant of the struct and simultaneously provide ease of access to end users; these two are probably difficult to achieve at the same time.
    – Jim
    Commented Mar 29, 2023 at 16:59
  • Indeed, then making the fields pub opens up the possibility that a user will violate an invariant after construction. Perhaps consider the readonly crate?
    – eggyal
    Commented Mar 30, 2023 at 7:37

2 Answers 2

9

You can use a private field trick, but the normal way to do this would probably be the #[non_exhaustive] annotation, which works with structs as well as enums.

Non-exhaustive types cannot be constructed outside of the defining crate:

  • Non-exhaustive variants (struct or enum variant) cannot be constructed with a StructExpression (including with functional update syntax).
4

Using getters is the most common. However, they're usually field and field_mut:

fn val(&self) -> &u8 {
    &self.val
}

fn val_mut(&mut self) -> &mut u8 {
    &mut self.val
}

A setter method is useful if you want to (leave open the option to) process incoming values.

Your second method isn't used much since it's functionally a downgrade from a fully public struct. If someone wanted to make one with arbitrary values, they could just construct one via a constructor and then reassign all the fields.

There is one common case where this is useful: reserving the ability to add fields without breaking your API. There is a built-in attribute for that: #[non_exhaustive].

#[non_exhaustive]
pub struct MyStruct {
    pub val: u8,
}

This is used more on enums, but on structs it prevents code outside the module from constructing the struct with the braces syntax, like MyStruct { val: 0 }.

2
  • Thank you for your reply. Using #[non_exhaustive] has a tradeoff which is downstream crates would not be able to destructure the struct exhaustively right?
    – Jim
    Commented Mar 29, 2023 at 6:07
  • 2
    They would not be able to destructure the struct exhaustively either way, but all that means is they have to use a wildcard for the "leftover" fields, exactly as with a mix of private and public fields.
    – Masklinn
    Commented Mar 29, 2023 at 6:25

Not the answer you're looking for? Browse other questions tagged or ask your own question.