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

Variables & Mutability

เมื่อประกาศตัวแปรด้วย let ตัวแปรนั้นจะ immutable — คุณกำหนดค่าให้ใหม่ไม่ได้ นี่ไม่ใช่ข้อจำกัด แต่คือคุณสมบัติที่ตั้งใจออกแบบมา ความ immutable โดยค่าเริ่มต้นช่วยให้คอมไพเลอร์ตรวจจับการเปลี่ยนแปลงที่ไม่ได้ตั้งใจและวิเคราะห์โค้ดได้แม่นยำกว่า

// @expect-error E0384
fn 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 แตกต่างจาก 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 ใช้ binding เดิมซ้ำ การให้ค่าที่มี type ต่างไปคือ type error ไม่ใช่โอกาสในการ shadow:

// @expect-error E0308
fn 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 type
4 | 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

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);
}
letconststatic
Type annotationไม่บังคับ (อนุมานได้)บังคับบังคับ
ค่าexpression ใดก็ได้ ประเมินตอน runtimeconstant expression ประเมินตอน compile timeconstant 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 vs const vs static

ในทางปฏิบัติ: ใช้ 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);
}

มองว่า let คือการตั้งชื่อให้ค่าหนึ่ง ไม่ใช่ memory cell ที่คุณเขียนทับได้ตามใจ mut อัปเกรดชื่อนั้นให้เขียนทับได้ด้วย — แต่ด้วยค่าที่มี type เดียวกันเป๊ะเท่านั้น เพราะคอมไพเลอร์กำหนด storage size และ layout ของ binding นั้นไปแล้ว Shadowing หลบคำถามนี้ไปเลย: มันไม่ใช้ storage เดิมซ้ำเลย มันสร้างชื่อใหม่ที่บังเอิญ print ออกมาเหมือนกันบนหน้าจอ ซึ่งเป็นเหตุผลที่มันเปลี่ยน type ได้

Immutable-by-default หมายความว่า binding ส่วนใหญ่ใน codebase ของ Rust มาพร้อมการรับประกันที่คอมไพเลอร์บังคับ: ไม่มีอะไรที่อยู่ถัดไปเปลี่ยนค่านี้ได้ นั่นทำให้การวิเคราะห์ฟังก์ชันแบบแยกเดี่ยวง่ายขึ้นมาก — ไม่ต้องตาม call site ทุกจุดเพื่อดูว่าค่าที่อ่านที่บรรทัด 40 ต่างจากตอนบรรทัด 10 หรือไม่ ต้นทุนคือความฝืด: คุณต้องเติม mut อย่างตั้งใจ และการลืมมันทำให้เกิด compile error แทนที่จะเป็น runtime surprise ซึ่งผู้เริ่มต้นบางคนรู้สึกว่าภาษา “ขวางทาง” ก่อนที่จะซึมซับว่าทางเลือกอื่นคือบั๊กที่หายากกว่ามาก

  • “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 มักเติม 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 ทิ้ง

keyword mut ทำหน้าที่อะไร?
ความแตกต่างหลักระหว่าง shadowing และ mut คืออะไร?
อะไรที่จำเป็นต้องมีในการประกาศ const?
โค้ดนี้ compile ไหม?
fn main() {
    let mut x: i32 = 5;
    x = "a";
    println!("x = {}", x);
}
ความแตกต่างหลักระหว่าง const กับ static คืออะไร?
underscore ใน 100_000 มีความหมายอย่างไรต่อคอมไพเลอร์ Rust?