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

environment — ตัวแปร scope และ​รูปทรง​ที่ ownership บังคับ

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

ตัวแปร​ตัว​แรก​ดูเหมือน​ของ​เล็ก — แค่​หา​ที่​เก็บ​ชื่อ​กับ​ค่า​ไว้​สัก​ที่​หนึ่ง

แต่​นี่​คือ​บท​ที่​ยาก​ที่สุด​ของ​คอร์ส และ​ความ​ยาก​ไม่​ได้​อยู่​ที่​ทฤษฎี​ภาษา มัน​อยู่​ตรง​ที่ Rust จะ​ปฏิเสธ​โครงสร้าง​ที่​ตำรา​ต้นทาง​วาด​ไว้ สอง​ครั้ง​ติด​กัน ด้วย​เหตุผล​คนละ​ข้อ

📦 code ของ​บท​นี้

ทุก block ของ code ใน​บทเรียน​นี้​ถูก​คัด​มา​จาก scripts/rust-interpreter/src/ ทีละ byte และ​มี​เทสต์เทียบ​ไว้ ถ้า​แก้​ใน​บทเรียน​อย่าง​เดียว​โดย​ไม่​แก้​ที่ source เทสต์จะ​แดง​ทันที

ผล​รัน​ทุก​บรรทัด​ที่​บท​นี้​แสดง คัด​มา​จาก expected/05-statements.txt ซึ่ง​เป็น stdout ที่​รัน​จริง

จาก​นิพจน์​ที่​ทิ้ง​ค่า มา​เป็น​คำ​สั่ง​ที่​ทิ้ง​ร่องรอย

หัวข้อ​ที่​มีชื่อ​ว่า “จาก​นิพจน์​ที่​ทิ้ง​ค่า มา​เป็น​คำ​สั่ง​ที่​ทิ้ง​ร่องรอย”

หัวข้อ​นี้​แยก​คำ​สั่ง​ออก​จาก​นิพจน์ ด้วย​เส้น​แบ่ง​ว่า​อะไร​คืน​ค่า​และ​อะไร​เปลี่ยน​สถานะ

นิพจน์​ให้​ค่า​กลับ​มา​แล้ว​จบ ส่วน​คำ​สั่ง​ไม่​ให้​ค่า​อะไร​เลย มัน​มี​ไว้​เปลี่ยน​สถานะ​ของ​โปรแกรม

var greeting = "สวัสดี"; ไม่​ได้​มี​ค่า​เป็น​อะไร​ทั้งนั้น สิ่ง​ที่​มัน​ทำ​คือ​ทำให้​ชื่อ greeting มี​ความหมาย​ตั้งแต่​บรรทัด​นั้น​เป็นต้น​ไป

src/interpreter.rs · คำ​สั่ง​ประกาศ​ตัวแปร และ​คำ​สั่ง block
Stmt::Var { name, init, .. } => {
let value = match init {
Some(e) => self.evaluate(e)?,
None => Value::Nil,
};
self.env.borrow_mut().define(name.clone(), value);
}
Stmt::Block(stmts) => {
let scope = Environment::child(&self.env);
self.execute_block(stmts, scope)?;
}

init เป็น Option เพราะ var x; ที่​ไม่​ให้​ค่า​เริ่มต้น​เป็น​รูป​ที่​ถูกต้อง และ​ตัวแปร​นั้น​ได้ nil ไป

scope ทั้ง​เรื่อง​คือ​สอง​บรรทัด​ใน Stmt::Block — สร้าง​ชั้น​ใหม่ รัน​คำ​สั่ง​ข้าง​ใน​บน​ชั้น​นั้น แล้ว​ทิ้ง​มัน

หัวข้อ​นี้​อธิบาย​กฎ​การ​หา​ชื่อ ว่า​มัน​เริ่ม​จาก​ชั้น​ใน​สุด​แล้ว​ไล่ออก ไม่ใช่​ไล่​เข้า

EnvironmentEnvironmentที่​เก็บ​ว่า​ชื่อ​ไหน​ผูก​กับ​ค่า​อะไร​อยู่​ตอน​นี้ ซ้อน​กัน​เป็น​ชั้น​ตาม block — ชั้น​ใน​มอง​เห็น​ชั้น​นอก แต่​ชั้น​นอก​ไม่​เห็น​ชั้น​ใน คือ​ที่​เก็บ​ว่า​ชื่อ​ไหน​ผูก​กับ​ค่า​อะไร​อยู่​ตอน​นี้ และ​มัน​ไม่​ได้​มี​ก้อน​เดียว แต่ละ block สร้าง​ชั้น​ใหม่​ทับ​ลง​ไป

