Mental Model — การปรับ Mindset จาก TypeScript
ห้าสิ่งที่จะรู้สึกแตกต่าง
หัวข้อที่มีชื่อว่า “ห้าสิ่งที่จะรู้สึกแตกต่าง”ความหงุดหงิดส่วนใหญ่ที่เกิดกับ Rust สำหรับผู้เรียนใหม่มาจากการต่อสู้กับ mental model ไม่ใช่ syntax TypeScript และ Rust ดูคล้ายกันในเชิงผิวเผิน — ทั้งคู่มี types, functions, generics, และ pattern matching — แต่ทำ trade-off ที่แตกต่างกันโดยพื้นฐาน การเข้าใจห้าการเปลี่ยนแปลงนี้ตั้งแต่ต้นจะช่วยให้คุณประหยัดเวลาความสับสนไปได้มาก
1. Ownership — ไม่มี garbage collector
หัวข้อที่มีชื่อว่า “1. Ownership — ไม่มี garbage collector”ใน JavaScript และ TypeScript หน่วยความจำถูกจัดการโดย garbage collector (GC) คุณสร้าง object ได้อย่างอิสระ แล้ว GC จะคอยสแกนหาค่าที่เข้าไม่ถึงแล้วคืน memory ให้เอง คุณแทบไม่ต้องคิดเรื่อง allocation เลย
ใน Rust ทุกค่ามีเจ้าของเพียงหนึ่งเดียว เมื่อเจ้าของหลุด scope ค่านั้นก็ถูก free — ไม่ต้องมี GC และทั้งหมดนี้ถูกกำหนดตั้งแต่ขั้น compile
// TypeScript: GC manages memory// You can pass an object anywhere, copy references freelyfunction greet(name: string): string { return `Hello, ${name}!`;}
let s1 = "hello";let s2 = s1; // s1 is still valid — GC tracks both refsconsole.log(s1, s2);fn greet(name: &str) -> String { format!("Hello, {}!", name)}
fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is *moved* into s2 — s1 is gone println!("{s2}"); // fine // println!("{s1}"); // compile error: value was moved}ความเข้าใจสำคัญ: การย้ายค่า (การ assign, การส่งให้ function) จะโอน ownership ไป binding เดิมจะไม่ valid อีกต่อไป ฟังดูเข้มงวด — แต่หมายความว่า compiler สามารถรับประกันตั้งแต่ขั้น compile ว่าหน่วยความจำถูก free พอดีหนึ่งครั้ง ไม่มี double-free, ไม่มี use-after-free
2. ความปลอดภัยของหน่วยความจำตั้งแต่ขั้น compile — borrow checker
หัวข้อที่มีชื่อว่า “2. ความปลอดภัยของหน่วยความจำตั้งแต่ขั้น compile — borrow checker”borrow checker ของ Rust คือคนบังคับใช้กฎ ownership โดยจะอ่านโค้ดของคุณตั้งแต่ขั้น compile และปฏิเสธโปรแกรมที่อาจทำให้หน่วยความจำพัง ไม่มีการตรวจตอน runtime ไม่มี null pointer exception ไม่มี heap corruption
กฎมีดังนี้:
- คุณสามารถมี mutable reference หนึ่งอัน (
&mut T) หรือ จำนวนใดก็ได้ของ immutable references (&T) — แต่ไม่เกิดขึ้นพร้อมกัน - References ต้องมีอายุการใช้งานไม่เกินค่าที่ชี้ถึง
// TypeScript: no borrow checker — races are possible at runtimelet arr = [1, 2, 3];const ref1 = arr;const ref2 = arr;// both ref1 and ref2 point to the same array// mutating through one is invisible to callers expecting stabilityref1.push(4);console.log(ref2); // [1, 2, 3, 4] — maybe surprisingfn main() { let mut v = vec![1, 2, 3];
let r1 = &v; // immutable borrow let r2 = &v; // second immutable borrow -- fine println!("{r1:?} {r2:?}"); // r1 and r2 are no longer used after this point
let r3 = &mut v; // mutable borrow -- fine here, r1/r2 are done r3.push(4); println!("{r3:?}");}3. Expressions มากกว่า statements
หัวข้อที่มีชื่อว่า “3. Expressions มากกว่า statements”ใน TypeScript if และ block เป็น statement — ไม่ให้ค่ากลับมา ส่วนใน Rust เกือบทุกอย่างเป็น expression ที่ให้ค่า คุณจึง assign ผลลัพธ์ของ if หรือ match ได้ตรง ๆ
// TypeScript: if is a statement, need ternary for expressionsconst score = 85;const grade = score >= 90 ? "A" : score >= 75 ? "B" : "C";console.log(grade);fn main() { let score = 85;
// if is an expression in Rust -- assign its result directly let grade = if score >= 90 { "A" } else if score >= 75 { "B" } else { "C" };
println!("{grade}");}4. Immutability เป็นค่าเริ่มต้น
หัวข้อที่มีชื่อว่า “4. Immutability เป็นค่าเริ่มต้น”let ของ TypeScript สร้าง binding แบบ mutable let ของ Rust สร้าง binding แบบ immutable คุณต้องเลือกใช้ mutability ด้วย let mut อย่างชัดเจน
// TypeScript: let is mutable by defaultlet count = 0;count = count + 1; // fine, let is mutableconsole.log(count); // 1fn main() { let count = 0; // count = count + 1; // compile error: cannot assign twice to immutable variable
let mut mutable_count = 0; mutable_count += 1; // fine, explicitly mutable println!("{mutable_count}");}Immutability เป็นค่าเริ่มต้น ไม่ใช่แค่แบบแผนอย่าง const ใน TypeScript — compiler เป็นคนบังคับใช้ให้ ผลคืออ่านโค้ดแล้วเดาพฤติกรรมได้ง่ายขึ้น และเป็นฐานให้ borrow checker ทำงานได้ถูกต้อง
5. Errors เป็นค่า — ไม่มี exceptions
หัวข้อที่มีชื่อว่า “5. Errors เป็นค่า — ไม่มี exceptions”TypeScript ใช้ throw และ try/catch สำหรับการจัดการข้อผิดพลาด Rust ไม่มี exceptions เลย Errors ถูกคืนออกมาเป็นค่าโดยใช้ enum Result<T, E> function ที่อาจล้มเหลวจะคืน Result<Success, Error> และ caller ต้องจัดการทั้งสองกรณี
// TypeScript: exceptions are thrown, not returnedfunction parseAge(s: string): number { const n = parseInt(s, 10); if (isNaN(n)) throw new Error(`Invalid age: ${s}`); return n;}
try { const age = parseAge("abc"); console.log(age);} catch (e) { console.error(e); // Error: Invalid age: abc}fn parse_age(s: &str) -> Result<u32, String> { s.parse::<u32>().map_err(|e| format!("Invalid age '{}': {}", s, e))}
fn main() { match parse_age("25") { Ok(age) => println!("age = {age}"), Err(e) => println!("error: {e}"), }
match parse_age("abc") { Ok(age) => println!("age = {age}"), Err(e) => println!("error: {e}"), }}ทดลองกับ expressions และการจัดการข้อผิดพลาดแบบ Result ในเบราว์เซอร์
fn parse_number(s: &str) -> Result<i32, String> { s.parse::<i32>().map_err(|e| format!("Cannot parse '{}': {}", s, e))}
fn main() { // expressions: if produces a value let score = 85; let grade = if score >= 90 { "A" } else if score >= 75 { "B" } else { "C" }; println!("grade = {grade}");
// errors as values match parse_number("42") { Ok(n) => println!("parsed: {n}"), Err(e) => println!("error: {e}"), }
match parse_number("oops") { Ok(n) => println!("parsed: {n}"), Err(e) => println!("error: {e}"), }}Compiling…