ข้ามไปยังเนื้อหา

Smart Pointers

ใน JavaScript และ TypeScript ทุก object อยู่บน heap และ garbage collector ดูแล lifetime ให้ คุณไม่ต้องคิดว่าใครเป็นเจ้าของอะไร GC จัดการเอง ส่วน Rust ไม่มี GC แต่ให้ smart pointer มาเป็นชุดเครื่องมือที่บอก ownership และ aliasing rule อย่างชัดเจน โดยมีต้นทุน runtime เป็นศูนย์

Smart pointerวัตถุประสงค์คู่เทียบ JS/TS
Box<T>Heap allocation, single ownernew MyClass() — เจ้าของคนเดียว ถูก drop เมื่อออกจาก scope
Rc<T>Reference-counted shared ownership (single-thread)GC reference counting
Arc<T>เหมือนกับ Rc<T> แต่ thread-safeShared reference ข้าม async tasks
RefCell<T>Interior mutability (runtime borrow checking){ mutable: T } wrapper
Rc<RefCell<T>>Shared + mutable (single-thread)Mutable shared state ข้ามหลาย holders

Box<T> คือ smart pointer ที่ง่ายที่สุด: ย้ายค่าไปไว้บน heap แล้วคืน pointer แบบ single-owner ให้ พอ Box หลุด scope หน่วยความจำบน heap จะถูกคืนอัตโนมัติ

TypeScript
// TypeScript — every object is heap-allocated by default
class Node {
constructor(
public value: number,
public next: Node | null = null
) {}
}
const list = new Node(1, new Node(2, new Node(3)));
console.log(list.value); // 1
console.log(list.next?.value); // 2
Rust
// Box<T>: explicit heap allocation in Rust
// Required for recursive types (the compiler needs a fixed-size pointer)
#[derive(Debug)]
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Cons(3,
Box::new(List::Nil))))));
println!("{:?}", list);
// Simple Box usage
let b = Box::new(5);
println!("b = {b}");
// b is freed here (end of scope)
}

เมื่อหลายส่วนของ program ต้องการ อ่าน ค่าเดียวกัน และคุณกำหนดตอน compile time ไม่ได้ว่าตัวไหนจะเลิกใช้เป็นตัวสุดท้าย ให้ใช้ Rc<T> ทุกครั้งที่เรียก Rc::clone reference count จะเพิ่มขึ้นหนึ่ง และเมื่อ Rc ตัวสุดท้าย drop ค่านั้นถึงจะถูกคืน นี่คือสิ่งที่ GC ของ JavaScript ทำให้อัตโนมัติ — ส่วน Rc ทำให้เห็นชัด ๆ

TypeScript
// TypeScript — two variables can reference the same object
const config = { maxRetries: 3 };
const serviceA = { config };
const serviceB = { config }; // same object, no copy
config.maxRetries = 5;
console.log(serviceA.config.maxRetries); // 5 — shared reference
Rust
use std::rc::Rc;
fn main() {
let config = Rc::new(String::from("max_retries=3"));
let service_a = Rc::clone(&config); // increments ref count
let service_b = Rc::clone(&config); // increments ref count
println!("config = {config}");
println!("service_a = {service_a}");
println!("service_b = {service_b}");
println!("ref count = {}", Rc::strong_count(&config)); // 3
drop(service_a); // decrements ref count
println!("after drop: ref count = {}", Rc::strong_count(&config)); // 2
}

borrow checker ของ Rust บังคับว่าคุณจะมี mutable reference หนึ่งตัว หรือ immutable reference หลายตัวก็ได้ — แต่ห้ามมีพร้อมกัน ส่วน RefCell<T> ย้ายการตรวจนี้ไปไว้ที่ runtime: เรียก .borrow() เมื่อต้องการ shared access และ .borrow_mut() เมื่อต้องการ exclusive access ถ้าละเมิดกฎตอน runtime จะเกิด panic แทนที่จะโดนปฏิเสธตั้งแต่ compile

use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
{
let mut v = data.borrow_mut(); // exclusive borrow
v.push(4);
} // borrow released here
println!("{:?}", data.borrow()); // shared borrow: [1, 2, 3, 4]

รวม Rc (หลาย owners) กับ RefCell (interior mutability) เพื่อให้ได้คู่เทียบที่ใกล้เคียงที่สุดกับ JavaScript’s mutable shared references

use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Box<T>: heap allocation, single owner
let b = Box::new(5);
println!("b = {b}");
// Rc<T>: reference-counted shared ownership
let a = Rc::new(String::from("hello"));
let b2 = Rc::clone(&a);
println!("a = {a}, b2 = {b2}, count = {}", Rc::strong_count(&a));
// RefCell<T>: interior mutability - borrow checking at runtime
let data = RefCell::new(vec![1, 2, 3]);
{
let mut v = data.borrow_mut();
v.push(4);
}
println!("data = {:?}", data.borrow());
// Rc<RefCell<T>>: shared mutable state
let shared = Rc::new(RefCell::new(0));
let clone1 = Rc::clone(&shared);
*clone1.borrow_mut() += 10;
println!("shared = {}", shared.borrow());
}
ต้องการ `Box<T>` แทน plain value เมื่อใด?
`Rc::clone(&ptr)` ทำอะไร?
อะไรเกิดขึ้นถ้าคุณเรียก `borrow_mut()` บน `RefCell<T>` เมื่อมี borrow active อยู่แล้ว?
ควรใช้ combination ใดสำหรับ shared mutable state ข้ามหลาย threads?