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

Ownership, การ​ยืม และ​การ​ย้าย — model หลัก​ของ Rust

ใน​โลก C#/.NET คุณ​เขียน var b = a; บน object แล้ว​ได้ 2 reference ชี้​ก้อน​เดียวกัน — ทั้ง a และ b ใช้​ต่อ​ได้ ใคร​แก้​ก็​เห็น​ทั้ง​คู่ และ GC จะ​คืน​หน่วย​ความ​จำ​ให้​เอง​ตอน​ไหน​ก็​ไม่รู้ Rust มอง​เรื่อง​นี้​ต่าง​ออก​ไป​ตั้งแต่​ราก: ทุก​ค่า​มี เจ้าของ​เดียว และ​คำ​สั่ง assign ที่​หน้าตา​เหมือนกันเป๊ะ​กลับ​มี​ความหมาย​ไม่​เหมือน​กัน นี่​คือ ownershipownershipกฎ​เจ้าของ​เดี่ยว: ทุก​ค่า​มี​เจ้าของ​หนึ่ง​เดียว ค่า​ถูก drop เมื่อ​เจ้าของ​หมด scope — วิธี​คืน​หน่วย​ความ​จำ​แบบ​ไม่มี GC — ไอเดีย​เดียว​ที่​ยาก​จริง​ของ Rust และ​เป็น​ฐาน​ที่​ทุก​อย่าง​ใน​คอร์ส​นี้​ตั้ง​อยู่​บน​มัน อย่า​รีบ​ผ่าน​บท​นี้

บท​ที่​แล้ว​เรา​สัญญา​ว่า compiler คือ​ครู บท​นี้​คือ​คาบ​เรียน​แรก​ที่​ครู​จะ​เริ่ม​โยน error กลับ​มา — และ​ทุก error ที่​เห็น​ใน​บท​นี้ (E0382, E0499, E0502, E0106) คือ bug หน่วย​ความ​จำ​ทั้ง​ชนชั้น​ที่​ถูกกัน​ไว้​ตั้งแต่​ตอน compile ไม่ใช่​ตอน production เรา​จะ​เดิน​แบบ red-then-green ตลอด: ดู code ที่ compile ไม่​ผ่าน​ก่อน เข้าใจ​ว่า​มัน​กัน​อะไร แล้ว​ค่อย​แก้​ให้​ผ่าน

📦 kaen-taskcli

code ลงมือ​ของ​คอร์ส​นี้​อยู่​ใน repo kaen-taskcli (code ตัวอย่าง​กำลัง​จัด​ทำ) — CLI ตัว​จัดการ​งาน to-do ที่​เก็บ​สถานะ​เป็น JSON ที่​เรา​ไล่​สร้าง​ยาว 8 บท ตัวอย่าง​ใน​บท​นี้​ยัง​เป็น drill สั้นๆ ที่​โฟกัส​กลไก ownership ล้วนๆ — ชนิด​ข้อมูล​จริง​ของ taskcli (Task, Command) จะ model ในบท3 แต่​กฎ​ที่​ฝึก​ใน​บท​นี้​จะ​ติดตัว​ไป​ใช้​ทุก​บรรทัด​หลัง​จาก​นี้ ทุก snippet ผ่าน cargo check บน rustc 1.97.1 (edition 2024) โดย​ไม่มี warning

หนังสือ The Rust Programming Language (ch4.1) วาง​กฎ​ไว้​สาม​ข้อสั้นๆ ที่​คุ้ม​ค่า​จะ​จำ​ขึ้นใจ:

  1. ทุก​ค่า​มี เจ้าของ (owner) หนึ่ง​ตัว
  2. มี​เจ้าของ​ได้​ครั้ง​ละ หนึ่ง​เดียว เท่านั้น
  3. เมื่อ​เจ้าของ​หมด scope ค่า​นั้น​จะ​ถูก drop (คืน​หน่วย​ความ​จำ) ทันที