กฎ​มี​ข้อ​เดียว คือ​ชั้น​ใน​มอง​เห็น​ชั้น​นอก แต่​ชั้น​นอก​ไม่มี​ทาง​มอง​เห็น​ชื่อ​ที่​เกิด​ใน​ชั้น​ใน

flowchart TD
    L["หาชื่อหนึ่งชื่อ · เริ่มจากชั้นในสุดเสมอ"] --> B["scope ของ block · มี greeting ของตัวเอง"]
    B -->|"ไม่เจอ จึงไล่ตาม enclosing ออกไป"| G["global · มี greeting กับ count"]
    G -->|"ไม่เจอที่นี่อีก"| X["ความผิดพลาดตอนรัน · ไม่รู้จักชื่อนั้น"]

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

นี่​คือ Lexical ScopeLexical Scopeกฎ​ที่​ตัดสิน​ว่า​ชื่อ​หนึ่ง​หมาย​ถึง​ตัวแปร​ตัว​ไหน โดย​ดู​จาก **ตำแหน่ง​ใน code** ไม่ใช่​จาก​ลำดับ​การ​เรียก​ตอน​รัน — ชื่อ​หนึ่ง​หมาย​ถึง​ตัวแปร​ตัว​ไหน ตัดสิน​จาก​ตำแหน่ง​ใน code ที่​เขียน​ไว้ ไม่ใช่​จาก​ลำดับ​การ​เรียก​ตอน​รัน

หัวข้อ​นี้​เดิน​ตาม​ทาง​ที่​ตรง​ที่สุด​ก่อน แล้ว​แสดง​ว่า​มัน​ถูก​ปฏิเสธ​ที่ไหน​และ​ด้วย​เหตุผล​อะไร

ตำรา​ต้นทาง​เขียน​ด้วย Java และ​ใน​นั้น​ชั้น​ที่​ครอบ​อยู่​คือ​ฟิลด์ธรรมดา​หนึ่ง​ฟิลด์ ทำนอง​ว่า Environment enclosing; แล้ว​จบ

Java ให้​ทุก​อย่าง​เป็น reference ที่​ชี้​ร่วม​กัน​ได้​อยู่​แล้ว และ​มี garbage collector คอย​เก็บกวาด​ข้าง​หลัง ประโยค​เดียว​นั้น​จึง​พอ

Rust ไม่​ให้​อะไร​ฟรี​สัก​อย่าง​ใน​สอง​ข้อ​นั้น

หัวข้อ​ย่อย​นี้​แสดง​ว่า​ทำไม Box ซึ่ง​เป็น​ทาง​ที่​ตรง​ที่สุด ถึง​ใช้​ไม่​ได้​ตั้งแต่​ต้น

ทาง​ที่​ตรง​ที่สุด​คือ​ให้​ชั้น​ลูก​เป็น​เจ้าของ​ชั้น​แม่​ผ่าน Option<Box<Environment>> ซึ่ง​อ่าน​แล้ว​เข้าใจ​ทันที

ปัญหา​โผล่​ตอน​ที่ environment ก้อน​เดียว​ต้อง​ถูก​ถือ​โดย​สอง​ที่​พร้อม​กัน — scope ลูก​ถือ​มัน​ไว้​ใน​ฐานะ​ชั้น​แม่ และ closure ใน​บท​ที่ 7 ก็​ถือ​มัน​ไว้​ใน​ฐานะ​ที่​ที่​มัน​เกิด

Box มี​เจ้าของ​ได้​คน​เดียว การ​ส่ง​มัน​ให้​ที่​ที่​สอง​จึง​เป็นการ​ย้าย​เจ้าของ ไม่ใช่​การ​แบ่ง​กัน คอมไพเลอร์​ปฏิเสธ​ด้วย E0382 คือ​ใช้​ค่าที่​ถูก​ย้าย​ไป​แล้ว

นี่​ไม่ใช่​ความ​จู้จี้​ของ​ภาษา มัน​คือ​คำถาม​ที่ Java ไม่​เคย​ต้อง​ตอบ​เพราะ garbage collector ตอบ​ให้​เงียบๆ อยู่​แล้ว — ใคร​เป็น​คน​คืน​หน่วย​ความ​จำ​ก้อน​นี้

