Variables & Mutability
ตัวแปร Immutable โดยค่าเริ่มต้น
หัวข้อที่มีชื่อว่า “ตัวแปร Immutable โดยค่าเริ่มต้น”เมื่อประกาศตัวแปรด้วย let ตัวแปรนั้นจะ immutable — คุณกำหนดค่าให้ใหม่ไม่ได้ นี่ไม่ใช่ข้อจำกัด แต่คือคุณสมบัติที่ตั้งใจออกแบบมา ความ immutable โดยค่าเริ่มต้นช่วยให้คอมไพเลอร์ตรวจจับการเปลี่ยนแปลงที่ไม่ได้ตั้งใจและวิเคราะห์โค้ดได้แม่นยำกว่า
// @expect-error E0384fn main() { let x = 5; x = 6; println!("x = {}", x);}การกำหนดค่าใหม่ให้ binding ที่เป็น immutable คือ compile-time error ไม่ใช่แค่ warning:
error[E0384]: cannot assign twice to immutable variable `x` --> src/main.rs:4:5 |3 | let x = 5; | - first assignment to `x`4 | x = 6; | ^^^^^ cannot assign twice to immutable variable |help: consider making this binding mutable |3 | let mut x = 5; | +++เพื่ออนุญาตให้เปลี่ยนค่าได้ ให้เพิ่ม mut หลัง let:
fn main() { let mut y = 10; y = 20; // allowed because y is mut println!("y = {}", y);}Shadowing
หัวข้อที่มีชื่อว่า “Shadowing”Shadowing แตกต่างจาก mutation คุณ ประกาศใหม่ ตัวแปรที่มีชื่อเดิมด้วย let binding ใหม่ binding ใหม่จะ บดบัง ของเดิมตลอด scope ที่เหลือ คุณแม้แต่เปลี่ยน type ได้เมื่อ shadow
fn main() { let z = 3; let z = z * 2; // shadows the previous z let z = z + 1; // shadows again println!("z = {}", z); // 7
// Shadowing can change the type let word = "hello"; let word = word.len(); // shadow with a usize println!("len = {}", word);}Shadowing ไม่เหมือน mut ด้วย mut คุณกำหนดค่าใหม่ให้ตัวแปรเดิม — และค่าใหม่นั้น ต้องมี type เดียวกัน กับของเดิม เพราะ mut เปลี่ยนแค่ค่าที่เก็บใน binding ที่ตายตัวอันเดียว ด้วย shadowing คุณสร้างตัวแปรใหม่ที่มีชื่อเดียวกัน ดังนั้น type ของมันจึงเปลี่ยนไปเป็นคนละอย่างได้อย่างอิสระ อย่างที่ word (เป็น &str) กลายเป็น word (เป็น usize) ด้านบนแสดงให้เห็น เมื่อ scope สิ้นสุด binding ด้านนอกจะกลับมามองเห็นได้อีกครั้ง
สิ่งที่คอมไพเลอร์บอก: mut เปลี่ยน Type ไม่ได้
หัวข้อที่มีชื่อว่า “สิ่งที่คอมไพเลอร์บอก: mut เปลี่ยน Type ไม่ได้”เพราะ mut ใช้ binding เดิมซ้ำ การให้ค่าที่มี type ต่างไปคือ type error ไม่ใช่โอกาสในการ shadow:
// @expect-error E0308fn main() { let mut x: i32 = 5; x = "a"; println!("x = {}", x);}error[E0308]: mismatched types --> src/main.rs:4:9 |3 | let mut x: i32 = 5; | --- expected due to this type4 | x = "a"; | ^^^ expected `i32`, found `&str`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.นี่คือ E0308: mismatched types ตัวเดียวกับที่จะเจอทั่วทั้ง Rust ทุกครั้งที่ type ของค่าไม่ตรงกับสิ่งที่ context คาดหวัง — annotation, parameter, return type mut แค่ยกเลิกข้อจำกัด “assigned twice” จาก E0384 เท่านั้น มันไม่ได้ยกเลิก type check
Constants
หัวข้อที่มีชื่อว่า “Constants”const ประกาศ ค่าคงที่ที่ compile time ต่างจาก let constants จะ:
- ต้องการ annotation ของ type อย่างชัดเจน
- ต้องกำหนดเป็น constant expression (ไม่ใช่ค่า runtime)
- มีอายุตลอด scope ที่ถูกประกาศ
- ใช้
SCREAMING_SNAKE_CASEตามธรรมเนียม
const MAX_POINTS: u32 = 100_000;const GRAVITY: f64 = 9.81;
fn main() { println!("MAX_POINTS = {}", MAX_POINTS); println!("GRAVITY = {}", GRAVITY);}เครื่องหมาย underscore ใน 100_000 คือ ตัวคั่นตัวเลข (numeric separator) เป็นเพียงการแสดงผล คอมไพเลอร์ไม่สนใจตัวคั่นนี้ ใช้เพื่อให้ตัวเลขขนาดใหญ่อ่านได้ง่ายขึ้น
static ก็ประกาศค่าที่มีอายุตลอดโปรแกรมเช่นกัน แต่ความหมายต่างจาก const: static มี ที่อยู่หน่วยความจำเดียวที่ตายตัว ตลอดการรันโปรแกรม ในขณะที่ const ไม่มีที่อยู่ที่รับประกันเลย — คอมไพเลอร์มีอิสระที่จะ inline ค่าของมันในทุกจุดที่ใช้ เสมือนคุณ paste literal นั้นด้วยมือ
static APP_NAME: &str = "rust-deep-dive";const MAX_RETRIES: u32 = 5;
fn main() { println!("{} allows {} retries", APP_NAME, MAX_RETRIES);}let | const | static | |
|---|---|---|---|
| Type annotation | ไม่บังคับ (อนุมานได้) | บังคับ | บังคับ |
| ค่า | expression ใดก็ได้ ประเมินตอน runtime | constant expression ประเมินตอน compile time | constant expression ประเมินตอน compile time |
| อายุ | จบเมื่อ scope จบ | inline ในทุกจุดที่ใช้ — ไม่มีที่อยู่เดียว | ที่อยู่หน่วยความจำเดียวตลอดโปรแกรม |
| เปลี่ยนค่าได้ไหม | ด้วย mut | ไม่ได้เลย | static mut มีอยู่แต่ต้องใช้ unsafe แตะต้อง — ใช้ atomic หรือ Mutex แทน |
flowchart TD L["let x = 5;"] -->|"scope ends"| D["dropped"] C["const MAX: u32 = 100;"] -->|"used"| I["inlined at each call site (no fixed address)"] S["static NAME: &str = ...;"] -->|"used"| A["same address for the whole program"]
ในทางปฏิบัติ: ใช้ let สำหรับเกือบทุกอย่าง, const สำหรับค่าคงที่ที่ต้องการตั้งชื่อ (limit, default) และ static เฉพาะเมื่อต้องการที่อยู่เดียวที่แชร์กันจริงๆ — ที่พบบ่อยที่สุดคือสำหรับ FFI หรือ global ที่จะห่อด้วย atomic type
fn main() { let x = 5; // x = 6; // error: cannot assign twice to immutable variable
let mut y = 10; y = 20; println!("y = {}", y);
// Shadowing: re-bind the same name let z = 3; let z = z * 2; let z = z + 1; println!("z = {}", z);
// Constants are always immutable and require a type annotation const MAX_POINTS: u32 = 100_000; println!("MAX_POINTS = {}", MAX_POINTS);}Compiling…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”มองว่า let คือการตั้งชื่อให้ค่าหนึ่ง ไม่ใช่ memory cell ที่คุณเขียนทับได้ตามใจ mut อัปเกรดชื่อนั้นให้เขียนทับได้ด้วย — แต่ด้วยค่าที่มี type เดียวกันเป๊ะเท่านั้น เพราะคอมไพเลอร์กำหนด storage size และ layout ของ binding นั้นไปแล้ว Shadowing หลบคำถามนี้ไปเลย: มันไม่ใช้ storage เดิมซ้ำเลย มันสร้างชื่อใหม่ที่บังเอิญ print ออกมาเหมือนกันบนหน้าจอ ซึ่งเป็นเหตุผลที่มันเปลี่ยน type ได้
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”Immutable-by-default หมายความว่า binding ส่วนใหญ่ใน codebase ของ Rust มาพร้อมการรับประกันที่คอมไพเลอร์บังคับ: ไม่มีอะไรที่อยู่ถัดไปเปลี่ยนค่านี้ได้ นั่นทำให้การวิเคราะห์ฟังก์ชันแบบแยกเดี่ยวง่ายขึ้นมาก — ไม่ต้องตาม call site ทุกจุดเพื่อดูว่าค่าที่อ่านที่บรรทัด 40 ต่างจากตอนบรรทัด 10 หรือไม่ ต้นทุนคือความฝืด: คุณต้องเติม mut อย่างตั้งใจ และการลืมมันทำให้เกิด compile error แทนที่จะเป็น runtime surprise ซึ่งผู้เริ่มต้นบางคนรู้สึกว่าภาษา “ขวางทาง” ก่อนที่จะซึมซับว่าทางเลือกอื่นคือบั๊กที่หายากกว่ามาก
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “Shadowing เป็นแค่ shorthand ของ mut” ทั้งสองแก้ปัญหาต่างกัน
mutเปลี่ยนค่าของ binding เดิมในที่เดิม (type เดียวกัน, storage เดียวกัน) Shadowing สร้าง binding ใหม่ทั้งหมด — เปลี่ยน type ได้ และ binding เก่ายังอยู่ (แค่เรียกชื่อไม่ได้แล้ว) จนกว่า scope ของมันจะจบ - “
constกับstaticใช้แทนกันได้สำหรับ global constant”constไม่มีที่อยู่หน่วยความจำที่รับประกัน — คอมไพเลอร์ inline มันได้และทำจริงในทุกจุดที่ใช้staticรับประกันที่อยู่เดียวตลอดโปรแกรม ใช้constเว้นแต่ต้องการที่อยู่เดียวที่แชร์กันนั้นจริงๆ - “หน่วยความจำของตัวแปร immutable เปลี่ยนแปลงไม่ได้เลย” Immutability เกี่ยวกับ binding ไม่ใช่ byte จำเป็นเสมอไป —
Stringที่อยู่หลังletแบบ immutable ยังเป็นเจ้าของ heap allocation ที่คอมไพเลอร์อาจใช้ซ้ำข้าม shadowed rebind ได้ในทางทฤษฎี สิ่งที่letโดยไม่มีmutรับประกันจริงๆ คือ คุณ เขียนผ่านชื่อนั้นไม่ได้
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”AI มักเติม mut “เผื่อไว้” — ให้ loop counter ที่อ่านอย่างเดียว หรือค่าที่ถูก reassign แค่ครั้งเดียวใน branch ที่จริงๆ ใช้ let ใหม่ก็ได้ คอมไพเลอร์จะไม่หยุดคุณ (unused mut เป็นแค่ warning ไม่ใช่ error) ดังนั้นความรกแบบนี้รอดผ่าน code review ได้ถ้าไม่มีใครสังเกต
🐛 หา bug ในโค้ดนี้
fn main() { let mut total = 0; for price in [10, 20, 30] { println!("adding {price}"); } total = 60; println!("total = {total}");}เฉลย
total ถูกประกาศเป็น mut แต่ loop for ไม่เคยอัปเดตมันเลย — มันถูก reassign แค่ครั้งเดียวหลังจากนั้น ซึ่งแค่ let mut total = 0; ตามด้วย total = 60; ก็ทำได้โดยไม่ต้องมี loop เลย (loop body ก็ไม่ได้ใช้ total ด้วยซ้ำ) การคอมไพล์โค้ดนี้จะได้ warning: variable does not need to be mutable ชี้ไปที่บรรทัด let mut total พร้อม help: remove this mut — ทางแก้จริงๆ ในกรณีนี้มักคือสะสมค่าจริงในลูป (total += price;) มากกว่าจะปิดปาก warning ด้วยการลบ mut ทิ้ง