ข้อ​สาม​คือ​หัวใจ: การ​คืน​หน่วย​ความ​จำ​เกิด​ขึ้น แน่นอน​และ​คาด​เดา​ได้ ณ จุด​ที่​เจ้าของ​หมด scope — ไม่ใช่ “ตอน​ไหน​ก็ได้​ที่ GC ว่าง” นี่​คือ RAII แบบ​เดียว​กับ using/IDisposable ใน C# แต่ อัตโนมัติ​และ​ครอบ​ทุก​อย่าง ไม่ใช่​แค่​ของ​ที่​คุณ implement IDisposable ไว้​เอง ผล​คือ​ไม่มี GC pause ไม่มี finalizer สุ่ม​จังหวะ — แลก​กับ​การ​ที่​คุณ​ต้อง ตัดสิน​ใจ​ให้​ชัด​ว่า​ใคร​เป็น​เจ้าของ​อะไร

นี่​คือ​จุด​ที่​สัญชาตญาณ​จาก C# จะ​สะดุด​ครั้ง​แรก ดู code ที่ compile ไม่​ผ่าน:

fn main() {
let s1 = String::from("hello");
let s2 = s1; // ❌ move: ความเป็นเจ้าของย้ายจาก s1 ไป s2
println!("s1 = {s1}, s2 = {s2}"); // ใช้ s1 ต่อไม่ได้แล้ว
}
error[E0382]: borrow of moved value: `s1`

String เก็บ​ข้อมูล​จริง​ไว้​บน heap และ​เก็บ​ส่วน​หัว (pointer, length, capacity) ไว้​บน stack เมื่อ​เขียน let s2 = s1; Rust ไม่ ได้ copy buffer บน heap และ ไม่ ได้​สร้าง reference ตัว​ที่​สอง — มัน​คัด​ลอก​เฉพาะ​ส่วน​หัว​บน stack แล้ว ทำให้ s1 ตาย​ไป นี่​คือ movemoveการ​ย้าย​ความ​เป็น​เจ้าของ; ตัวแปร​เดิม​ใช้​ต่อ​ไม่​ได้ (ต่าง​จาก​การ copy อ้างอิง​ใน C#) เหตุผล​ตรง​ไป​ตรง​มา: ถ้า​ปล่อย​ให้​ทั้ง s1 และ s2 เป็น​เจ้าของ buffer เดียวกัน พอ​ทั้ง​คู่​หมด scope Rust จะ​พยายาม free buffer เดิม สอง​ครั้ง — นั่น​คือ double-free bug ความ​ปลอดภัย​คลาสสิก​ที่ E0382 กัน​ให้​เรา​ตั้งแต่ compile

flowchart TB
  subgraph CS["C#: var s2 = s1 — alias 2 ref ชี้ก้อนเดียว"]
    direction TB
    CS1["s1 (reference)"] --> HEAP1["heap: hello"]
    CS2["s2 (reference)"] --> HEAP1
  end
  subgraph RS["Rust: let s2 = s1 — move เจ้าของเดียว"]
    direction TB
    RS1["s1 ❌ moved ใช้ต่อไม่ได้"]
    RS2["s2 (owner)"] --> HEAP2["heap: hello"]
  end

  classDef live fill:#fde68a,stroke:#92400e,color:#451a03;
  classDef dead fill:#e5e7eb,stroke:#9ca3af,color:#6b7280;
  classDef mem fill:#fed7aa,stroke:#7c2d12,color:#431407;
  class CS1,CS2,RS2 live;
  class RS1 dead;
  class HEAP1,HEAP2 mem;

คำ​บรรยาย​ภาพ: ย้าย (move) ไม่ใช่ copy อ้างอิง — ใน C# หลัง var s2 = s1; มี2 reference ชี้ heap ก้อน​เดียว แต่​ใน Rust หลัง let s2 = s1; เหลือ​เจ้าของ​เดียว (s2) ส่วน s1 ถูก​ทำให้​ใช้​ต่อ​ไม่​ได้ กัน double-free ตั้งแต่ compile

ถ้า​คุณ อยาก ได้​สอง​ก้อน​ที่​แยก​กันจริงๆ สั่ง .clone() ให้ deep-copy buffer บน heap:

fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // deep-copy: s1 ได้ buffer ของตัวเอง s2 ได้อีก buffer
println!("s1 = {s1}, s2 = {s2}");
}

