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

พื้นฐาน & ไวยากรณ์

Rust คือ ภาษาโปรแกรมระดับระบบ (systems programming language) ที่ออกแบบมาเพื่อเป้าหมายสามประการ:

  1. ความปลอดภัยของหน่วยความจำ — คอมไพเลอร์รับประกันความปลอดภัยจาก null-pointer dereference, use-after-free bug และ data race ทั้งหมดนี้ทำที่ compile time
  2. ประสิทธิภาพ — Rust คอมไพล์เป็น machine code โดยตรงโดยไม่มี runtime overhead ไม่มี garbage collector ไม่มี virtual machine ไม่มี interpreter
  3. Concurrency ที่ไม่น่ากลัว — กฎ ownership เดียวกันที่ป้องกัน memory bug ยังป้องกัน data race ระหว่าง thread อีกด้วย

แนวคิดสำคัญคือ Rust บรรลุการรับประกันเหล่านี้ผ่าน borrow checker ที่เป็นส่วนหนึ่งของคอมไพเลอร์ที่บังคับใช้กฎ ownership และ lifetime ก่อนที่โปรแกรมจะรันจริง หากคอมไพล์ผ่าน แสดงว่าโปรแกรมปลอดภัยในด้านหน่วยความจำ

บทเรียนแนวคิด
Variables & Mutabilitylet, mut, shadowing, const
Data TypesScalar, tuple, array, type inference
Functionsfn, parameters, return types, expressions vs statements
Control Flowif แบบ expression, loop, while, for, ranges

ทุก Rust binary เริ่มต้นจาก fn main() macro println! ใช้เขียนข้อความที่จัดรูปแบบแล้วออก standard output คอมไพเลอร์อนุมาน type เมื่อไม่คลุมเครือ จึงไม่จำเป็นต้องระบุ type ทุกตัวแปร

fn main() {
println!("Rust guarantees memory safety at compile time.");
println!("No garbage collector. No null pointer exceptions.");
println!("Zero-cost abstractions. Systems-level performance.");
let x: i32 = 42;
let name = "Rust";
println!("x = {}, name = {}", x, name);
}

{} ใน format string คือ ตัวแทน (placeholder) — macro จะแทนที่ด้วยค่าของ argument ที่สอดคล้องกันขณะ runtime

fn main() {
println!("Rust guarantees memory safety at compile time.");
println!("No garbage collector. No null pointer exceptions.");
println!("Zero-cost abstractions. Systems-level performance.");
let x: i32 = 42;
let name = "Rust";
println!("x = {}, name = {}", x, name);
}

ทุกบทในคอร์สนี้พึ่งพา loop เดียวกัน: คุณเขียน source, rustc (ผ่าน cargo) ตรวจสอบมัน และมีแค่โค้ดที่ผ่านทุกการตรวจเท่านั้นที่จะกลายเป็น binary ได้ ไม่มีขั้นตอน “linting” แยกต่างหากที่ข้ามได้ — type checking และ borrow checking เป็นส่วนบังคับของการคอมไพล์เอง

