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

closure ที่​พก scope ติดตัว และ resolver ที่​ปิด​รู​ของ​มัน

บท​ที่​แล้ว​ทิ้ง​ฟิลด์หนึ่ง​ไว้​โดย​ไม่​อธิบาย คือ closure ใน​โครงสร้าง Function

บท​นี้​ทั้ง​บท​มี​ไว้​อธิบาย​ฟิลด์นั้น​ฟิลด์เดียว และ​รู​ที่​มัน​เปิด​ทิ้ง​ไว้​โดยที่​คน​ส่วน​ใหญ่​ไม่ทัน​เห็น

สิ่ง​ที่​ทำให้​เรื่อง​นี้​ไม่ใช่​แค่​รายละเอียด​ปลีกย่อย คือ​รู​นั้น​ทำให้​โปรแกรม​ที่​ถูกต้อง​ให้​คำ​ตอบ​ผิด โดย​ไม่มี error สัก​บรรทัด

หัวข้อ​นี้​แสดง​ว่า ClosureClosureฟังก์ชัน​ที่​พก​สภาพ​แวดล้อม​ตอน​ที่​มัน​ถูก​สร้าง​ติดตัว​ไป​ด้วย จึง​ยัง​อ่าน​ตัวแปร​ของ scope นั้น​ได้​แม้ scope นั้น​จบ​ไป​แล้ว ใน​ภาษา nok เกิด​จาก​การ​เก็บ environment ปัจจุบัน​ไว้​ตอน​ประกาศ ไม่​ได้​ใช้​กลไก​อื่น​เพิ่ม

ตอน​ที่ interpreter เจอ​คำ​สั่ง​ประกาศ function มัน​ทำ​สอง​อย่าง คือ​สร้าง​ค่า​ขึ้น​มา​แล้ว​ผูก​กับ​ชื่อ

src/interpreter.rs · ประกาศ function คือ​การ​เก็บ environment ปัจจุบัน​ไว้ (ตัด​มา​บาง​ส่วน)
Stmt::Function(decl) => {
// closure คือ environment *ตอนที่ประกาศ* ไม่ใช่ตอนที่เรียก — บทที่ 7
let function = Function::new(Rc::clone(decl), Rc::clone(&self.env), false);
self.env.borrow_mut().define(decl.name.clone(), Value::Function(Rc::new(function)));
}

Rc::clone(&self.env) คือ​ทั้งหมด​ของ​เรื่อง มัน​เพิ่ม​ตัว​นับ​ผู้​ถือ​ของ environment ที่​กำลัง​ทำงาน​อยู่​ตอน​นั้น แล้ว​เก็บ​ติด​ไป​กับ​ตัว function

ผล​ตาม​มา​คือ environment ก้อน​นั้น​จะ​ไม่​หาย​ไป​แม้ scope ที่​สร้าง​มัน​จบ​ไป​แล้ว เพราะ​ยัง​มี​ผู้​ถือ​อยู่​หนึ่ง​ราย

ตัวอย่าง​ของ​บท​นี้​ใช้ makeCounter ที่​คืน function ตัว​ใน​ออก​มา ตัวแปร count อยู่​ใน scope ที่​จบ​ไป​ตั้งแต่ makeCounter คืน​ค่า แต่​ค่า​ยัง​เดิน​หน้า​จาก 1 ไป 2 ได้ เพราะ scope นั้น​ยัง​มี​ชีวิต

การ​เรียก makeCounter ครั้ง​ที่​สอง​สร้าง scope ใหม่​ทั้ง​ก้อน counter ตัว​ที่​สอง​จึง​เริ่ม​นับ​จาก 1 ใหม่ ไม่ใช่​นับ​ต่อ​จาก​ตัว​แรก

หัวข้อ​นี้​อธิบาย​ว่าการ​ไล่​หา​ชื่อ​ตอน​รัน ทำให้ function ตัว​เดิม​ให้​คำ​ตอบ​ต่าง​กัน​สอง​ครั้ง​ได้