clone() ผ่าน​ฉลุย แต่​เรียก​มัน​ว่า​แบบ กำปั้น​ทุบ​ดิน เพราะ​มัน​แก้​ปัญหา​ด้วย​การ​จ่าย​ค่า allocation + copy เต็ม​ก้อน​ทุก​ครั้ง version ดิบ ❌ ที่​โปรย​แต่ .clone() ทุก​ที่​ที่ compiler บ่น​จะ​รัน​ได้​ก็​จริง แต่​คุณ​กำลัง​จ่าย​ราคา​หน่วย​ความ​จำ​และ​ความเร็ว​โดย​ไม่​จำเป็น — บ่อย​ครั้ง​สิ่ง​ที่​คุณ​ต้อง​การจริงๆ ไม่ใช่ “ก้อน​ที่​สอง” แต่​คือ​แค่ “ขอ ดู ก้อน​เดิม​หน่อย” ซึ่ง​นำ​เรา​ไป​สู่​การ​ยืม

fn calculate_length(s: &String) -> usize {
s.len()
}
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // ยืม s1 ไม่ได้ย้าย
println!("The length of '{s1}' is {len}."); // s1 ยังใช้ได้
}

&s1 ส่ง borrowingborrowingการ​ยืม​ค่า​ด้วย `&` (อ่าน) หรือ `&mut` (แก้ไข) โดย​ไม่​ย้าย​ความ​เป็น​เจ้าของ (การ​ยืม) เข้าไป​ใน function แทนที่​จะ​ย้าย​ความ​เป็น​เจ้าของ function ได้ ดู s1 แต่​ไม่​ได้​เป็น​เจ้าของ​มัน พอ function จบ ไม่มี​อะไร​ถูก drop (เพราะ​ไม่​ได้​เป็น​เจ้าของ) และ s1 กลับ​มา​ใช้ได้​ต่อ​ใน​บรรทัด​ถัด​ไป นี่​คือ​ทาง​เลือก​ที่​ถูกต้อง​เกือบ​ตลอด​เวลา

แล้ว​ทำไม code นี้​ถึง​ไม่ error ทั้ง​ที่​หน้าตา​เหมือน move เป๊ะ?

fn main() {
let x = 5;
let y = x; // i32 เป็น Copy — copy ไม่ move
println!("x = {x}, y = {y}"); // x ยังใช้ได้สบาย
}

เพราะ i32 implement trait ชื่อ CopyCopytrait ของ​ชนิด​ที่​อยู่​บน stack ล้วน (int, bool, char, float) — assign แล้ว copy ไม่​ย้าย ชนิด​ที่​เป็น Copy คือ​ชนิด​ที่​อยู่ บน stack ล้วน ไม่มี​ส่วน heap ให้​ต้อง free — i32, bool, char, ชนิด float, และ tuple/array ที่​สมาชิก​ทุก​ตัว​เป็น Copy การ assign จึง​แค่​คัด​ลอก bit บน stack ทั้ง​ก้อน ต้นฉบับ​ยัง​ใช้ได้ ไม่มี​ประเด็น double-free ให้​กังวล นี่​คือ​เส้น​แบ่ง​เดียว​กับ value type ใน C# (int, struct) ที่ copy ตอน assign — ต่าง​กัน​ตรง Rust ต้อง opt-in และ​ชนิด​จะ​เป็น Copy ได้​ก็​ต่อ​เมื่อ​ไม่มี field ไหน​ที่​ต้อง Drop (เช่น​ถ้า​มี String อยู่​ข้าง​ใน​ก็​เป็น Copy ไม่​ได้)

การ​ยืม​มี​สอง​แบบ และ​ความ​ต่าง​คือ​ทั้งหมด​ของ​เรื่อง:

  • &sshared borrow (ยืม​แบบ​อ่าน) มี​ได้​พร้อม​กัน​กี่​ตัว​ก็ได้
  • &mut sexclusive borrow (ยืม​แบบ​แก้ไข) ต้อง​รักษา​ความ ผูกขาด ไว้

กฎ​การ​ยืม (Book ch4.2) มี​ข้อ​เดียว​แต่​เด็ดขาด: ณ ขณะ​ใด​ขณะ​หนึ่ง คุณ​มี​ได้ &mut หนึ่ง​ตัว หรือ & กี่​ตัว​ก็ได้ — แต่​ไม่ใช่​ทั้ง​สอง​พร้อม​กัน (one &mut XOR any number of &)

สอง &mut พร้อม​กัน​จะ​โดน E0499:

// ❌ compile ไม่ผ่าน
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // error[E0499]: cannot borrow `s` as mutable more than once
println!("{r1} {r2}");