หัวข้อ​ย่อย​นี้​แสดง​ว่า Rc แก้​ปัญหา​เจ้าของ​หลาย​คน​ได้ แต่​เปิด​ปัญหา​ใหม่​ทันที

คำ​ตอบ​ของ “เจ้าของ​หลาย​คน” คือ Rc ซึ่ง​นับ​จำนวน​ผู้​ถือ และ​คืน​หน่วย​ความ​จำ​เมื่อ​ตัว​นับ​ถึง​ศูนย์

แต่ Rc ให้​สิทธิ์​แค่​อ่าน เพราะ​ถ้า​มี​คน​ถือ​อยู่​หลาย​คน​แล้ว​ยัง​แก้​ได้​อีก กฎ​การ​ยืม​ของ Rust ก็​ล่ม​ทันที

count = count + 1; ต้อง​แก้​ค่า​ใน environment ที่​มี​ผู้​ถือ​อยู่​หลาย​คน เขียนตรงๆ แล้ว​คอมไพเลอร์ ปฏิเสธ​ด้วย E0596 คือ​ยืม​ของ​ที่​อยู่​ใน Rc มา​แก้​ไม่​ได้

ทางออก​ที่​เหลือ​อยู่​ทาง​เดียว​คือ Interior MutabilityInterior Mutabilityรูปแบบ​ของ Rust ที่​ยอม​ให้​แก้​ค่า​ข้าง​ใน​ได้​แม้​ถือ​อยู่​แบบ​อ่าน​อย่าง​เดียว โดย​ย้าย​การ​ตรวจ​กฎ​การ​ยืม​ไป​ตอน​รัน — `RefCell` คือ​ตัว​ที่​คอร์ส​นี้​ใช้​กับ environment — ยอม​ให้​แก้​ข้าง​ใน​ได้​แม้​ถือ​แบบ​อ่าน​อย่าง​เดียว โดย​ย้าย​การ​ตรวจ​กฎ​การ​ยืม​จาก​ตอน​คอมไพล์​ไป​ตอน​รัน

หัวข้อ​นี้​แสดง​รูปทรง​ที่​เหลือ​อยู่​หลัง​จาก​ตัดทาง​ที่​เขียน​ไม่​ได้​ออก​ไป​หมด​แล้ว

src/environment.rs · รูปทรง​ของ environment
/// ชื่อย่อที่ใช้ทั้งโปรเจกต์ · เขียน `Rc<RefCell<Environment>>` ซ้ำทุกที่แล้วอ่านยาก
pub type EnvRef = Rc<RefCell<Environment>>;
pub struct Environment {
values: HashMap<String, Value>,
/// scope ที่ครอบอยู่ · `None` แปลว่านี่คือ scope นอกสุด (global)
enclosing: Option<EnvRef>,
}

อ่าน​ชนิด​นี้​จาก​นอก​เข้า​ใน แล้ว​มัน​เล่า​เหตุผล​ของ​ตัวเอง — Rc ตอบ​ข้อ​แรก คือ​มี​เจ้าของ​หลาย​คน​ได้ RefCell ตอบ​ข้อ​สอง คือ​แก้​ได้​ทั้ง​ที่​ถูก​ถือ​ร่วม​กัน

enclosing เป็น Option เพราะ​ชั้น global ไม่มี​ชั้น​ที่​ครอบ​มัน​อยู่ ค่า None จึง​เป็น​จุด​สิ้นสุด ของ​การ​ไล่ออก ไม่ใช่​กรณี​พิเศษ​ที่​ต้อง​ดัก​แยก

src/environment.rs · สร้าง​ชั้น​ใหม่​ที่​ชี้​กลับ​ไป​หา​ชั้น​แม่
pub fn child(parent: &EnvRef) -> EnvRef {
Rc::new(RefCell::new(Environment {
values: HashMap::new(),
enclosing: Some(Rc::clone(parent)),
}))
}

Rc::clone(parent) คือ​บรรทัด​ที่​ผู้​ถือ​คน​ที่​สอง​เกิด​ขึ้น​จริง มัน​ไม่​ได้​คัด​ลอก environment มัน​เพิ่ม​ตัว​นับ​แล้ว​ชี้​ไป​ที่​ก้อน​เดิม