รูปแบบ​ที่​เปิด​รู​นี้​เขียน​สั้น​มาก — ประกาศ function ใน block ก่อน แล้ว​ค่อย​ประกาศ​ตัวแปร​ชื่อ​ซ้ำ​ทีหลัง ใน​ระหว่าง​ที่​เรียก function นั้น​คั่น​อยู่

src/resolver.rs · ปัญหา​ที่​บท​นี้​แก้ ใน​คำ​ของ source เอง (ตัด​มา​บาง​ส่วน)
//! ถ้า interpreter ไล่หาชื่อ `a` ตอนรัน การเรียกครั้งที่สองจะเจอ `a` ตัวใหม่
//! ที่เพิ่งประกาศ แล้วพิมพ์ `block` · แต่ `showA` ถูกเขียนไว้ *ก่อน* `a` ตัวนั้นมีอยู่
//! มันจึงไม่ควรเห็นเลย · closure ที่เปลี่ยนความหมายตามเวลาไม่ใช่ closure
//!
//! ทางแก้คือเดินต้นไม้อีกรอบก่อนรัน แล้วตอบคำถามเดียว: ชื่อนี้อยู่ห่างจากจุดที่ใช้
//! กี่ชั้น · คำตอบถูกจดไว้ด้วย `id` ของ node แล้วส่งให้ interpreter ใช้ตอนรัน
//! เมื่อคำตอบถูกล็อกไว้ตั้งแต่ก่อนรัน มันจึงเปลี่ยนตามลำดับการรันไม่ได้อีก
//!
//! เดินรอบนี้ยังเป็นที่ที่ error เชิงโครงสร้างถูกจับได้ก่อนโปรแกรมเริ่มทำงาน:
//! `return` นอก function · `this` นอก class · ประกาศชื่อซ้ำใน scope เดียวกัน

ประโยค​ที่​ต้อง​อ่าน​ซ้ำ​คือ​ประโยค​ปิด​ของ​ย่อหน้า​แรก — closure ที่​เปลี่ยน​ความหมาย​ตาม​เวลา​ไม่ใช่ closure

lexical scope ที่​บท​ที่ 5 วาง​กฎ​ไว้ สัญญา​ว่า​ชื่อ​หนึ่ง​หมาย​ถึง​ตัว​ไหน ตัดสิน​จาก​ตำแหน่ง​ใน code ไม่ใช่​จาก​ลำดับ​การ​รัน การ​ไล่​หา​ตอน​รัน​ผิด​สัญญา​ข้อ​นั้น

ทำไม​รู​นี้​ถึง​อันตราย​กว่า​รู​ที่​ทำให้​โปรแกรม​พัง

ไม่มี error ไม่มี warning ไม่มี​อะไร​ผิด​ปกติ​ให้​เห็น มี​แต่​คำ​ตอบ​ที่​ต่าง​จาก​ที่​ควร​เป็น

โปรแกรม​ที่​พัง​บอก​ตัวเอง​ว่า​พัง ส่วน​โปรแกรม​ที่​เข้าใจ​ชื่อ​ผิด​จะ​เดิน​ต่อ​ไป​เรื่อยๆ แล้ว​สะสม ผล​ที่​ผิด​ไป​เงียบๆ จนถึง​จุด​ที่​ไม่มี​ใคร​โยง​กลับ​มา​ถึง​ต้นเหตุ​ได้​แล้ว

หัวข้อ​นี้​แสดง​กลไก​ของ ResolverResolverรอบ​เดิน​บน AST ที่​ทำ​ก่อน​รัน​จริง เพื่อ​ตัดสิน​ล่วงหน้า​ว่า​ชื่อ​แต่ละ​ตัว​หมาย​ถึง​ตัวแปร​ใน scope ชั้น​ที่​เท่าไร — เป็น​ตัว​ปิด​บั๊ก scope ที่ closure ทำให้​เกิด คือ​การ​นับ​ชั้น​ไว้​ก่อน​รัน แล้ว​ให้ interpreter ใช้​คำ​ตอบ​นั้น​แทน​การ​ไล่​หา​เอง

