พื้นฐาน & ไวยากรณ์
ทำไมต้อง Rust?
หัวข้อที่มีชื่อว่า “ทำไมต้อง Rust?”Rust คือ ภาษาโปรแกรมระดับระบบ (systems programming language) ที่ออกแบบมาเพื่อเป้าหมายสามประการ:
- ความปลอดภัยของหน่วยความจำ — คอมไพเลอร์รับประกันความปลอดภัยจาก null-pointer dereference, use-after-free bug และ data race ทั้งหมดนี้ทำที่ compile time
- ประสิทธิภาพ — Rust คอมไพล์เป็น machine code โดยตรงโดยไม่มี runtime overhead ไม่มี garbage collector ไม่มี virtual machine ไม่มี interpreter
- Concurrency ที่ไม่น่ากลัว — กฎ ownership เดียวกันที่ป้องกัน memory bug ยังป้องกัน data race ระหว่าง thread อีกด้วย
แนวคิดสำคัญคือ Rust บรรลุการรับประกันเหล่านี้ผ่าน borrow checker ที่เป็นส่วนหนึ่งของคอมไพเลอร์ที่บังคับใช้กฎ ownership และ lifetime ก่อนที่โปรแกรมจะรันจริง หากคอมไพล์ผ่าน แสดงว่าโปรแกรมปลอดภัยในด้านหน่วยความจำ
สิ่งที่โมดูลนี้ครอบคลุม
หัวข้อที่มีชื่อว่า “สิ่งที่โมดูลนี้ครอบคลุม”| บทเรียน | แนวคิด |
|---|---|
| Variables & Mutability | let, mut, shadowing, const |
| Data Types | Scalar, tuple, array, type inference |
| Functions | fn, parameters, return types, expressions vs statements |
| Control Flow | if แบบ expression, loop, while, for, ranges |
โปรแกรม Rust แรกของคุณ
หัวข้อที่มีชื่อว่า “โปรแกรม Rust แรกของคุณ”ทุก 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);}Compiling…
จาก Source สู่โปรแกรมที่รันได้
หัวข้อที่มีชื่อว่า “จาก Source สู่โปรแกรมที่รันได้”ทุกบทในคอร์สนี้พึ่งพา 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"]
การจัดรูปแบบของ println!
หัวข้อที่มีชื่อว่า “การจัดรูปแบบของ println!”println! รองรับ format specifier หลายแบบนอกเหนือจาก {} ธรรมดาที่ใช้ด้านบน:
{}เรียก traitDisplay— สำหรับ output ที่มนุษย์อ่านง่าย เฉพาะ type ที่ implementDisplayเท่านั้นที่ใช้ตรงนี้ได้ (primitive ส่วนใหญ่ทำได้ แต่ struct ที่คุณสร้างเองไม่ได้ เว้นแต่จะ implement เอง){:?}เรียก traitDebug— สำหรับ 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);}สิ่งที่คอมไพเลอร์บอก: {} ต้องการ Display
หัวข้อที่มีชื่อว่า “สิ่งที่คอมไพเลอร์บอก: {} ต้องการ Display”{} ไม่ใช่ placeholder แบบ “print อะไรก็ได้” ทั่วไป — มันต้องการ type ของค่าที่ implement std::fmt::Display โดยเฉพาะ struct ธรรมดาไม่มี Display impl ให้โดยค่าเริ่มต้น:
// @expect-error E0277struct 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 ทุกประการ ไม่มีอันไหนได้มาฟรีๆ
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”มองว่า {} และ {:?} เป็นคำถามสองคำถามที่แยกกันที่คุณถามค่าหนึ่งๆ: {} ถามว่า “ผู้ใช้อยากเห็นคุณแบบไหน?” (trait ที่คุณเลือกเปิดใช้เองอย่างตั้งใจ เพราะมันกลายเป็นส่วนหนึ่งของ public contract ของ type) ในขณะที่ {:?} ถามว่า “developer อยากเห็นคุณแบบไหนเพื่อ debug?” (trait ที่คุณ derive ได้ฟรีแทบทุกครั้ง) การเลือก #[derive(Debug)] เป็นค่าเริ่มต้นและ implement Display เฉพาะเมื่อต้องการ rendering ที่ผู้ใช้เห็นจริงๆ ช่วยให้ความแตกต่างนี้ชัดเจนอยู่เสมอ
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”ความเข้มงวดตั้งแต่แรกของ Rust — ทุกค่าต้องมี type ที่รู้จัก ทุก format placeholder ต้องตรงกับ trait ที่ค่านั้น implement จริง — แลก loop แบบ “รันแล้วดูเลย” ที่เร็วของภาษา scripting กับคอมไพเลอร์ที่จับบั๊กเรื่อง formatting ทั้งหมวดหมู่ได้ก่อนโปรแกรมจะเริ่มรันด้วยซ้ำ ต้นทุนนั้นมีจริง: cargo build ช้ากว่าการเปิด Python REPL และผู้เริ่มต้นใช้เวลามากกว่าในการทำให้คอมไพเลอร์พอใจตั้งแต่แรก ผลตอบแทนก็มีจริงเช่นกัน: Rust binary ที่คอมไพล์ผ่านจะไม่มีทาง crash จาก TypeError: unsupported format string argument ตอนตี 3 ใน production ได้ — บั๊กหมวดหมู่นั้นไปไม่ถึง binary ที่คอมไพล์แล้วเลย
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ถ้าคอมไพล์ผ่าน แปลว่าไม่มีบั๊ก” 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 ไม่ได้เท่านั้น —letbinding ส่วนใหญ่ในโค้ด Rust จริงไม่มี annotation เลย - “
{:?}กับ{}ใช้แทนกันได้ — ใช้อันไหนก็ได้ที่คอมไพล์ผ่าน” ทั้งสองมีกลุ่มเป้าหมายต่างกัน การใช้{:?}กับ type ที่มีDisplayimpl ที่มีความหมายอยู่แล้ว (เช่น printStringด้วย{:?}แล้วได้ output ที่มี quote และ escape) เป็น smell เล็กๆ ที่ควรสังเกต
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”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 ที่ผู้ใช้เห็นจริงๆ