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

Mental Model — การปรับ Mindset จาก TypeScript

ความหงุดหงิดส่วนใหญ่ที่เกิดกับ Rust สำหรับผู้เรียนใหม่มาจากการต่อสู้กับ mental model ไม่ใช่ syntax TypeScript และ Rust ดูคล้ายกันในเชิงผิวเผิน — ทั้งคู่มี types, functions, generics, และ pattern matching — แต่ทำ trade-off ที่แตกต่างกันโดยพื้นฐาน การเข้าใจห้าการเปลี่ยนแปลงนี้ตั้งแต่ต้นจะช่วยให้คุณประหยัดเวลาความสับสนไปได้มาก

ใน JavaScript และ TypeScript หน่วยความจำถูกจัดการโดย garbage collector (GC) คุณสร้าง object ได้อย่างอิสระ แล้ว GC จะคอยสแกนหาค่าที่เข้าไม่ถึงแล้วคืน memory ให้เอง คุณแทบไม่ต้องคิดเรื่อง allocation เลย

ใน Rust ทุกค่ามีเจ้าของเพียงหนึ่งเดียว เมื่อเจ้าของหลุด scope ค่านั้นก็ถูก free — ไม่ต้องมี GC และทั้งหมดนี้ถูกกำหนดตั้งแต่ขั้น compile

TypeScript
// TypeScript: GC manages memory
// You can pass an object anywhere, copy references freely
function greet(name: string): string {
return `Hello, ${name}!`;
}
let s1 = "hello";
let s2 = s1; // s1 is still valid — GC tracks both refs
console.log(s1, s2);
Rust
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
// TypeScript: no borrow checker — races are possible at runtime
let 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 stability
ref1.push(4);
console.log(ref2); // [1, 2, 3, 4] — maybe surprising
Rust
fn 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:?}");
}

ใน TypeScript if และ block เป็น statement — ไม่ให้ค่ากลับมา ส่วนใน Rust เกือบทุกอย่างเป็น expression ที่ให้ค่า คุณจึง assign ผลลัพธ์ของ if หรือ match ได้ตรง ๆ

TypeScript
// TypeScript: if is a statement, need ternary for expressions
const score = 85;
const grade = score >= 90 ? "A" : score >= 75 ? "B" : "C";
console.log(grade);
Rust
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}");
}

let ของ TypeScript สร้าง binding แบบ mutable let ของ Rust สร้าง binding แบบ immutable คุณต้องเลือกใช้ mutability ด้วย let mut อย่างชัดเจน

TypeScript
// TypeScript: let is mutable by default
let count = 0;
count = count + 1; // fine, let is mutable
console.log(count); // 1
Rust
fn 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 ทำงานได้ถูกต้อง

TypeScript ใช้ throw และ try/catch สำหรับการจัดการข้อผิดพลาด Rust ไม่มี exceptions เลย Errors ถูกคืนออกมาเป็นค่าโดยใช้ enum Result<T, E> function ที่อาจล้มเหลวจะคืน Result<Success, Error> และ caller ต้องจัดการทั้งสองกรณี

TypeScript
// TypeScript: exceptions are thrown, not returned
function 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
}
Rust
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}"),
}
}
เมื่อคุณ assign `let s2 = s1;` ใน Rust (โดยที่ s1 เป็น String) เกิดอะไรขึ้นกับ s1?
มี mutable references ไปยังค่าหนึ่งได้กี่อันในเวลาเดียวกันใน Rust?
ใน Rust `let x = if condition { 1 } else { 2 };` ใช้ได้เพราะ:
Rust บ่งบอกอย่างไรว่า function สามารถล้มเหลวได้?