คำถาม​เดียว​ที่ resolver ตอบ​คือ ชื่อ​นี้​อยู่​ห่าง​จาก​จุด​ที่​ใช้​กี่​ชั้น

src/resolver.rs · นับ​ชั้น​แล้ว​จด​คำ​ตอบ​ไว้
/// นับว่าชื่อนี้อยู่ห่างออกไปกี่ชั้น แล้วจดคำตอบไว้
///
/// ไม่เจอในชั้นไหนเลย = เป็น global · ไม่จดอะไร แล้วปล่อยให้ interpreter
/// ไปหาที่ global ตอนรัน ซึ่งเป็นพฤติกรรมที่ถูกต้องสำหรับ global พอดี
fn resolve_local(&mut self, name: &str, id: usize) {
for (depth, scope) in self.scopes.iter().rev().enumerate() {
if scope.contains_key(name) {
self.locals.insert(id, depth);
return;
}
}
}

คำ​ตอบ​ถูก​จด​ด้วย id ของ node ใน​ต้นไม้ ไม่ใช่​ด้วย​ชื่อ — ชื่อ​เดียวกัน​ที่​ปรากฏ​คนละ​ที่​ใน​โปรแกรม จึง​มี​คำ​ตอบ​ของ​ตัวเอง​แยก​กัน

ฝั่ง interpreter รับคำ​ตอบนั้นไปใช้ตรงๆ

src/interpreter.rs · หา​ค่า​ของ​ชื่อ ด้วย​คำ​ตอบ​ที่​จด​ไว้​แล้ว
/// หาค่าของชื่อ · ใช้คำตอบของ resolver ถ้ามี ไม่มีก็ถือว่าเป็น global
fn look_up(&self, name: &str, id: usize, line: usize) -> Result<Value, RuntimeError> {
let found = match self.locals.get(&id) {
Some(&depth) => Environment::get_at(&self.env, depth, name),
None => Environment::get(&self.globals, name),
};
found.ok_or_else(|| RuntimeError {
line, message: format!("ไม่รู้จักชื่อ '{name}'")
})
}

ความ​ต่าง​อยู่​ที่​สอง​บรรทัด​กลาง เส้นทาง​ที่​มี​คำ​ตอบ​จด​ไว้​ใช้ get_at ข้าม​ขึ้น​ไป​ตาม​จำนวน​ชั้น​นั้น แล้ว​หยิบ​จาก​ชั้น​เดียว

เส้นทาง​ที่​ไม่มี​คำ​ตอบ​ก็​ไม่​ได้​ไล่​หา​จาก​ชั้น​ใน​ออก​ไป​เช่น​กัน มัน​ถาม globals ตรงๆ ที่​เดียว เพราะ​ชื่อ​ที่ resolver ไม่​จด​ไว้ มี​ที่​อยู่​ได้ที่​เดียว​คือ​ที่​นั่น

เมื่อ​จำนวน​ชั้น​ถูก​ล็อก​ไว้​ตั้งแต่​ก่อน​โปรแกรม​เริ่ม​ทำงาน ตัวแปร​ที่​ประกาศ​ทีหลัง​จึง​เข้า​มา​แทรก​ไม่​ได้​อีก

flowchart TD
    A["AST จาก parser"] --> B["resolver · เดินต้นไม้รอบที่หนึ่ง"]
    B --> C["locals · id ของ node คู่กับ จำนวนชั้น"]
    B --> D["ResolveError · จับได้ตั้งแต่ก่อนรัน"]
    A --> E["interpreter · เดินต้นไม้รอบที่สอง"]
    C --> E
    E --> F{"node นี้อยู่ใน locals ไหม"}
    F -->|อยู่| G["get_at · ข้ามขึ้นไปตามจำนวนชั้นที่จดไว้"]
    F -->|ไม่อยู่| H["ไล่หาที่ global ตอนรัน"]