ชื่อ clone ทำให้​คน​อ่าน​เผลอ​คิด​ว่า​มี​การ​คัด​ลอก​ของ ซึ่ง​เป็น​กับดัก​ของ​ชื่อ ไม่ใช่​ของ​กลไก

หัวข้อ​นี้​แสดง​การ​ไล่​หา​ชื่อ​ออก​ไป​ข้าง​นอก และ​เหตุผล​ที่​มัน​เขียน​แบบ​นี้​แทนที่​จะ​รับ &self

src/environment.rs · หา​ชื่อ​จาก​ชั้น​ใน​ไล่ออก​ไป​ข้าง​นอก
/// หาชื่อจาก scope นี้ไล่ออกไปข้างนอกจนถึง global
///
/// รับ `&EnvRef` ไม่ใช่ `&self` เพราะต้องเดินขึ้นไปหา scope แม่ ซึ่งอยู่หลัง `Rc`
/// อีกชั้น · เขียนเป็น associated function ทำให้การยืม `RefCell` แต่ละชั้น
/// เริ่มและจบภายใน block เดียว ไม่มีการยืมค้างข้ามชั้นให้ panic ตอนรัน
pub fn get(env: &EnvRef, name: &str) -> Option<Value> {
let parent = {
let scope = env.borrow();
if let Some(v) = scope.values.get(name) {
return Some(v.clone());
}
scope.enclosing.clone()?
};
Environment::get(&parent, name)
}

รูปแบบ let parent = { … }; ไม่ใช่​การ​จัด​หน้า​ให้​สวย มัน​คือ​กลไก — การ​ยืม RefCell ของ​ชั้น​นี้ เกิด​และ​ตาย​ภายใน​วงเล็บ​ปีกกา​นั้น ก่อน​ที่​การ​เรียก​ซ้ำ​จะ​เริ่ม​ยืม​ชั้น​ถัด​ไป

ถ้า​ปล่อย​ให้การ​ยืม​ค้าง​ไว้​แล้ว​เรียก​ซ้ำ​ทั้ง​อย่าง​นั้น คอมไพเลอร์​จะ​ไม่​ทัก​เลย เพราะ RefCell ย้าย​การ​ตรวจ​ไป​ตอน​รัน​แล้ว

มัน​จะ​รอด​ตราบ​ใด​ที่​ชั้น​ที่​ไล่​ไป​เจอ​ไม่ใช่​ชั้น​เดิม ซึ่ง​เป็น​เงื่อนไข​ที่​จริง​อยู่​ตอน​นี้ แต่​ไม่มี code บรรทัด​ไหน​ค้ำ​ไว้ · การ​เขียน​ให้การ​ยืม​จบ​ก่อน​เรียก​ซ้ำ แปล​ว่า​ไม่​ต้อง​พึ่ง​เงื่อนไข​นั้น​เลย

src/environment.rs · กำหนด​ค่า​ให้​ชื่อ​ที่​มี​อยู่​แล้ว​เท่านั้น
/// กำหนดค่าให้ชื่อที่ *มีอยู่แล้ว* · คืน `false` ถ้าไม่เจอชื่อนั้นเลย
///
/// ต่างจาก `define` ตรงที่มันไม่สร้างชื่อใหม่ — `x = 1` โดยไม่เคย `var x` คือ error
pub fn assign(env: &EnvRef, name: &str, value: Value) -> bool {
let parent = {
let mut scope = env.borrow_mut();
if scope.values.contains_key(name) {
scope.values.insert(name.to_string(), value);
return true;
}
match &scope.enclosing {
Some(p) => Rc::clone(p),
None => return false,
}
};
Environment::assign(&parent, name, value)
}

ความ​ต่าง​ระหว่าง define กับ assign คือ​เส้น​แบ่ง​ของ​ภาษา ไม่ใช่​รายละเอียด​ของ​การ​เขียน

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

ผล​ของ​เส้น​แบ่ง​นี้​คือ count = count + 10; ที่​อยู่​ใน block ไม่​ได้​สร้าง count ตัว​ใหม่​ใน​ชั้น​นั้น มัน​เดิน​ออก​ไป​แก้ตัว​ข้าง​นอก และ​ค่า​นั้น​ยัง​อยู่​หลัง block จบ

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

หัวข้อ​นี้​แสดง​ว่าการ​คืน environment เดิม​ต้อง​เกิด​ขึ้น​แม้​ตอน​ที่​คำ​สั่ง​ข้าง​ใน​พัง

