RefCell<T>
와 내부 가변성 패턴
**내부 가변성(Interior Mutability)**은 러스트의 디자인 패턴 중 하나로, 데이터에 대한 불변 참조가 존재할 때도 데이터를 변경할 수 있게 해준다. 일반적으로 이러한 동작은 러스트의 빌림 규칙에 의해 금지된다. 이 패턴은 데이터 구조 내부에 unsafe
코드를 사용해 러스트의 일반적인 뮤테이션과 빌림 규칙을 유연하게 적용한다. unsafe
코드는 컴파일러에게 우리가 직접 규칙을 확인하고 있음을 알려주며, 컴파일러가 이를 대신 확인하지 않도록 한다. unsafe
코드에 대해서는 20장에서 더 자세히 다룰 것이다.
내부 가변성 패턴을 사용하는 타입은 컴파일러가 보장할 수 없더라도 런타임에 빌림 규칙이 지켜질 것임을 확신할 수 있을 때만 사용할 수 있다. 이때 관련된 unsafe
코드는 안전한 API로 감싸지며, 외부 타입은 여전히 불변성을 유지한다.
이 개념을 이해하기 위해 내부 가변성 패턴을 따르는 RefCell<T>
타입을 살펴보자.
RefCell<T>
를 사용해 런타임에 빌림 규칙 적용하기
Rc<T>
와 달리, RefCell<T>
타입은 자신이 가지고 있는 데이터에 대해 단일 소유권을 나타낸다. 그렇다면 RefCell<T>
는 Box<T>
같은 타입과 어떻게 다른가? 4장에서 배운 빌림 규칙을 다시 떠올려보자:
- 특정 시점에 하나의 가변 참조 또는 여러 개의 불변 참조 중 하나만 가질 수 있다(둘 다 동시에 가질 수 없다).
- 참조는 항상 유효해야 한다.
참조와 Box<T>
의 경우, 빌림 규칙의 불변성은 컴파일 타임에 강제된다. 반면 RefCell<T>
의 경우, 이 불변성은 런타임에 강제된다. 참조를 사용할 때 이 규칙을 어기면 컴파일 오류가 발생하지만, RefCell<T>
를 사용할 때 규칙을 어기면 프로그램이 패닉 상태에 빠지고 종료된다.
컴파일 타임에 빌림 규칙을 검사하는 장점은 개발 과정에서 오류를 더 빨리 발견할 수 있고, 모든 분석이 사전에 완료되기 때문에 런타임 성능에 영향을 미치지 않는다는 점이다. 이러한 이유로 대부분의 경우 컴파일 타임에 빌림 규칙을 검사하는 것이 최선의 선택이며, 이것이 Rust의 기본 동작 방식이다.
반면, 런타임에 빌림 규칙을 검사하는 장점은 컴파일 타임 검사에서는 허용되지 않았던 특정 메모리 안전 시나리오를 허용할 수 있다는 점이다. Rust 컴파일러와 같은 정적 분석은 본질적으로 보수적이다. 코드의 일부 특성은 코드를 분석하는 것만으로는 감지할 수 없다. 가장 유명한 예는 정지 문제(Halting Problem)로, 이 책의 범위를 벗어나지만 연구해볼 만한 흥미로운 주제다.
어떤 분석은 불가능하기 때문에, Rust 컴파일러가 코드가 소유권 규칙을 준수하는지 확신할 수 없으면 올바른 프로그램도 거부할 수 있다. 이런 의미에서 컴파일러는 보수적이다. 만약 Rust가 잘못된 프로그램을 허용한다면, 사용자는 Rust가 제공하는 보장을 신뢰할 수 없게 될 것이다. 반면, Rust가 올바른 프로그램을 거부한다면 프로그래머에게 불편을 줄 수는 있지만, 치명적인 문제는 발생하지 않는다. RefCell<T>
타입은 코드가 빌림 규칙을 준수한다고 확신하지만 컴파일러가 이를 이해하고 보장할 수 없는 경우에 유용하다.
Rc<T>
와 마찬가지로, RefCell<T>
는 단일 스레드 시나리오에서만 사용할 수 있으며, 멀티스레드 환경에서 사용하려고 하면 컴파일 타임 오류가 발생한다. 16장에서 멀티스레드 프로그램에서 RefCell<T>
의 기능을 어떻게 사용할 수 있는지 알아볼 것이다.
Box<T>
, Rc<T>
, RefCell<T>
를 선택하는 이유를 정리하면 다음과 같다:
Rc<T>
는 동일한 데이터에 대해 여러 소유자를 허용한다.Box<T>
와RefCell<T>
는 단일 소유자만 허용한다.Box<T>
는 컴파일 타임에 검사되는 불변 또는 가변 빌림을 허용한다.Rc<T>
는 컴파일 타임에 검사되는 불변 빌림만 허용한다.RefCell<T>
는 런타임에 검사되는 불변 또는 가변 빌림을 허용한다.RefCell<T>
는 런타임에 가변 빌림을 허용하기 때문에,RefCell<T>
가 불변일 때도 내부 값을 변경할 수 있다.
불변 값 내부의 값을 변경하는 것을 내부 가변성(interior mutability) 패턴이라고 한다. 내부 가변성이 유용한 상황을 살펴보고, 어떻게 가능한지 알아보자.
내부 가변성: 불변 값에 대한 가변 참조
빌림 규칙의 한 가지 결과는 불변 값을 가변적으로 빌릴 수 없다는 점이다. 예를 들어, 다음 코드는 컴파일되지 않는다:
fn main() {
let x = 5;
let y = &mut x;
}
이 코드를 컴파일하려고 하면 다음과 같은 에러가 발생한다:
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
그러나 특정 상황에서는 값이 자신의 메서드 내에서 변이할 수 있으면서도 외부 코드에서는 불변으로 보이는 것이 유용할 때가 있다. 값의 메서드 외부에서는 해당 값을 변이할 수 없다. RefCell<T>
를 사용하면 내부 가변성을 얻을 수 있지만, RefCell<T>
가 빌림 규칙을 완전히 우회하는 것은 아니다: 컴파일러의 빌림 검사기는 이 내부 가변성을 허용하며, 빌림 규칙은 런타임에 검사된다. 규칙을 위반하면 컴파일러 에러 대신 panic!
이 발생한다.
RefCell<T>
를 사용해 불변 값을 변이할 수 있는 실제 예제를 통해 왜 이것이 유용한지 살펴보자.
내부 가변성 사용 사례: Mock 객체
테스트를 진행할 때 프로그래머는 특정 동작을 관찰하고 구현이 올바른지 확인하기 위해 한 타입을 다른 타입으로 대체할 수 있다. 이 대체 타입을 **테스트 더블(test double)**이라고 한다. 영화 촬영에서 스턴트 더블이 배우를 대신해 어려운 장면을 수행하는 것과 비슷하다고 생각하면 된다. 테스트 더블은 테스트를 실행할 때 다른 타입을 대신한다. Mock 객체는 테스트 중에 발생한 일을 기록해 올바른 동작이 수행되었는지 확인할 수 있도록 하는 특수한 테스트 더블이다.
Rust는 다른 언어와 같은 의미의 객체를 가지고 있지 않으며, 표준 라이브러리에 Mock 객체 기능이 내장되어 있지 않다. 하지만 Mock 객체와 동일한 목적을 수행할 수 있는 구조체를 직접 만들 수 있다.
테스트할 시나리오는 다음과 같다: 최댓값과 현재 값을 비교해 얼마나 가까운지 추적하고, 그에 따라 메시지를 보내는 라이브러리를 만든다. 이 라이브러리는 사용자가 허용된 API 호출 횟수를 추적하는 등 다양한 용도로 활용할 수 있다.
우리가 만드는 라이브러리는 최댓값에 얼마나 가까운지 추적하고, 특정 시점에 어떤 메시지를 보내야 하는지 결정하는 기능만 제공한다. 이 라이브러리를 사용하는 애플리케이션은 메시지를 보내는 방식을 직접 제공해야 한다. 애플리케이션은 메시지를 화면에 표시하거나, 이메일을 보내거나, 문자 메시지를 보내는 등 다양한 방식으로 메시지를 전달할 수 있다. 라이브러리는 이러한 세부 사항을 알 필요가 없다. 단지 우리가 제공할 Messenger
라는 트레이트를 구현한 무언가만 필요하다. 다음은 라이브러리 코드이다.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
이 코드에서 중요한 부분은 Messenger
트레이트가 self
에 대한 불변 참조와 메시지 텍스트를 인자로 받는 send
메서드를 가지고 있다는 점이다. 이 트레이트는 Mock 객체가 실제 객체와 동일한 방식으로 사용될 수 있도록 구현해야 하는 인터페이스이다. 또 다른 중요한 부분은 LimitTracker
의 set_value
메서드의 동작을 테스트하려 한다는 점이다. value
매개변수에 전달하는 값을 변경할 수 있지만, set_value
는 우리가 확인할 수 있는 값을 반환하지 않는다. 우리는 Messenger
트레이트를 구현한 객체와 특정 max
값을 가진 LimitTracker
를 생성한 후, value
에 다른 숫자를 전달했을 때 메신저가 적절한 메시지를 보내도록 하는지 확인하고 싶다.
우리는 send
를 호출할 때 이메일이나 문자 메시지를 보내는 대신, 전달받은 메시지를 기록만 하는 Mock 객체가 필요하다. Mock 객체의 새 인스턴스를 생성하고, 이 Mock 객체를 사용하는 LimitTracker
를 만든 후, LimitTracker
의 set_value
메서드를 호출하고, Mock 객체가 예상한 메시지를 가지고 있는지 확인할 수 있다. 다음은 이를 구현하려는 시도이지만, 빌림 검사기가 이를 허용하지 않는다.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
MockMessenger
구현 시도이 테스트 코드는 MockMessenger
구조체를 정의한다. 이 구조체는 전달받은 메시지를 추적하기 위해 String
값의 Vec
을 가진 sent_messages
필드를 가지고 있다. 또한, 빈 메시지 목록으로 시작하는 MockMessenger
값을 쉽게 생성할 수 있도록 new
연관 함수를 정의한다. 그리고 MockMessenger
에 Messenger
트레이트를 구현해 LimitTracker
에 MockMessenger
를 전달할 수 있게 한다. send
메서드의 정의에서는 매개변수로 전달된 메시지를 MockMessenger
의 sent_messages
목록에 저장한다.
테스트에서는 LimitTracker
가 max
값의 75% 이상인 value
를 설정하도록 요청했을 때 어떤 일이 발생하는지 확인한다. 먼저, 빈 메시지 목록으로 시작하는 새로운 MockMessenger
를 생성한다. 그런 다음, 새로운 LimitTracker
를 만들고, 새로운 MockMessenger
에 대한 참조와 max
값으로 100
을 전달한다. LimitTracker
의 set_value
메서드를 80
이라는 값으로 호출한다. 이 값은 100의 75%를 초과한다. 그런 다음, MockMessenger
가 추적 중인 메시지 목록에 하나의 메시지가 있어야 한다고 확인한다.
그러나 이 테스트에는 한 가지 문제가 있다:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
send
메서드가 self
에 대한 불변 참조를 받기 때문에 MockMessenger
를 수정해 메시지를 추적할 수 없다. 또한, 오류 메시지에서 제안한 대로 impl
메서드와 trait
정의 모두에서 &mut self
를 사용할 수도 없다. 테스트를 위해서만 Messenger
트레이트를 변경하고 싶지 않다. 대신, 기존 설계와 함께 테스트 코드가 올바르게 작동하도록 하는 방법을 찾아야 한다.
이런 상황에서 **내부 가변성(interior mutability)**이 도움이 될 수 있다! sent_messages
를 RefCell<T>
안에 저장하면, send
메서드가 sent_messages
를 수정해 우리가 본 메시지를 저장할 수 있다. 다음은 이를 구현한 코드이다.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T>
사용sent_messages
필드는 이제 Vec<String>
대신 RefCell<Vec<String>>
타입이다. new
함수에서는 빈 벡터를 감싸는 새로운 RefCell<Vec<String>>
인스턴스를 생성한다.
send
메서드의 구현에서 첫 번째 매개변수는 여전히 self
에 대한 불변 참조로, 트레이트 정의와 일치한다. self.sent_messages
에 있는 RefCell<Vec<String>>
에 borrow_mut
를 호출해 RefCell<Vec<String>>
내부의 벡터에 대한 가변 참조를 얻는다. 그런 다음, 벡터에 대한 가변 참조에 push
를 호출해 테스트 중에 전송된 메시지를 추적할 수 있다.
마지막으로 변경해야 할 부분은 assertion이다. 내부 벡터에 몇 개의 항목이 있는지 확인하기 위해 RefCell<Vec<String>>
에 borrow
를 호출해 벡터에 대한 불변 참조를 얻는다.
이제 RefCell<T>
를 사용하는 방법을 살펴봤으니, 이제 RefCell<T>
가 어떻게 동작하는지 자세히 알아보자!
RefCell<T>
를 사용해 런타임에 대여 상태 추적하기
불변 참조와 가변 참조를 만들 때 각각 &
와 &mut
구문을 사용한다. RefCell<T>
를 사용할 때는 borrow
와 borrow_mut
메서드를 사용한다. 이 메서드들은 RefCell<T>
의 안전한 API에 속한다. borrow
메서드는 스마트 포인터 타입인 Ref<T>
를 반환하고, borrow_mut
는 RefMut<T>
를 반환한다. 두 타입 모두 Deref
를 구현하므로 일반 참조처럼 사용할 수 있다.
RefCell<T>
는 현재 활성화된 Ref<T>
와 RefMut<T>
스마트 포인터의 수를 추적한다. borrow
를 호출할 때마다 RefCell<T>
는 불변 대여의 수를 증가시킨다. Ref<T>
값이 스코프를 벗어나면 불변 대여의 수가 1 감소한다. 컴파일 타임의 대여 규칙과 마찬가지로, RefCell<T>
는 여러 불변 대여 또는 하나의 가변 대여를 허용한다.
이 규칙을 위반하려고 하면, 일반 참조에서와 같이 컴파일러 에러가 발생하는 대신 RefCell<T>
의 구현이 런타임에 패닉을 일으킨다. 리스트 15-23은 리스트 15-22의 send
구현을 수정한 예시다. 같은 스코프에서 두 개의 가변 대여를 의도적으로 생성해 RefCell<T>
가 런타임에 이를 방지하는 것을 보여준다.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T>
가 런타임에 패닉을 일으키는 것을 확인borrow_mut
에서 반환된 RefMut<T>
스마트 포인터를 one_borrow
변수에 저장한다. 그런 다음 같은 방식으로 two_borrow
변수에 또 다른 가변 대여를 생성한다. 이렇게 하면 같은 스코프에서 두 개의 가변 참조가 생기는데, 이는 허용되지 않는다. 라이브러리의 테스트를 실행하면 리스트 15-23의 코드는 컴파일 에러 없이 빌드되지만, 테스트는 실패한다:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
코드가 already borrowed: BorrowMutError
메시지와 함께 패닉을 일으킨 것을 확인할 수 있다. 이는 RefCell<T>
가 런타임에 대여 규칙 위반을 처리하는 방식이다.
컴파일 타임이 아닌 런타임에 대여 에러를 잡는 방식을 선택하면, 개발 과정에서 코드의 실수를 더 늦게 발견할 가능성이 있다. 심지어 프로덕션 환경에 배포된 후에야 발견될 수도 있다. 또한, 컴파일 타임이 아닌 런타임에 대여 상태를 추적하기 때문에 코드에 약간의 런타임 성능 저하가 발생한다. 하지만 RefCell<T>
를 사용하면 불변 값만 허용되는 컨텍스트에서도 자신을 수정해 메시지를 추적할 수 있는 목 객체를 작성할 수 있다. RefCell<T>
는 이러한 트레이드오프가 있음에도 불구하고 일반 참조보다 더 많은 기능을 제공한다.
Rc<T>
와 RefCell<T>
를 사용해 가변 데이터에 여러 소유자 허용하기
RefCell<T>
를 사용하는 일반적인 방법은 Rc<T>
와 결합하는 것이다. Rc<T>
는 데이터에 여러 소유자를 허용하지만, 데이터에 대해 불변 접근만 제공한다. 만약 Rc<T>
가 RefCell<T>
를 가지고 있다면, 여러 소유자를 가질 수 있고 동시에 데이터를 변경할 수 있는 값을 얻을 수 있다.
예를 들어, 15장 18번 예제에서 Rc<T>
를 사용해 여러 리스트가 다른 리스트의 소유권을 공유할 수 있게 한 cons 리스트 예제를 떠올려보자. Rc<T>
는 불변 값만 보유하므로, 리스트를 생성한 후에는 리스트 내의 값을 변경할 수 없다. 이제 RefCell<T>
를 추가해 리스트 내의 값을 변경할 수 있게 해보자. 15장 24번 예제는 Cons
정의에서 RefCell<T>
를 사용해 모든 리스트에 저장된 값을 수정할 수 있음을 보여준다.
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {a:?}"); println!("b after = {b:?}"); println!("c after = {c:?}"); }
Rc<RefCell<i32>>
를 사용해 수정 가능한 List
생성Rc<RefCell<i32>>
의 인스턴스인 값을 생성하고, 이를 value
라는 변수에 저장해 나중에 직접 접근할 수 있게 한다. 그런 다음 Cons
변형을 사용해 value
를 보유하는 List
를 a
에 생성한다. value
를 복제해 a
와 value
가 내부의 5
값을 공유하도록 해야 한다. 이렇게 하면 value
에서 a
로 소유권이 이전되거나 a
가 value
에서 빌리는 일이 없어진다.
리스트 a
를 Rc<T>
로 감싸서 리스트 b
와 c
를 생성할 때 둘 다 a
를 참조할 수 있게 한다. 이는 15장 18번 예제에서 했던 것과 동일하다.
리스트 a
, b
, c
를 생성한 후, value
의 값에 10을 더하고 싶다. 이를 위해 value
에 borrow_mut
를 호출한다. 이 메서드는 5장에서 논의한 자동 역참조 기능을 사용해 Rc<T>
를 내부의 RefCell<T>
값으로 역참조한다. borrow_mut
메서드는 RefMut<T>
스마트 포인터를 반환하고, 이를 역참조 연산자와 함께 사용해 내부 값을 변경한다.
a
, b
, c
를 출력하면 모두 5
가 아닌 15
로 수정된 값을 가지고 있음을 확인할 수 있다:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
이 기법은 상당히 유용하다! RefCell<T>
를 사용해 외부적으로는 불변인 List
값을 가지면서도, 내부 가변성을 제공하는 RefCell<T>
의 메서드를 사용해 필요할 때 데이터를 수정할 수 있다. 빌림 규칙에 대한 런타임 검사는 데이터 경쟁으로부터 보호해주며, 데이터 구조에서 이 유연성을 얻기 위해 약간의 속도를 희생하는 것도 가치가 있다. 다만 RefCell<T>
는 멀티스레드 코드에서는 작동하지 않는다는 점을 주의해야 한다. Mutex<T>
는 RefCell<T>
의 스레드 안전 버전이며, 16장에서 Mutex<T>
에 대해 논의할 것이다.