คำ​บรรยาย​ภาพ: ต้นไม้​ก้อน​เดียว​ถูก​เดิน​สอง​รอบ · รอบ​แรก​ไม่​รัน​อะไร​เลย มัน​แค่​ตอบ​คำถาม​เรื่อง​ชื่อ​ล่วงหน้า แล้ว​ส่ง​คำ​ตอบ​ให้​รอบ​ที่​สอง​ใช้

หัวข้อ​ย่อย​นี้​บอกว่า scope นอก​สุด​ไม่​ถูก​นับ​ชั้น และ​เหตุผล​ที่ resolver ตอบ​เรื่อง​มัน​ไม่​ได้

src/resolver.rs · สถานะ​ทั้งหมด​ที่ resolver ถือ​อยู่
struct Resolver {
/// scope ซ้อนกัน · `true` แปลว่าประกาศเสร็จแล้ว `false` แปลว่ากำลังประกาศอยู่
///
/// scope ระดับ global ไม่อยู่ในนี้โดยตั้งใจ — ตัวแปร global ถูกประกาศตอนไหน
/// ก็ได้ (REPL ประกาศทีละบรรทัด) resolver จึงตอบเรื่องมันไม่ได้ และปล่อยให้
/// interpreter ไล่หาตอนรันแทน
scopes: Vec<HashMap<String, bool>>,
locals: HashMap<usize, usize>,
errors: Vec<ResolveError>,
current_fn: FunctionKind,
current_class: ClassKind,
}

เหตุผล​อยู่​ที่ REPL — ผู้​ใช้​พิมพ์ var a = 1; บรรทัด​หนึ่ง แล้ว​พิมพ์ print a; อีก​บรรทัด​ถัด​มา​ได้ resolver ที่​เห็น​ที​ละ​บรรทัด​จึง​ตัดสิน​เรื่อง​ชื่อ​ระดับ​บน​สุด​ล่วงหน้า​ไม่​ได้​เลย

ผล​ที่​ตาม​มา​ต้อง​พูด​ออก​มาตรงๆ เพราะ​มัน​คือ​ข้อ​แลก — รูปแบบ​เดียวกัน​ที่​ถูก​ปิด​สนิท​เมื่อ​อยู่​ใน block ยัง​เปิด​อยู่​เมื่อ​อยู่​ที่​ระดับ​บน​สุด ชื่อ global ยัง​ถูก​ไล่​หา​ตอน​รัน จึง​ยัง​เห็น​ตัว​ที่​ประกาศ​ทีหลัง​ได้

current_fn กับ current_class เป็น​อีก​หน้าที่​หนึ่ง​ของ​การ​เดิน​รอบ​นี้ มัน​จำ​ว่า​ตอน​นี้​อยู่​ใน​อะไร​อยู่ ซึ่ง​เป็น​สิ่ง​เดียว​ที่​ทำให้​ตอบ​ได้​ว่า return ตัว​นี้​อยู่​นอก function หรือ​เปล่า

หัวข้อ​นี้​แสดง​ว่าการ​เดิน​ต้นไม้​รอบ​เดียวกัน​นั้น​ตรวจ​ลำดับ​การ​ประกาศ​และ​ชื่อ​ซ้ำ​ได้​ด้วย

การ​ประกาศ​ตัวแปร​ถูก​ทำ​เป็น​สอง​จังหวะ ไม่ใช่​จังหวะ​เดียว

src/resolver.rs · ประกาศ แล้ว​ค่อย​นับ​ว่า​ใช้ได้ (ตัด​มา​บาง​ส่วน)
Stmt::Var { name, line, init } => {
// ประกาศ → resolve ค่าเริ่มต้น → ค่อยนับว่าใช้ได้
// ลำดับนี้คือสิ่งที่ทำให้ `var a = a;` เป็น error แทนที่จะเงียบๆ ได้ nil
self.declare(name, *line);
if let Some(e) = init {
self.expr(e);
}
self.define(name);
}