src/interpreter.rs · รัน​คำ​สั่ง​ชุด​หนึ่ง​แล้ว​คืน environment เดิม​เสมอ
/// รันคำสั่งชุดหนึ่งใน environment ที่กำหนด แล้ว *คืน* environment เดิมเสมอ
///
/// จุดที่ต้องระวัง: ถ้าออกจากตรงนี้ตอน `Err` โดยไม่คืนค่า `self.env`
/// scope จะรั่วออกไปนอก block · Java ใช้ `try { … } finally { … }`
/// ที่นี่เก็บ error ไว้ในตัวแปรก่อน คืน environment แล้วค่อยส่ง error ออกไป
pub fn execute_block(&mut self, stmts: &[Stmt], scope: EnvRef) -> Result<(), Flow> {
let previous = std::mem::replace(&mut self.env, scope);
let mut outcome = Ok(());
for s in stmts {
if let Err(flow) = self.execute(s) {
outcome = Err(flow);
break;
}
}
self.env = previous;
outcome
}

สังเกต​ว่าที่​นี่​ไม่มี ? สัก​ตัว ทั้ง​ที่​บท​ที่​แล้ว​ใช้​มัน​ทุก​บรรทัด

เหตุผล​คือ ? จะ​ออก​จาก function ทันที และ​การ​ออก​ทันที​ตรง​นี้​แปล​ว่า self.env ค้าง​อยู่​ที่ scope ของ block ที่​พัง​ไป​แล้ว คำ​สั่ง​ถัด​ไป​ทั้งหมด​จะ​ทำงาน​บน​ชั้น​ที่​ควร​ตาย​ไป​แล้ว

error จึง​ถูก​เก็บ​ใส่ outcome ไว้​ก่อน แล้ว​คืน environment เดิม แล้ว​ค่อย​ส่ง error ออก​ไป เป็น​ลำดับ​เดียว​กับ​ที่ finally ของ Java รับประกัน​ให้

หัวข้อ​นี้​แสดง​ผล​รัน​จริง​ของ script ที่​มี​ทั้ง​การ​บัง​ชื่อ​และ​การ​แก้​ค่า​ข้าม​ชั้น

examples/05-statements.nok
// บทที่ 5 — ตัวแปร · block scope · และ environment ที่ซ้อนกันเป็นชั้น
var greeting = "สวัสดี";
print greeting;
var count = 1;
count = count + 1;
print count;
{
// ชื่อเดียวกัน แต่เป็นคนละตัวแปร — ตัวนี้เกิดใน scope นี้แล้วตายไปพร้อมกัน
var greeting = "อยู่ใน block";
print greeting;
// ส่วนตัวนี้ไม่ได้ประกาศใหม่ มันจึงเดินออกไปหา count ตัวข้างนอก
count = count + 10;
}
print greeting;
print count;

บท​นี้​รัน script ตรงๆ ไม่​ต้อง​พึ่ง​โหมด --eval แบบ​บท​ที่​แล้ว เพราะ​ตอน​นี้​ภาษา​มี​คำ​สั่ง print แล้ว

expected/05-statements.txt
สวัสดี
2
อยู่ใน block
สวัสดี
12

ผล​นี้​ตอบ​สอง​คำถาม​ที่​ต่าง​กัน​คนละ​ขั้ว ทั้ง​ที่ script ใช้​ท่า​ที่​หน้าตา​คล้าย​กัน​มาก

  • greeting กลับ​มา​เป็น สวัสดี หลัง block จบ เพราะ var ข้าง​ใน​สร้าง​ตัว​ใหม่​ใน​ชั้น​ใหม่ ตัว​ข้าง​นอก​ไม่​เคย​ถูก​แตะ​เลย มัน​แค่​ถูก​บัง​ไว้​ชั่วคราว
  • count กลาย​เป็น 12 และ​ค่า​นั้น​อยู่​ต่อ เพราะ​การ​กำหนด​ค่า​โดย​ไม่มี var ไม่​สร้าง​ชื่อ​ใหม่ มัน​เดิน​ออก​ไป​หา​ตัว​เดิม​แล้ว​แก้​ที่​นั่น
  • print ไม่มี​เครื่องหมาย​คำ​พูด​รอบ​ข้อความ ต่าง​จาก​ค่าที่ REPL สะท้อน​กลับ​ใน​บท​ที่​แล้ว เพราะ​การ​พิมพ์​ออก​จอ​เป็น​ของ​ผู้​ใช้ ส่วน​การ​สะท้อน​ค่า​เป็น​ของ​คน​ที่​กำลัง​สำรวจ​ภาษา