มี & อยู่​แล้ว​ยัง​ไป​ขอ &mut จะ​โดน E0502:

// ❌ compile ไม่ผ่าน
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // error[E0502]: cannot borrow `s` as mutable ... also borrowed as immutable
println!("{r1} {r2}");

ทำไม​กฎ​นี้​ถึง​สำคัญ​ขนาด​นั้น? เพราะ aliasing XOR mutation — “จะ​แก้​ได้​ต้อง​ผูกขาด จะ share ได้​ต้อง​อ่าน​อย่าง​เดียว” — คือ​นิยาม​ของ​การ ไม่มี data race พอดี​เป๊ะ data race เกิด​เมื่อ​มี​สอง​ทาง​เข้าถึง​ข้อมูล​เดียวกัน​พร้อม​กัน​โดย​อย่าง​น้อยหนึ่ง​ทาง​เป็นการ​เขียน กฎ​การ​ยืม​ของ Rust ห้าม​สภาพ​นั้น​ตั้งแต่ compile ใน code single-thread ธรรม​ดาๆ — แล้ว​ใน​บท8 เรา​จะ​เห็น​ว่า​กฎ​เดียวกัน​นี้​แหละ​ที่​ทำให้ Rust กล้า​เรียก concurrency ของ​มัน​ว่า fearless: data race กลาย​เป็น compile error ไม่ใช่ bug ที่​ต้อง​ไป​ไล่​จับ​ตอน runtime

ถ้า​กฎ​เข้ม​ขนาด​นั้น code นี้​ควร​จะ​พัง​ไหม? มัน​มี​ทั้ง & และ &mut บน​ตัว​เดียวกัน:

fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // r1, r2 ถูกใช้ครั้งสุดท้ายตรงนี้ — shared borrow จบ
let r3 = &mut s; // ตอนนี้เหลือ exclusive borrow ตัวเดียว: OK
r3.push_str(", world");
println!("{r3}");
}

code นี้ compile ผ่าน เพราะ​ขอบเขต​ของ​การ​ยืม​ไม่​ได้​ยาว​ถึง​ปีกกา​ปิด scope แต่​จบ​ที่ การ​ใช้​ครั้ง​สุดท้าย ต่างหาก นี่​คือ non-lexical lifetime (NLL): พอ println! บรรทัด​แรก​ใช้ r1/r2 เสร็จ shared borrow ก็​ตาย​ไป​ทันที ทำให้​บรรทัด​ถัด​มา​ขอ &mut ได้​โดย​ไม่​ชน​กฎ compiler ฉลาด​พอ​จะ​ดู​ว่า borrow ถูก​ใช้​จริง​ถึง​เมื่อไหร่ ไม่ใช่​แค่​ดู​ว่า​ตัวแปร​อยู่​ใน scope หรือ​เปล่า

slicesliceมุมมอง​ยืม (ptr,len) เข้าไป​ใน​ข้อมูล​ต่อ​เนื่อง​ที่​คน​อื่น​เป็น​เจ้าของ เช่น `&str`, `&[T]` คือ​การ​ยืม​ชนิด​พิเศษ: มัน​คือ (pointer, length) ที่​ชี้​เข้าไป​ดู ส่วน​หนึ่ง ของ​ข้อมูล​ต่อ​เนื่อง​ที่​คน​อื่น​เป็น​เจ้าของ โดย​ตัว​มัน​เอง​ไม่​ได้​เป็น​เจ้าของ​อะไร​เลย สอง​ตัว​ที่​เจอ​บ่อย​สุด​คือ &[T] (slice ของ Vec/array) และ &str&strstring แบบ​ยืม อ่าน​อย่าง​เดียว UTF-8 (คู่​กับ `String` ที่​เป็น​เจ้าของ heap) (slice ของ String)

ประเด็น​สำคัญ​เชิง​ปฏิบัติ​คือ deref coercion: &String จะ​ถูก แปลง​ให้​อัตโนมัติ เป็น &str เมื่อ function ต้องการ &str ดังนั้น​ถ้า​คุณ​รับ parameter เป็น &str function ของ​คุณ​จะ​รับ​ได้​ทั้ง string literal และ &String — กว้าง​กว่า​การ​รับ &String ที่​รับ​ได้​แค่ String เท่านั้น