ระหว่าง declare กับ define ชื่อ​นั้น​มี​อยู่​แต่​ยัง​ใช้​ไม่​ได้ ค่า​เริ่มต้น​ที่​อ้าง​ถึง​ตัวเอง​จึง​ถูก​จับ​ได้ แทนที่​จะ​ได้​ค่า​ว่าง​มา​เงียบๆ

จังหวะ​แรก​ยัง​เป็น​ที่​ที่​ชื่อ​ซ้ำ​ถูก​จับ​ด้วย

src/resolver.rs · จังหวะ​แรก​ของ​การ​ประกาศ
fn declare(&mut self, name: &str, line: usize) {
let Some(scope) = self.scopes.last_mut() else {
return; // global — ปล่อยให้ตอนรันจัดการ
};
if scope.contains_key(name) {
self.error(line, &format!("มีชื่อ '{name}' ประกาศไว้ใน scope นี้แล้ว"));
return;
}
scope.insert(name.to_string(), false);
}

ประโยค let … else ที่​หัว​ของ body คือ global ที่​พูด​ถึง​ไป​แล้ว ไม่มี scope ให้​ประกาศ​ลง ก็​เลย​ไม่มี​อะไร​ให้​ตรวจ

นั่น​คือ​เหตุผล​ที่​ประกาศ var ชื่อ​เดิม​ซ้ำ​สอง​ครั้ง​ที่​ระดับ​บน​สุด​ไม่​เป็น error แต่​ทำ​แบบ​เดียวกัน ใน block กลับ​เป็น error — กฎ​เดียวกัน คนละ​ที่ ให้​ผล​คนละ​แบบ

ลอง​รัน​จริง — ทั้ง​ฝั่ง​ที่​ถูก​และ​ฝั่ง​ที่​ถูก​จับ​ได้

หัวข้อ​ที่​มีชื่อ​ว่า “ลอง​รัน​จริง — ทั้ง​ฝั่ง​ที่​ถูก​และ​ฝั่ง​ที่​ถูก​จับ​ได้”

หัวข้อ​นี้​รัน​ตัวอย่าง​ที่​มี​ทั้ง closure ที่​ทำงาน​ถูก​และ error ที่ resolver จับ​ได้ อยู่​ใน file เดียวกัน

examples/07-closures.nok
// บทที่ 7 — closure จับ environment ตอนถูกประกาศ ไม่ใช่ตอนถูกเรียก
fun makeCounter() {
var count = 0;
fun tick() {
count = count + 1;
return count;
}
return tick;
}
var next = makeCounter();
next();
next();
// counter ตัวที่สองมี environment ของตัวเอง ไม่ใช้ count ร่วมกับตัวแรก
var other = makeCounter();
other();
// bug scope ที่คนส่วนใหญ่มองข้าม
// showA ถูกเขียนไว้ก่อน a ตัวใน block จะมีอยู่ มันจึงต้องเห็น a ตัวนอกทั้งสองครั้ง
var a = "global";
{
fun showA() {
print a;
}
showA();
var a = "block";
showA();
}
// การเดินต้นไม้รอบเดียวกันนั้นยังจับ error เชิงโครงสร้างได้ตั้งแต่ก่อนโปรแกรมเริ่มรัน
return 1;
{
var dup = 1;
var dup = 2;
}
this;
print "ตรวจ scope จบแล้ว คำสั่งที่เหลือยังได้รัน";

ตัวอย่าง​นี้​รัน​ด้วย​โหมด --eval ซึ่ง​ป้อน​คำ​สั่ง​เข้า interpreter ที​ละ​คำ​สั่ง​แบบ​เดียว​กับ​ที่ REPL ทำ สอง​อย่าง​ที่​ตาม​มา​จาก​โหมด​นี้​อธิบาย​ผล​ข้าง​ล่าง​ได้​ทั้งหมด

  • ค่า​ของ​คำ​สั่ง​ที่​เป็น​นิพจน์​ระดับ​บน​สุด​ถูก​สะท้อน​กลับ​มาnext() จึง​พิมพ์​ตัวเลข​ออก​มา​ทั้ง​ที่​ไม่มี print
  • คำ​สั่ง​ที่​พัง​ไม่​หยุด​คำ​สั่ง​ถัด​ไป — ตรง​ข้าม​กับ​การ​รัน​ทั้ง file เป็น​โปรแกรม​เดียว ซึ่ง​จะ​หยุด ตั้งแต่​ยัง​ไม่​ได้​รัน​อะไร​เลย​ถ้า​ตรวจ scope ไม่​ผ่าน