flowchart LR
  S[".rs source"] --> P["rustc: parse + type-check"]
  P --> B["rustc: borrow-check"]
  B -->|"rejected"| E["compile error
(no binary produced)"]
  B -->|"accepted"| O["native binary"]
  O --> R["cargo run / ./binary"]
จาก source ไปสู่โปรแกรมที่รันได้

println! รองรับ format specifier หลายแบบนอกเหนือจาก {} ธรรมดาที่ใช้ด้านบน:

  • {} เรียก trait Display — สำหรับ output ที่มนุษย์อ่านง่าย เฉพาะ type ที่ implement Display เท่านั้นที่ใช้ตรงนี้ได้ (primitive ส่วนใหญ่ทำได้ แต่ struct ที่คุณสร้างเองไม่ได้ เว้นแต่จะ implement เอง)
  • {:?} เรียก trait Debug — สำหรับ output ที่ developer ใช้ debug #[derive(Debug)] สร้างสิ่งนี้ให้อัตโนมัติสำหรับ struct หรือ enum
  • {:#?} คือรูปแบบ “pretty” ของ Debug — หนึ่ง field ต่อหนึ่งบรรทัด มี indent
  • {x} คือ captured identifier: แทนที่จะเขียน println!("{}", x) คุณเขียน println!("{x}") ได้โดยตรงในวงเล็บปีกกา เมื่อ x เป็นตัวแปรที่อยู่ใน scope แล้ว
  • {:>8.2} รวม alignment (> right-align), width (8) และ precision (.2 สองตำแหน่งทศนิยม) เข้าด้วยกัน — มีประโยชน์สำหรับการจัด output ตัวเลขให้เรียงคอลัมน์
#[derive(Debug)]
struct Point { x: i32, y: i32 }
fn main() {
let p = Point { x: 3, y: -4 };
println!("{:?}", p);
println!("{:#?}", p);
let name = "Rust";
println!("{name} has {} letters", name.len());
let pi = 3.14159;
println!("[{:>8.2}]", pi);
}

{} ไม่ใช่ placeholder แบบ “print อะไรก็ได้” ทั่วไป — มันต้องการ type ของค่าที่ implement std::fmt::Display โดยเฉพาะ struct ธรรมดาไม่มี Display impl ให้โดยค่าเริ่มต้น:

// @expect-error E0277
struct Point { x: i32, y: i32 }
fn main() {
let p = Point { x: 1, y: 2 };
println!("{}", p);
}
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:6:20
|
6 | println!("{}", p);
| -- ^ `Point` cannot be formatted with the default formatter
| |
| required by this formatting parameter
|
help: the trait `std::fmt::Display` is not implemented for `Point`
--> src/main.rs:2:1
|
2 | struct Point { x: i32, y: i32 }
| ^^^^^^^^^^^^
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.

คำแนะนำของคอมไพเลอร์เองคือทางแก้: เปลี่ยนไปใช้ {:?} และเพิ่ม #[derive(Debug)] ให้ Point {:?} โดยไม่ derive Debug ก็พังแบบเดียวกัน ด้วย error[E0277]: Point doesn’t implement Debug แทน — Debug ต้องถูกขอ (ผ่าน #[derive(Debug)]) เหมือนกับที่ Display ต้อง implement ทุกประการ ไม่มีอันไหนได้มาฟรีๆ

มองว่า {} และ {:?} เป็นคำถามสองคำถามที่แยกกันที่คุณถามค่าหนึ่งๆ: {} ถามว่า “ผู้ใช้อยากเห็นคุณแบบไหน?” (trait ที่คุณเลือกเปิดใช้เองอย่างตั้งใจ เพราะมันกลายเป็นส่วนหนึ่งของ public contract ของ type) ในขณะที่ {:?} ถามว่า “developer อยากเห็นคุณแบบไหนเพื่อ debug?” (trait ที่คุณ derive ได้ฟรีแทบทุกครั้ง) การเลือก #[derive(Debug)] เป็นค่าเริ่มต้นและ implement Display เฉพาะเมื่อต้องการ rendering ที่ผู้ใช้เห็นจริงๆ ช่วยให้ความแตกต่างนี้ชัดเจนอยู่เสมอ

ความเข้มงวดตั้งแต่แรกของ Rust — ทุกค่าต้องมี type ที่รู้จัก ทุก format placeholder ต้องตรงกับ trait ที่ค่านั้น implement จริง — แลก loop แบบ “รันแล้วดูเลย” ที่เร็วของภาษา scripting กับคอมไพเลอร์ที่จับบั๊กเรื่อง formatting ทั้งหมวดหมู่ได้ก่อนโปรแกรมจะเริ่มรันด้วยซ้ำ ต้นทุนนั้นมีจริง: cargo build ช้ากว่าการเปิด Python REPL และผู้เริ่มต้นใช้เวลามากกว่าในการทำให้คอมไพเลอร์พอใจตั้งแต่แรก ผลตอบแทนก็มีจริงเช่นกัน: Rust binary ที่คอมไพล์ผ่านจะไม่มีทาง crash จาก TypeError: unsupported format string argument ตอนตี 3 ใน production ได้ — บั๊กหมวดหมู่นั้นไปไม่ถึง binary ที่คอมไพล์แล้วเลย

  • “ถ้าคอมไพล์ผ่าน แปลว่าไม่มีบั๊ก” borrow checker และ type checker คัดกรองเฉพาะบั๊กด้าน memory-safety และ type เท่านั้น — logic error (สูตรผิด, off-by-one ใน business logic) คอมไพล์ผ่านสบายๆ
  • “ต้อง annotate type ของทุกตัวแปร” ต้อง annotate แบบ let x: i32 = 42; เฉพาะเมื่อคอมไพเลอร์อนุมาน type จาก context หรือ literal suffix ไม่ได้เท่านั้น — let binding ส่วนใหญ่ในโค้ด Rust จริงไม่มี annotation เลย
  • “{:?} กับ {} ใช้แทนกันได้ — ใช้อันไหนก็ได้ที่คอมไพล์ผ่าน” ทั้งสองมีกลุ่มเป้าหมายต่างกัน การใช้ {:?} กับ type ที่มี Display impl ที่มีความหมายอยู่แล้ว (เช่น print String ด้วย {:?} แล้วได้ output ที่มี quote และ escape) เป็น smell เล็กๆ ที่ควรสังเกต

AI มักสร้าง println!("{}", some_struct) ให้ struct ที่เพิ่งประกาศ เพราะมัน “ดูเหมือน” placeholder ธรรมชาติ — โดยไม่ตรวจว่า Display มีให้ type นั้นหรือไม่

🐛 หา bug ในโค้ดนี้

#[derive(Debug)]
struct Order { id: u32, total: f64 }
fn main() {
let order = Order { id: 42, total: 19.99 };
println!("Order: {}", order);
}
เฉลย

Order derive Debug ไม่ใช่ Display — print ด้วย {} คอมไพล์ไม่ผ่าน ด้วย error[E0277]: Order doesn’t implement std::fmt::Display เหมือนตัวอย่าง Point ด้านบนทุกประการ ทางแก้คือ println!("Order: {:?}", order) (หรือ {:#?} สำหรับมุมมองหลายบรรทัด) เว้นแต่ type นั้นต้องการ Display impl ที่เขียนเองเพื่อ output ที่ผู้ใช้เห็นจริงๆ

Rust รับประกัน memory safety อย่างไร?
Entry point ของ Rust binary คืออะไร?
placeholder {} ใน println! format string ทำหน้าที่อะไร?
โค้ดนี้ compile ไหม ถ้าไม่ผ่าน error code อะไร?
struct Point { x: i32, y: i32 }

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{}", p);
}
โค้ดนี้ print อะไร?
fn main() {
    let pi = 3.14159;
    println!("[{:>8.2}]", pi);
}