fn print_len(s: &str) { // รับ &str = รับได้กว้างสุด
println!("{} bytes", s.len());
}
fn main() {
let owned = String::from("hello");
print_len(&owned); // &String -> &str อัตโนมัติ (deref coercion)
print_len("literal"); // string literal เป็น &str อยู่แล้ว
}

นี่​คือ​ที่มา​ของ​สำนวน “รับ &str, คืน/เก็บ String” ที่​เรา​จะ​ย้ำ​อีก​ที​ใน​บท5 — ใน​บท​นี้​ตัวอย่าง​ก่อนหน้า​ยัง​รับ &String เพื่อ​ให้​ตรง​กับ​ลำดับ​ของ​หนังสือ ch4 แต่​จาก​นี้​ให้​ติด​นิสัย​มอง &str เป็น default ของ parameter string

เทียบ C#: Span<T> คือ​จุด​ที่ C# ยืม​ไอเดีย​นี้​ไป

Span<T>/ReadOnlySpan<T> ของ C# คือ ref struct ที่​อยู่​บน stack เท่านั้น มี​กฎ​ห้าม escape คล้าย lifetime ของ Rust — เป็น ที่​เดียว ที่ C# รับ model แบบ Rust เข้า​มา ต่าง​กัน​ตรง C# ใช้​กฎ​นี้​เฉพาะ​กับ span ไม่​กี่​ชนิด แต่ Rust ใช้​กับ ทุก reference ใน​ภาษา

lifetime ตัว​แรก​แบบ​ใจดี — 'a อธิบาย​ความ​สัมพันธ์ ไม่​ได้​ยืด​อายุ

หัวข้อ​ที่​มีชื่อ​ว่า “lifetime ตัว​แรก​แบบ​ใจดี — 'a อธิบาย​ความ​สัมพันธ์ ไม่​ได้​ยืด​อายุ”

มา​ถึง​คำ​ที่​หลาย​คน​กลัว​ที่สุด: lifetimelifetimeคำ​อธิบาย​ความ​สัมพันธ์​ของ​อายุ reference เพื่อ​กัน​การ​ชี้​ค้าง (dangling); ส่วน​ใหญ่​ถูก elide ลอง​เขียน function คืน reference ตัว​ที่​ยาว​กว่า​จาก​สอง​ตัว — เขียน​แบบ ไม่​ใส่ lifetime ก่อน มัน​จะ​พัง:

// ❌ compile ไม่ผ่าน
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
error[E0106]: missing lifetime specifier

compiler บอกว่า​มัน​ไม่รู้​ว่า reference ที่​คืน​ออก​ไป​ผูก​อายุ​กับ x หรือ y — และ​มัน​ต้อง​รู้​เพื่อ​จะ​รับประกัน​ว่า reference ที่​คืน​ไป​จะ​ไม่​ชี้​ค้าง (dangling) หลัง​จาก​ข้อมูล​ต้นทาง​ตาย​ไป​แล้ว ใส่ 'a เข้าไป​เพื่อ บอก​ความ​สัมพันธ์:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

'a อ่าน​ว่า “reference ที่​คืน​ออก​ไป​จะ​มี​ชีวิต​อยู่​ได้ ไม่​เกิน ตัว​ที่​สั้น​กว่า​ระหว่าง x กับ y” หัวใจ​ที่​ต้อง​เข้าใจ (Book ch10.3): 'a ไม่​ได้ ยืด อายุ​ของ​อะไร​ทั้งนั้น มัน​แค่ อธิบาย​ความ​สัมพันธ์ ของ​อายุ​ที่​มี​อยู่​แล้ว ให้ compiler มี​ข้อมูล​พอ​จะ​ปฏิเสธ code ที่​จะ​คืน reference อายุ​ยืน​กว่า​ข้อมูล​จริง lifetime เป็น​เครื่องมือ บรรยาย ไม่ใช่​เครื่องมือ ควบคุม อายุ

dangling ref และ​ทาง​แก้​ที่​ตรง​ที่สุด: คืน String ที่​เป็น​เจ้าของ

หัวข้อ​ที่​มีชื่อ​ว่า “dangling ref และ​ทาง​แก้​ที่​ตรง​ที่สุด: คืน String ที่​เป็น​เจ้าของ”