expected/07-closures.txt
1
2
1
global
global
[บรรทัด 32] ผิดพลาดตอนตรวจ scope: ใช้ 'return' นอก function ไม่ได้
[บรรทัด 35] ผิดพลาดตอนตรวจ scope: มีชื่อ 'dup' ประกาศไว้ใน scope นี้แล้ว
[บรรทัด 37] ผิดพลาดตอนตรวจ scope: ใช้ 'this' นอก class ไม่ได้
ตรวจ scope จบแล้ว คำสั่งที่เหลือยังได้รัน

สาม​บรรทัด​แรก​คือ closure ที่​ทำงาน​ถูก — counter ตัว​แรก​เดิน​จาก 1 ไป 2 ส่วนตัว​ที่​สอง​เริ่ม​จาก 1 เพราะ​ถือ environment คนละ​ก้อน

สอง​บรรทัด​ถัด​มา​คือ​หัวใจ​ของ​บท​นี้ showA พิมพ์ global ทั้ง​สอง​ครั้ง ทั้ง​ที่​ครั้ง​ที่​สอง​ถูก​เรียก​หลัง​จาก มี​ตัวแปร​ชื่อ a ตัว​ใหม่​ประกาศ​อยู่​ใน block เดียวกัน​แล้ว

สาม​บรรทัด​ที่​เป็น error ไม่​ได้​เกิด​ตอน​รัน มัน​เกิด​ตอน​ตรวจ ข้อความ​บอกไว้ตรงๆ ว่า ผิดพลาดตอนตรวจ scope ซึ่ง​เป็น​คนละ​ด่าน​กับ​ความ​ผิดพลาด​ตอน​รัน​ใน​บท​ที่ 4

บรรทัด​สุดท้าย​ยัง​ได้​รัน แม้​จะ​มี error สาม​ข้อ​มา​ก่อนหน้า สัญญาณ​สำหรับ​เครื่อง​อ่าน​จึง​ไม่​ได้​อยู่​ใน​ผล​ที่​พิมพ์ แต่​อยู่​ที่ exit code

ป้าย provenance ที่​หัว​ของ expected/07-closures.txt บันทึก​ค่า​นั้น​ไว้​เป็น 65 ซึ่ง​แปล​ว่า​ผิดพลาด ตั้งแต่​ก่อน​รัน ไม่ใช่ 70 ที่​แปล​ว่า​ผิดพลาด​ระหว่าง​รัน

หัวข้อ​นี้​บอกว่า Rc กับ closure ทำให้​เกิด reference cycle ได้​จริง และ​ภาษา​นี้​ไม่มี​อะไร​มา​เก็บกวาด

Rc คืน​หน่วย​ความ​จำ​เมื่อ​ตัว​นับ​ผู้​ถือ​ลง​ถึง​ศูนย์ ซึ่ง​แปล​ว่า​ของ​ที่​อ้าง​ถึงกัน​เป็น​วง​จะ​ไม่มี​วัน​ถึง​ศูนย์

และ​รูปแบบ​นั้น​เกิด​ขึ้น​จริง​ใน​ภาษา​นี้ — function ถือ environment ที่​มัน​ถูก​ประกาศ​ไว้ ส่วน environment ก้อน​นั้น​ก็​ถือ function ตัว​นั้น​ไว้​ใน​ฐานะ​ค่า​ของ​ชื่อ​หนึ่ง