คำ​เดียว​ที่​ต่าง​กัน​คือ var

สอง​บรรทัด​ที่​ทำให้​ผลออก​มา​คนละ​แบบ ต่าง​กัน​แค่​คำ​ว่า var คำ​เดียว

นี่​คือ​ราคา​ของ​ภาษา​ที่​แยก define ออก​จาก assign — เส้น​แบ่ง​ชัด​สำหรับ​คน​ที่​รู้ แต่​มอง​ไม่​เห็น สำหรับ​คน​ที่​ยัง​ไม่รู้ และ​ไม่มี error ไหน​มา​เตือน​เลย​เพราะ​ทั้ง​สอง​แบบ​ถูกต้อง​ทั้ง​คู่

หัวข้อ​นี้​บอก​สิ่ง​ที่ทาง​เลือก​นี้​แลก​ไป ทั้ง​ข้อ​ที่​โผล่​ตอน​รัน​และ​ข้อ​ที่​ไม่​โผล่​เลย

ราคา​ข้อ​แรก​คือ​กฎ​การ​ยืม​ยัง​อยู่​ครบ​ทุก​ข้อ แค่​ย้าย​เวลา​ตรวจ​ไป​ตอน​รัน borrow_mut ที่​ทับ​กับ borrow ที่​ยัง​ไม่​หมด​อายุ ทำให้​โปรแกรม panic ทันที

ความ​ผิดพลาด​ที่​คอมไพเลอร์​เคย​จับ​ให้​ฟรี จึง​กลาย​เป็น​ความ​ผิดพลาด​ที่​ต้อง​รัน​ถึง​จะ​เจอ และ​เจอ​เฉพาะ​เส้นทาง​ที่​ถูกรัน​จริง​เท่านั้น

นี่​คือ​เหตุผล​ที่ get กับ assign เขียน​ให้การ​ยืม​แต่ละ​ชั้น​เริ่ม​และ​จบ​ใน​วงเล็บ​ปีกกา​เดียว มัน​เป็น​วินัย​ที่​ต้อง​รักษา​ด้วย​มือ เพราะ​ไม่มี​ด่าน​ไหน​บังคับ​ให้​แล้ว

ราคา​ข้อ​ที่​สอง​ไม่​โผล่​ให้​เห็น​เลย — Rc เก็บ​วงแหวน​ไม่​เป็น

function ที่​ถูก​ประกาศ​ใน environment แล้ว​เก็บ environment นั้น​ไว้​ใน​ตัวเอง ทำให้​สอง​ก้อน​ชี้​หา​กัน​เป็น​วง ตัว​นับ​ของ​ทั้ง​คู่​จึง​ไม่มี​วัน​ถึง​ศูนย์ และ​หน่วย​ความ​จำ​ก้อน​นั้น​ไม่​ถูก​คืน​จนกว่า​โปรแกรม​จะ​จบ

🔁 ข้อ​แลก​นี้​ถูกรับ​ไว้​โดย​ตั้งใจ

คอร์ส​นี้​ไม่มี garbage collector และ​ไม่​เติม Weak เข้า​มา​แก้​วงแหวน เพราะ​การ​ทำ​แบบ​นั้น ต้อง​ตัดสิน​ใจ​ให้​ได้​ก่อน​ว่า​ทิศ​ไหน​คือ​ทิศ​ที่​ไม่​ควร​ถือ​ความ​เป็น​เจ้าของ ซึ่ง​เป็น​หัวข้อ​ของ​คอร์ส​อื่น

สิ่ง​ที่​คอร์ส​นี้​ทำได้​คือ​พูด​ออก​มา​ให้​ครบ ว่า​มัน​เป็น​ข้อ​จำกัด​ที่​รู้​อยู่ ไม่ใช่​จุด​ที่​มอง​ข้าม บท​ที่ 7 จะ​พา​ไป​เจอ​กรณี​ที่​วงแหวน​นี้​เกิด​ขึ้น​จริง ตอน​ที่ closure จับ environment ของ​ตัวเอง​ไว้