ทีนี้​ดู​กรณี​ที่ lifetime ช่วย​อะไร​ไม่​ได้ เพราะ​มัน​ผิด​ที่​ราก​เลย — function พยายาม​คืน reference ไป​ยัง​ค่า local ของ​ตัวเอง:

// ❌ compile ไม่ผ่าน
fn make_greeting() -> &String {
let s = String::from("hello");
&s // s จะถูก drop ตอน function จบ
}
error[E0106]: missing lifetime specifier

s เป็น​เจ้าของ buffer และ​มัน​จะ​ถูก drop ทันที​ที่ function จบ — reference ที่​คืน​ออก​ไป​จะ​ชี้​ไป​ยัง​หน่วย​ความ​จำ​ที่​ถูก​คืน​ไป​แล้ว นั่น​คือ dangling pointer ที่​ใน​ภาษา​ไม่มี borrow checker คือ use-after-free ทาง​แก้ ไม่ใช่ การ​หา lifetime มา​ยัด แต่​คือ​การ​คืน ค่าที่​เป็น​เจ้าของ ออกไปตรงๆ ให้​ผู้​เรียก​รับ​ความ​เป็น​เจ้าของ​ไป:

fn make_greeting() -> String { // คืน String ที่เป็นเจ้าของ ไม่ใช่ reference
let s = String::from("hello");
s // move ความเป็นเจ้าของออกไปให้ผู้เรียก
}
fn main() {
let g = make_greeting(); // g เป็นเจ้าของ buffer ต่อ
println!("{g}");
}

ความ​เป็น​เจ้าของ​ถูก ย้าย ออก​จาก function ไป​ให้​ผู้​เรียก ไม่มี​อะไร​ถูก drop ก่อน​เวลา และ​ไม่​ต้อง​มี lifetime สัก​ตัว นี่​คือ​สำนวน​ที่​คุณ​จะ​ใช้​บ่อย​กว่า lifetime มาก: เมื่อ​สงสัย ให้​คืน​ของ​ที่​เป็น​เจ้าของ — เก็บ lifetime ไว้​ใช้​ตอน​ที่​คุณ จำเป็น ต้อง​คืน reference จริงๆ

บท​นี้​คือ​ฐาน​ของ​ทั้ง​คอร์ส: กฎ ownership สาม​ข้อ​ทำให้​ค่า​ถูก drop แบบ deterministic โดย​ไม่​ต้อง​มี GC; let s2 = s1; บน String เป็น move ไม่ใช่ alias — ใช้ s1 ต่อ​ไม่​ได้ (E0382) เพราะ​มัน​กัน double-free แก้​ได้​ด้วย clone (กำปั้น​ทุบ​ดิน) หรือ​ยืม​ด้วย & (ฉลาด​กว่า); ชนิด stack ล้วน​เป็น Copy จึง copy แทนที่​จะ move; กฎ​การ​ยืม one &mut XOR any & (E0499/E0502) คือ aliasing-XOR-mutation ที่​ทำให้ data race เป็น​ไป​ไม่​ได้ — ฐาน​ของ fearless concurrency ในบท8; NLL ทำให้ borrow จบ​ตอน​ใช้​ครั้ง​สุดท้าย ไม่ใช่​สุด scope; slice (&str/&[T]) คือ​มุมมอง​ยืม​ที่​ไม่​เป็น​เจ้าของ​อะไร และ &String deref-coerce เป็น &str ให้​เอง; และ lifetime 'a แค่​อธิบาย​ความ​สัมพันธ์​ของ​อายุ ไม่​ได้​ยืด​อายุ (E0106) — ส่วน dangling ref แก้​ตรง​ที่สุด​ด้วย​การ​คืน String ที่​เป็น​เจ้าของ

บท3 เรา​จะ​เอา ownership ที่​ฝึก​มา​นี้​ไป model ข้อมูล​จริง: struct, enum, pattern matching และ Option — ระบบ​ชนิด​ที่​ทำให้ “ไม่มี​ค่า” (null) กลาย​เป็น​สิ่ง​ที่ แทน​ไม่​ได้ เว้น​แต่​คุณ​จะ​ประกาศ​มัน​ออก​มาตรงๆ


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

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

ข้อ 1 / 3

หลังบรรทัด `let s2 = s1;` เมื่อ `s1` เป็น `String` เกิดอะไรขึ้น และทำไมการใช้ `s1` ต่อจึงได้ error E0382?