นี่​ไม่ใช่​ความ​กังวล​เชิง​ทฤษฎี มัน​ทิ้ง​รอย​ไว้​ใน​ที่​ที่​ไม่มี​ใคร​คาด

src/interpreter.rs · เขียน Debug เอง​เพราะ​วงแหวน​นั้น​มี​อยู่​จริง
/// เขียน `Debug` เองแทนที่จะ derive
///
/// `derive(Debug)` จะไล่พิมพ์ environment ที่อยู่ใน closure ต่อไปเรื่อยๆ และเพราะ
/// closure กับ environment อ้างถึงกันเป็นวงได้ (ดู `environment.rs`) มันจะวนไม่รู้จบ
/// จนกองซ้อนล้น · การพิมพ์แบบเดียวกับที่ผู้ใช้เห็นจึงทั้งปลอดภัยกว่าและมีประโยชน์กว่า
impl fmt::Debug for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.repr())
}
}

ภาษา​จริง​แก้​เรื่อง​นี้​ด้วย garbage collector ที่​ตาม​เก็บ​ของ​ที่​ไม่มี​ใคร​เข้าถึง​ได้​แล้ว ไม่​ว่า​มัน​จะ​อ้าง​ถึงกันเอง​อย่างไร

nok ไม่มี และ​คอร์ส​นี้​ไม่​ทำ เพราะ garbage collector เป็น​เรื่อง​ใหญ่​พอที่​จะ​เป็น​คอร์ส​ของ​ตัวเอง

ผล​ที่​ตาม​มา​อ่าน​ออก​ได้​จาก​รูปร่าง​ของ code ที่​เขียน — ทุก​ครั้ง​ที่​เรียก makeCounter scope ที่​ได้​กับ function ข้าง​ใน​ถือ​กัน​ไว้​เป็น​วง เรียก​ซ้ำ​กี่​ครั้ง หน่วย​ความ​จำ​ก็​ค้าง​ไว้​เท่านั้น​ก้อน จนกว่า​กระบวนการ​จะ​จบ

ทางออก​ครึ่ง​ทาง​ที่ Rust มี​ให้​คือ Weak ซึ่ง​เป็น​ตัว​ชี้​ที่​ไม่​นับ​เป็น​เจ้าของ แต่​การ​เลือก​ว่า เส้น​ไหน​ควร​เป็น Weak ต้อง​รู้​ทิศทาง​ของ​วงแหวน​ล่วงหน้า ซึ่ง​ใน interpreter ทั่วไป​ไม่มี​ใคร​รู้


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Crafting Interpreters — Robert Nystrom, บท​ที่ 11 “Resolving and Binding” (ตรวจ​แล้ว 2026-08-12) — ที่มา​ของ​การ​เดิน​ต้นไม้​รอบ​ที่​สอง​ก่อน​รัน ทั้ง​ตัวอย่าง​ที่​เปิด​รู​ให้​เห็น และ​วิธี​จด​จำนวน​ชั้น​ไว้​ล่วงหน้า ตัว​หนังสือ​เก็บ​คำ​ตอบ​ด้วย​ตัว node เอง ส่วน​คอร์ส​นี้​ใช้ id เป็น​กุญแจ
  • The Rust Programming Language — บท​ที่ 15.6 “Reference Cycles Can Leak Memory” (ตรวจ​แล้ว 2026-08-12) — ที่มา​ของ​ข้อ​แลก​ที่​บท​นี้​พูด​ถึง​ตอน​ท้าย ทั้ง​กลไก​ที่​ทำให้ Rc เก็บ​วงแหวน​ไม่​ได้ และ​บทบาท​ของ Weak ใน​การ​ตัด​วง

เช็กความเข้าใจ — บทที่ 7

ข้อ 1 / 3

ในตัวอย่างของบทนี้ showA พิมพ์คำว่า global ออกมาทั้งสองครั้ง ถ้าย้าย code รูปแบบเดียวกันนั้นออกมาไว้ที่ระดับบนสุดแทนที่จะอยู่ใน block ผลจะเป็นอย่างไร