홈강좌Rust 입문·중급 47편 중 39편째
Rust Rc 와 RefCell — 주인 여럿이 한 값을 함께 갖고 고친다
Rust 의 Rc<T> 는 주인이 여럿인 값을 만든다. Rc::clone 할 때마다 주인 수가 늘고, 마지막 주인이 사라질 때 값이 치워진다. Rc 안의 값은 고칠 수 없어 E0596 이 나는데, RefCell 로 감싸면 borrow_mut 로 고칠 수 있다. 대신 빌림 규칙을 실행 중에 따져서, 어기면 패닉한다.
01 Rc — 주인이 여럿
15편의 규칙은 "주인은 하나" 였다. 그런데 공지 하나를 여러 팀이 함께 들고 있어야 하고, 누가 마지막까지 쓸지 미리 알 수 없다면 어떻게 할까.
use std::rc::Rc;
fn main() {
let notice = Rc::new(String::from("server check at 3am"));
println!("owners: {}", Rc::strong_count(¬ice));
let team_a = Rc::clone(¬ice);
let team_b = Rc::clone(¬ice);
println!("owners: {}", Rc::strong_count(¬ice));
println!("{team_a} / {team_b}");
drop(team_a);
println!("owners: {}", Rc::strong_count(¬ice));
} 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: 2Rc::new(값)으로 만든다. Rc 는 Reference Counted(주인 수를 센다)의 줄임이다.use std::rc::Rc;로 불러온다.Rc::clone(¬ice)는 문자열을 복사하지 않는다. 같은 값을 가리키는 주인을 하나 늘리고 수만 센다. 16편의.clone()과 이름은 같지만 하는 일이 다르다는 것을 드러내려고Rc::clone으로 쓰는 것이 관례다.Rc::strong_count가 지금 주인 수다. 1 → 3 → 2 로 바뀌었다.drop(team_a)는 그 주인을 지금 치운다. 블록이 끝날 때 저절로 일어나는 일을 앞당긴 것이다. 주인 수가 0 이 되는 순간 문자열이 치워진다.
02 Rc 안은 고칠 수 없다
주인이 여럿인 목록에 항목을 더해 보자.
use std::rc::Rc;
fn main() {
let list = Rc::new(Vec::new());
let other = Rc::clone(&list);
list.push("first");
println!("{other:?}");
}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>>`- 무엇 —
Rc안의 데이터를 고칠 수 있게 빌릴 수 없다. - 어디 — 6번째 줄
list.push. - 고치는 법 —
help:는 기술적인 이유(DerefMut트레이트가 없다)만 적었다.
주인이 여럿인데 아무나 고칠 수 있으면, 19편에서 막았던 "읽는 중에 다른 쪽이 고치기" 가 일어난다. 그래서 Rc 는 읽기만 허락한다.
03 RefCell — 실행 중에 빌림을 따진다
RefCell 로 감싸면 Rc 안의 값도 고칠 수 있다.
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());
} 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 해 보자.
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:?}");
} 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 가 한다. 다음 편은 코드를 모듈로 나누는 법이다.