Share Access to a field in the outer struct

Hi,

I am working on a project using rust and I have a complex data structure like:

struct inner {
    data: String,
    id: i32,
}

struct outer {
    field_1: String,
    field_2: inner,
}

impl outer {
    pub fn init() -> self {
        outer { field_1: "example".to_string(), 
                   field_2: inner {data: field_1, id: 1} 
       }
    }
}

In other words, the field_1 is a shared information among all other fields in the outer. Doing so provides a clear and concise struct definition for outer since we can have fields specific to inner defined separately instead of putting all the stuff in outer.

However, I am aware that this is probably not allowed in rust due to the single ownership requirement. A solution might be to use Rc<> to wrap field_1. Nevertheless, Rc<> only provides a shared reference to field_1, and field_1 can only be modified when there is only one reference in the scope, if my understanding is correct.

Since I may need to modify field_1 with the methods of both outer and inner, it it obvious that Rc<> won't work in this case. An alternative approach might be to use Refcell, but it's not safe.

I am wondering if there is a better design to this kind of struct, or if there is any way that can allow field_1 to be shared between different fields of outer, as they all work on a single thread which makes the accesses to field_1 serialized.

I appreciate any help and clarification.

RefCell is safe.

You are the victim of a fundamental and widespread misunderstanding. Multi-threading is not the only situation when shared mutability is unsafe/incorrect. Shared mutability is basically never memory-safe. So if you want to mutate stuff and observe it at the same time: sorry, that is not possible in Rust.

(So-called "shared mutability" types such as Cell and RefCell are basically tricks to achieve not-quite-shared mutability, either by copying values and not lending out direct shared references, or by applying runtime borrow checking.)

Certainly – don't make it redundant! Simply omit field_1 from Outer, and always access data through Inner.

1 Like

Note that fields beeing public or private in Rust is relevant per module (source file or mod foo { ... } block). So if your inner and outer structs are defined in the same module, you can access self.field_2.data in methods of outer (and mutate it if you have an &mut self) without declaring data as pub in inner.

Thanks for the response! I understand that we can get rid of this problem by accessing data through inner. But if we have this outer definition instead:

struct outer {
    field_1: String,
    field_2: inner,
    field_3: inner,
}

impl outer {
    pub fn init() -> self {
        outer { field_1: "example".to_string(), 
                   field_2: inner {data: field_1, id: 1},
                   field_3: inner {data: field_1, id: 2},
       }
    }
}

then we will still suffer from the problem since now both field_2 and field_3 owns references to the same memory, right?

Also, may I get a clarification on the RefCell is safe argument?
Does it mean I can have something like below safely or it just means RefCell itself is safe?

struct outer {
    field_1: Rc<RefCell<String>>,
    field_2: inner,
    field_3: inner,
}
let x = outer::init(); 
x.field_1.borrow_mut().modify("test".to_string());

I don't understand this. Those two fields aren't references. The init() function doesn't compile as-is; there is no field_1 variable declared. But if field_1 was a variable, and it was indeed a String, the function would still not compile, because its first use would move it into field_2 and field_3 couldn't use it, unless it was cloned.

I don't understand what you are asking here, either. "RefCell is safe" means you can use it without unsafe code, and it's sound (i.e., memory-safe). The equivalent of the last two lines with borrow_mut() should compile and run as expected (except that there's no modify() method on String).

Sorry I didn't states my question clearly. For the first one the code should be something like:

impl outer {
    pub fn init() -> self {
        let test = "example".to_string();
        outer { field_1: test.clone(), 
                   field_2: inner {data: test.clone(), id: 1},
                   field_3: inner {data: test.clone(), id: 2},
       }
    }
}

and I realize that in this case all three fields initialize with test.clone() may access different memory address, if understand clone() correctly. And we still can have a single shared field_1 among outer itself, field_2.data and field_3.data.

For the second question, I just tried another example program:

struct A {
    pub data: Rc<RefCell<String>>,
}

impl A {
    pub fn change(&mut self) {
        *self.data.borrow_mut() = "test".to_string();
    }
}
struct B {
    field_1: Rc<RefCell<String>>,
    pub data: A,
}

impl B {
    pub fn init() -> B {
        let data = Rc::new(RefCell::new("field_1".to_string()));
        B {
            field_1: data.clone(),
            data: A{data: data.clone()},
        }
    }
}

let mut test = B::init();
    test.data.change();
    println!("{:?}, {:?}", test.data.data, test.field_1);
    *test.field_1.borrow_mut() = "hello".to_string();
    println!("{:?}, {:?}", test.data.data, test.field_1);

This code is compiled and work as expected: the data modified in either way can be propagated to both A and B. And I wondered if this code safe in Rust or not since I remember I found another document saying that this way might not be safe.

If there's no unsafe keyword, then its safe.

2 Likes

You might still be misunderstanding what "safe" means. In Rust, it can be one of two things:

  • The strictest interpretation is that "it doesn't contain unsafe" or "it doesn't need to use unsafe". This automatically means that it is also sound or memory-safe (modulo library/compiler bugs), since Rust wants to guarantee that unless you use unsafe, it is impossible to cause memory corruption, data races, or other kinds of undefined behavior in general.
  • The colloquial, broader interpretation is synonymous with "sound", i.e. "memory-safe". However, I don't like that interpretation, since it is possible to write sound unsafe code, which manages and accesses memory correctly, but it is still not "safe", because the correctness guarantees can't be verified by the compiler.

I see! Thank you so much!