내부 가변성 문서의 검증 코드다.
//! 내부 가변성: &self로는 Vec에 push할 수 없는 것(E0596), &self 메서드 안에서 Cell로 세기, RefCell의 빌림 규칙은 실행 중에 검사된다
//! (try_borrow_mut는 Err(BorrowMutError), borrow_mut는 panic), 빌림을 너무 오래 쥐는 흔한 실수,
//! OnceCell 캐시, LazyLock 정적 값이 여러 스레드에서도 한 번만 초기화되는 것,
//! Cell을 스레드끼리 나누면 컴파일 오류(E0277)이고 Mutex는 되는 것, Rc<RefCell<T>>로 주인 여럿이 함께 고치는 것,
//! RefCell 빌림을 쥔 채 .await하면 clippy가 기본으로 경고하는 것을 확인한다.
//! 의존성: 없음 (표준 라이브러리만)
//! 실행: python3 "_시스템/Rust 계획/rs_check.py" "Rust/3.개념집/29_interior-mutability_verify.rs"
use std::cell::{Cell, OnceCell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, LazyLock, Mutex};
// ---------- 1. Cell: &self로 세기 ----------
struct Parser {
calls: Cell<u32>,
}
impl Parser {
fn parse(&self, s: &str) -> Option<i32> {
self.calls.set(self.calls.get() + 1); // &self인데도 바꿀 수 있다
s.parse().ok()
}
}
// ---------- 2. RefCell: 가짜 메신저(테스트 대역) ----------
trait Messenger {
fn send(&self, msg: &str);
}
struct MockMessenger {
sent: RefCell<Vec<String>>,
}
impl Messenger for MockMessenger {
fn send(&self, msg: &str) {
self.sent.borrow_mut().push(msg.to_string());
}
}
fn warn_if_over(m: &dyn Messenger, used: u32, limit: u32) {
if used * 100 >= limit * 90 {
m.send("over 90% of quota");
}
}
// ---------- 3. 빌림을 오래 쥐는 실수 ----------
struct Registry {
names: RefCell<Vec<String>>,
}
impl Registry {
fn add(&self, n: &str) {
self.names.borrow_mut().push(n.to_string());
}
/// 틀린 버전: 읽기 빌림을 쥔 채 add를 부른다 → 실행 중 panic
fn copy_first_buggy(&self) {
let names = self.names.borrow();
if let Some(first) = names.first() {
self.add(first); // 여기서 borrow_mut → 이미 빌려 있어 panic
}
}
/// 고친 버전: 필요한 값을 복사해 빌림을 먼저 끝낸다
fn copy_first_fixed(&self) {
let first = self.names.borrow().first().cloned(); // 이 줄 끝에서 빌림이 끝난다
if let Some(first) = first {
self.add(&first);
}
}
}
// ---------- 4. OnceCell 캐시 ----------
struct Report {
rows: Vec<u64>,
total: OnceCell<u64>,
computed: Cell<u32>,
}
impl Report {
fn total(&self) -> u64 {
*self.total.get_or_init(|| {
self.computed.set(self.computed.get() + 1);
self.rows.iter().sum()
})
}
}
// ---------- 5. LazyLock 정적 값 ----------
static INIT_COUNT: AtomicU32 = AtomicU32::new(0);
static STATUS_TEXT: LazyLock<HashMap<u16, &'static str>> = LazyLock::new(|| {
INIT_COUNT.fetch_add(1, Ordering::SeqCst);
HashMap::from([
(200, "OK"),
(404, "Not Found"),
(503, "Service Unavailable"),
])
});
fn compile_errors(code: &str) -> String {
compile_with("rustc", "rustc", code)
}
fn compile_with(tool: &str, tag: &str, code: &str) -> String {
let dir = std::env::temp_dir().join(format!("vr29_{}_{tag}", std::process::id()));
std::fs::create_dir_all(&dir).expect("임시 폴더");
let file = dir.join("snippet.rs");
std::fs::write(&file, code).expect("쓰기");
let out = std::process::Command::new(tool)
.args([
"--edition",
"2024",
"--crate-type=lib",
"--emit=metadata",
"--out-dir",
])
.arg(&dir)
.arg(&file)
.output()
.expect("컴파일러 실행");
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn panic_message(r: std::thread::Result<()>) -> String {
let p = r.expect_err("panic이 나야 한다");
if let Some(s) = p.downcast_ref::<String>() {
s.clone()
} else if let Some(s) = p.downcast_ref::<&str>() {
s.to_string()
} else {
"unknown".into()
}
}
fn main() {
std::panic::set_hook(Box::new(|_| {}));
// 1. Cell
let p = Parser {
calls: Cell::new(0),
};
let r: Vec<Option<i32>> = ["1", "x", "3"].iter().map(|s| p.parse(s)).collect();
assert_eq!(r, vec![Some(1), None, Some(3)]);
assert_eq!(p.calls.get(), 3);
println!(
"1. Cell: &self 메서드 세 번 호출, calls = {}",
p.calls.get()
);
// 2. RefCell 대역
let m = MockMessenger {
sent: RefCell::new(vec![]),
};
warn_if_over(&m, 95, 100);
warn_if_over(&m, 10, 100);
assert_eq!(*m.sent.borrow(), vec!["over 90% of quota".to_string()]);
// RefCell 없이 &self에서 Vec에 push하면 컴파일 오류
let e = compile_errors(
"pub struct M { sent: Vec<String> }\nimpl M { pub fn send(&self, m: &str) { self.sent.push(m.to_string()); } }\n",
);
assert!(e.contains("error[E0596]"), "{e}");
assert!(
e.contains("cannot borrow `self.sent` as mutable, as it is behind a `&` reference"),
"{e}"
);
// 2-1. Rc<RefCell<T>>: 주인 여럿이 같은 Vec을 고친다
let shared = Rc::new(RefCell::new(Vec::new()));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
a.borrow_mut().push("from a");
b.borrow_mut().push("from b");
assert_eq!(*shared.borrow(), vec!["from a", "from b"]);
assert_eq!(Rc::strong_count(&shared), 3);
println!("2. Rc<RefCell<Vec>>: 주인 셋, 내용 {:?}", shared.borrow());
// 3. 실행 중 빌림 검사
let c = RefCell::new(5);
{
let r1 = c.borrow();
let r2 = c.borrow(); // 읽기 빌림은 여러 개 가능
assert_eq!(*r1 + *r2, 10);
let err = c
.try_borrow_mut()
.expect_err("읽기 빌림 중에는 쓰기 빌림 불가");
assert_eq!(format!("{err:?}"), "BorrowMutError");
assert_eq!(err.to_string(), "RefCell already borrowed");
}
{
let mut w = c.borrow_mut();
*w += 1;
let err = c.try_borrow().expect_err("쓰기 빌림 중에는 읽기 빌림 불가");
assert_eq!(format!("{err:?}"), "BorrowError");
assert_eq!(err.to_string(), "RefCell already mutably borrowed");
}
assert_eq!(*c.borrow(), 6); // 빌림이 끝나면 다시 된다
// borrow_mut 충돌은 panic
let msg = panic_message(std::panic::catch_unwind(|| {
let c = RefCell::new(0);
let _a = c.borrow_mut();
let _b = c.borrow_mut();
}));
println!("3. borrow_mut 두 번: panic 메시지 {msg:?}");
assert_eq!(msg, "RefCell already borrowed");
// 흔한 실수
let reg = Registry {
names: RefCell::new(vec!["a".into()]),
};
let msg = panic_message(std::panic::catch_unwind(std::panic::AssertUnwindSafe(
|| reg.copy_first_buggy(),
)));
assert_eq!(msg, "RefCell already borrowed");
reg.copy_first_fixed();
assert_eq!(*reg.names.borrow(), vec!["a", "a"]);
println!(" 빌림을 쥔 채 add: panic / 복사 후 add: [\"a\", \"a\"]");
// 같은 버그를 RefCell 없이 &mut self로 쓰면 컴파일 오류(E0502)로 잡힌다
let e = compile_errors(
"pub struct R { names: Vec<String> }\n\
impl R {\n\
fn add(&mut self, n: &str) { self.names.push(n.to_string()); }\n\
pub fn copy_first(&mut self) { if let Some(first) = self.names.first() { self.add(first); } }\n\
}\n",
);
assert!(e.contains("error[E0502]"), "{e}");
println!(" 같은 코드를 &mut self로: E0502");
// 4. OnceCell
let rep = Report {
rows: vec![10, 20, 30],
total: OnceCell::new(),
computed: Cell::new(0),
};
assert_eq!(rep.total(), 60);
assert_eq!(rep.total(), 60);
assert_eq!(rep.computed.get(), 1);
// set은 한 번만 된다: 두 번째는 넣으려던 값을 Err로 돌려준다
let once = OnceCell::new();
assert_eq!(once.set(92), Ok(()));
assert_eq!(once.set(62), Err(62));
assert_eq!(once.get(), Some(&92));
println!("4. OnceCell: total() 두 번, 계산은 한 번 / set 두 번째는 Err(62)");
// 5. LazyLock: 8개 스레드가 동시에 읽어도 초기화는 한 번
assert_eq!(INIT_COUNT.load(Ordering::SeqCst), 0); // 아직 아무도 안 읽었다
let hs: Vec<_> = (0..8)
.map(|_| std::thread::spawn(|| STATUS_TEXT[&404]))
.collect();
for h in hs {
assert_eq!(h.join().expect("panic 없음"), "Not Found");
}
assert_eq!(INIT_COUNT.load(Ordering::SeqCst), 1);
println!("5. LazyLock: 스레드 8개, 초기화 1번");
// 6. 스레드 경계
let e = compile_errors(
"use std::cell::Cell;\npub fn f() { let c = Cell::new(0); std::thread::scope(|s| { s.spawn(|| c.set(1)); }); }\n",
);
assert!(e.contains("error[E0277]"), "{e}");
assert!(
e.contains("`Cell<i32>` cannot be shared between threads safely"),
"{e}"
);
assert!(
e.contains("use `std::sync::RwLock` or `std::sync::atomic::AtomicI32` instead"),
"{e}"
);
let e = compile_errors(
"use std::{cell::RefCell, sync::Arc};\npub fn f() { let c = Arc::new(RefCell::new(0)); std::thread::spawn(move || *c.borrow_mut() += 1); }\n",
);
assert!(e.contains("error[E0277]"), "{e}");
assert!(
e.contains("`RefCell<i32>` cannot be shared between threads safely"),
"{e}"
);
let counter = Arc::new(Mutex::new(0));
let hs: Vec<_> = (0..4)
.map(|_| {
let c = Arc::clone(&counter);
std::thread::spawn(move || {
for _ in 0..1000 {
*c.lock().expect("잠금이 오염되지 않았다") += 1;
}
})
})
.collect();
for h in hs {
h.join().expect("panic 없음");
}
assert_eq!(*counter.lock().expect("잠금"), 4000);
println!("6. Cell·RefCell을 스레드로: E0277 / Arc<Mutex>: 4000");
// 7. RefCell 빌림을 쥔 채 .await → clippy 기본 경고
let e = compile_with(
"clippy-driver",
"await",
"pub async fn w(c: &std::cell::RefCell<i32>) { let b = c.borrow_mut(); std::future::ready(()).await; drop(b); }\n",
);
assert!(
e.contains("`#[warn(clippy::await_holding_refcell_ref)]` on by default"),
"{e}"
);
assert!(
e.contains("this `RefCell` reference is held across an await point"),
"{e}"
);
println!("7. clippy: await_holding_refcell_ref 기본 경고");
println!("ALL CHECKS PASSED");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cell_replace_and_take() {
let c = Cell::new(3);
assert_eq!(c.replace(7), 3);
assert_eq!(c.take(), 7);
assert_eq!(c.get(), 0);
}
#[test]
fn borrow_ends_at_scope_end() {
let c = RefCell::new(vec![1]);
{
let r = c.borrow();
assert!(c.try_borrow_mut().is_err());
drop(r);
}
c.borrow_mut().push(2);
assert_eq!(c.borrow().len(), 2);
}
}