hyunsu blog

홈강좌Rust 입문·중급 47편 중 39편째

Rust Rc 와 RefCell — 주인 여럿이 한 값을 함께 갖고 고친다

목차 · 4
  1. 01Rc — 주인이 여럿
  2. 02Rc 안은 고칠 수 없다
  3. 03RefCell — 실행 중에 빌림을 따진다
  4. 04실습 — 규칙을 어기면 패닉

Rust 의 Rc<T> 는 주인이 여럿인 값을 만든다. Rc::clone 할 때마다 주인 수가 늘고, 마지막 주인이 사라질 때 값이 치워진다. Rc 안의 값은 고칠 수 없어 E0596 이 나는데, RefCell 로 감싸면 borrow_mut 로 고칠 수 있다. 대신 빌림 규칙을 실행 중에 따져서, 어기면 패닉한다.

01 Rc — 주인이 여럿

15편의 규칙은 "주인은 하나" 였다. 그런데 공지 하나를 여러 팀이 함께 들고 있어야 하고, 누가 마지막까지 쓸지 미리 알 수 없다면 어떻게 할까.

rust
use std::rc::Rc;

fn main() {
    let notice = Rc::new(String::from("server check at 3am"));
    println!("owners: {}", Rc::strong_count(&notice));

    let team_a = Rc::clone(&notice);
    let team_b = Rc::clone(&notice);
    println!("owners: {}", Rc::strong_count(&notice));
    println!("{team_a} / {team_b}");

    drop(team_a);
    println!("owners: {}", Rc::strong_count(&notice));
}
text
   Compiling shared v0.1.0 (~/projects/shared)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/shared`
owners: 1
owners: 3
server check at 3am / server check at 3am
owners: 2
  • Rc::new(값) 으로 만든다. Rc 는 Reference Counted(주인 수를 센다)의 줄임이다. use std::rc::Rc; 로 불러온다.
  • Rc::clone(&notice) 는 문자열을 복사하지 않는다. 같은 값을 가리키는 주인을 하나 늘리고 수만 센다. 16편의 .clone() 과 이름은 같지만 하는 일이 다르다는 것을 드러내려고 Rc::clone 으로 쓰는 것이 관례다.
  • Rc::strong_count 가 지금 주인 수다. 1 → 3 → 2 로 바뀌었다.
  • drop(team_a) 는 그 주인을 지금 치운다. 블록이 끝날 때 저절로 일어나는 일을 앞당긴 것이다. 주인 수가 0 이 되는 순간 문자열이 치워진다.

02 Rc 안은 고칠 수 없다

주인이 여럿인 목록에 항목을 더해 보자.

rust
use std::rc::Rc;

fn main() {
    let list = Rc::new(Vec::new());
    let other = Rc::clone(&list);
    list.push("first");
    println!("{other:?}");
}
text
error[E0596]: cannot borrow data in an `Rc` as mutable
 --> src/main.rs:6:5
  |
6 |     list.push("first");
  |     ^^^^ cannot borrow as mutable
  |
  = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Rc<Vec<&str>>`
  1. 무엇 — Rc 안의 데이터를 고칠 수 있게 빌릴 수 없다.
  2. 어디 — 6번째 줄 list.push.
  3. 고치는 법 — help: 는 기술적인 이유(DerefMut 트레이트가 없다)만 적었다.

주인이 여럿인데 아무나 고칠 수 있으면, 19편에서 막았던 "읽는 중에 다른 쪽이 고치기" 가 일어난다. 그래서 Rc 는 읽기만 허락한다.

03 RefCell — 실행 중에 빌림을 따진다

RefCell 로 감싸면 Rc 안의 값도 고칠 수 있다.

rust
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let list = Rc::new(RefCell::new(Vec::new()));
    let other = Rc::clone(&list);

    list.borrow_mut().push("first");
    other.borrow_mut().push("second");

    println!("{:?}", list.borrow());
}
text
   Compiling shared v0.1.0 (~/projects/shared)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/shared`
["first", "second"]
  • Rc<RefCell<Vec<…>>> 는 "주인이 여럿이고, 안을 고칠 수 있는 Vec" 이다.
  • borrow_mut() 는 고칠 수 있게 빌리고, borrow() 는 읽기로 빌린다. 19편의 &mut·& 와 같다.
  • list 와 other 둘 다 같은 Vec 에 더했고, 결과는 하나의 목록이다.

19편의 빌림 규칙(가변은 하나, 읽기와 섞지 않기)이 없어진 것이 아니다. 컴파일할 때 따지던 것을 실행 중에 따진다.

04 실습 — 규칙을 어기면 패닉

같은 RefCell 을 동시에 두 번 borrow_mut 해 보자.

rust
use std::cell::RefCell;

fn main() {
    let list = RefCell::new(vec![1, 2]);
    let first = list.borrow_mut();
    let second = list.borrow_mut();
    println!("{first:?} {second:?}");
}
text
   Compiling shared v0.1.0 (~/projects/shared)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/shared`

thread 'main' (213147191) panicked at src/main.rs:6:23:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

컴파일은 됐지만 6번째 줄에서 멈췄다. 이미 빌려 간 상태(already borrowed)라는 뜻이다. 19편이었다면 E0499 로 컴파일이 막혔을 코드다. RefCell 은 컴파일러가 증명하지 못하는 경우에 "실행 중에 내가 지키겠다" 고 미루는 도구라서, 지키지 못하면 이렇게 실행 중에 멈춘다. first 를 다 쓴 뒤에 second 를 빌리면 문제없다.

Rc 는 한 스레드 안에서만 쓴다. 여러 스레드에서 주인을 나누는 것은 45편의 Arc 가 한다. 다음 편은 코드를 모듈로 나누는 법이다.