หัวข้อ​นี้​ชี้​ช่องว่าง​ที่​เหลือ​หลัง​จบ​บท​นี้ ซึ่ง​เป็น​งาน​ของ​บท​ถัด​ไป

ภาษา​ตอน​นี้​จำ​ค่า​ไว้​ได้ มี scope ที่​ซ้อน​กัน และ​แยก​การ​ประกาศ​ออก​จาก​การ​กำหนด​ค่า​ได้​แล้ว

สิ่ง​ที่​ยัง​ไม่มี​คือ​การ​เลือก​ทาง​และ​การวน​ซ้ำ ทุก​คำ​สั่ง​ยัง​ถูกรัน​เรียง​กัน​จาก​บน​ลง​ล่าง​เสมอ ไม่มี​ทางข้าม​หรือ​ย้อน​กลับ

บท​ที่ 6 เติม if while และ​การ​เรียก function เข้าไป และ​โครง environment ที่​สร้าง​ใน​บท​นี้ คือ​สิ่ง​ที่​ทำให้การ​เรียก function มี​ที่​เก็บ​ของ​ตัวเอง​ได้​โดย​ไม่​ต้อง​เพิ่ม​กลไก​ใหม่


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Crafting Interpreters — Robert Nystrom, บท​ที่ 8 “Statements and State” (ตรวจ​แล้ว 2026-08-12) — โครง​ของ environment ที่​ซ้อน​กัน​เป็น​ชั้น การ​แยก define ออก​จาก assign และ​การ​คืน scope เดิม​หลัง​จบ block ตัว code ใน​ตำรา​เขียน​ด้วย Java ซึ่ง​ไม่​ต้อง​ตอบ คำถาม​เรื่อง​ความ​เป็น​เจ้าของ​เลย จึง​ไม่มี​เนื้อหา​ส่วน​ที่​บท​นี้​ใช้​เวลา​ไป​มาก​ที่สุด
  • The Rust Programming Language — บท​ที่ 15.4 “Rc<T>, the Reference-Counted Smart Pointer” (ตรวจ​แล้ว 2026-08-12) — ที่มา​ของ​การ​มี​เจ้าของ​หลาย​คน และ​เหตุผล​ที่ Rc ให้​สิทธิ์​แค่​อ่าน คือ​ถ้า​ให้​แก้​ได้​ด้วย​ทั้ง​ที่​มี​ผู้​ถือ​หลาย​คน กฎ​การ​ยืม​ก็​ไม่​เหลือ​อะไร​ให้​บังคับ
  • The Rust Programming Language — บท​ที่ 15.5 “RefCell<T> and the Interior Mutability Pattern” (ตรวจ​แล้ว 2026-08-12) — ที่มา​ของ​การ​ย้าย​การ​ตรวจ​กฎ​การ​ยืม​ไป​ตอน​รัน หน้า​นี้​บอกตรงๆ ว่า การ​ละเมิด​กฎ​กลาย​เป็น panic แทนที่​จะ​เป็น​ความ​ผิดพลาด​ตอน​คอมไพล์
  • The Rust Programming Language — บท​ที่ 15.6 “Reference Cycles Can Leak Memory” (ตรวจ​แล้ว 2026-08-12) — ที่มา​ของ​ราคา​ข้อ​ที่​สอง คือ​ตัว​นับ​ของ​ก้อน​ที่​ชี้​หา​กัน​เป็น​วง​ไม่มี​วัน​ถึง​ศูนย์ พร้อม​ทาง​แก้​ด้วย Weak ที่​คอร์ส​นี้​เลือก​ไม่​ใช้
  • Rust error index — E0596 (ตรวจ​แล้ว 2026-08-12) — รหัส​ของ​การ​ปฏิเสธ​ที่​ได้​จริง​เมื่อ​พยายาม​ยืม​ของ​ที่​ถือ​อยู่​แบบ​อ่าน​อย่าง​เดียว มา​แก้ ซึ่ง​เป็น​สิ่ง​ที่​เกิด​ขึ้น​เมื่อ​ใช้ Rc โดย​ไม่มี RefCell

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

ข้อ 1 / 3

ใน script ของบทนี้ ชื่อ greeting กลับมาเป็นค่าเดิมหลังจบ block ส่วน count ไม่กลับ ทั้งที่ทั้งคู่ถูกเขียนถึงใน block เดียวกัน อะไรคือสาเหตุ