Keywords: ownership, borrowing, error handling, macros, serde_json
อ่านแบบคน Python:
- ถ้าอยากเอา “ภาพรวม” ก่อน: อ่านคำอธิบายแล้วค่อยดูโค้ด
- ถ้าอยาก “ลงมือทำ”: ดู Python → ดู Rust → ดู Output แล้วค่อยย้อนกลับมาอ่านเหตุผลอีกรอบ
- ถ้าติดระหว่างทาง: เปิด 12-learning-playbook.md
บทนี้คือ “จุดตั้งต้น” สำหรับคนเขียน Python ที่อยากเริ่ม Rust แบบไม่หลงทาง
เป้าหมายไม่ใช่ให้คุณท่อง syntax แล้วไปต่อเอง แต่ให้คุณเริ่ม คิด/ออกแบบแบบ Rust ได้ตั้งแต่วันแรก โดยยึด 3 แกนนี้:
- Ownership/borrowing — ข้อมูลก้อนนี้ใครเป็นเจ้าของ, ใครแค่ยืม, ยืมอ่านหรือยืมแก้
- Error path — จุดที่พังต้องถูกมองเห็น (ไม่กลืนเงียบ ๆ)
- Boundary ชัด — แยก “รับ input / I/O” ออกจาก “core logic” ให้เร็วที่สุด
วิธีอ่านที่เวิร์ค: อ่านคำอธิบาย → ดู Python → ดู Rust → ดู Output → แล้วตอบตัวเองว่า
- “Rust บังคับแบบนี้เพื่อกันบั๊กอะไร?”
- “ถ้าเป็น Python โค้ดแบบนี้พังแบบไหนได้บ้างตอน runtime?”
หมายเหตุ:
Output (example)คือ output ตัวอย่างเพื่อให้เห็นภาพ (ของจริงอาจต่างได้เล็กน้อยตาม environment/เวอร์ชัน)(no output — ...)หมายถึง snippet นั้นเป็นแค่การประกาศ/นิยาม/assign ยังไม่ได้รัน logic ที่พิมพ์อะไร
Rust เป็นภาษา static typing ที่ออกแบบให้:
- ปลอดภัยด้านหน่วยความจำ โดยไม่ต้องใช้ garbage collector (GC)
- คุม concurrency ได้มั่นใจขึ้น (โดยเฉพาะเรื่อง data race ใน safe code)
- refactor ได้มั่นใจ เพราะ compiler ช่วยเช็คโครงสร้างได้เยอะมาก
มุมมองแบบ Python → Rust ที่ควรตั้งไว้ตั้งแต่ต้น:
- Python เก่งมากที่ “เขียนเร็ว ทดลองเร็ว” แต่บั๊กหลายแบบจะโผล่ตอน runtime (ชนิดข้อมูล/ขอบเขต reference/state กระจาย)
- Rust ทำให้คุณ “คิดให้ชัดตั้งแต่ก่อนรัน” โดยให้ compiler ช่วยกันบั๊กจำนวนมากให้ตั้งแต่แรก (แลกกับความ strict)
คำศัพท์ที่เจอบ่อยและควรทำความคุ้น (จะวนกลับมาทั้งชุดเอกสาร):
ownership: ใครเป็นเจ้าของข้อมูลก้อนนี้ (ใครเป็นคน drop)borrowing: ใครกำลัง “ยืมอ่าน/ยืมแก้” ข้อมูลอยู่Option/Result: บังคับให้เส้นทาง “ไม่มีค่า/ล้มเหลว” ถูกมองเห็นใน type
Rust มี toolchain หลัก ๆ 2 ตัวที่คุณจะใช้แทบทุกวัน:
rustc(compiler)cargo(build/test/dependency manager)
rustup คือ tool ที่ช่วยจัดการเวอร์ชัน toolchain (เทียบแนวคิดได้กับ pyenv/conda แต่โฟกัสที่ Rust toolchain)
ตรวจสอบว่าเครื่องมี Rust แล้ว:
rustc --versioncargo --version
อ้างอิง (ทางการ):
ทิปสำหรับคนเริ่ม (ช่วยลดสะดุด):
- ถ้า IDE รองรับ ให้ติดตั้ง rust-analyzer (autocomplete + jump-to-def + error inline)
- ถ้าเห็น error แปลก ๆ หลังอัปเดต ให้ลอง
rustup updateแล้วรันใหม่
cargo new hello_rust
cd hello_rust
cargo runOutput (example):
... (cargo build output)
Hello, world!
อ่านแบบ Python mindset:
Cargo.toml= manifest ของโปรเจกต์ (dependencies + metadata) คล้ายpyproject.toml+ lockfile conceptsrc/main.rs= entry point ของโปรแกรม (คล้ายmain.py)
ทิป: ถ้าตอนนี้คุณยังไม่อยาก build/run จริงทุกครั้ง ใช้ cargo check ได้ (เร็วกว่า build/run) — จะอธิบายเป็นระบบในบท workflow (06-tooling-and-workflow.md)
ถ้าคุณจะทำตัวอย่างที่เกี่ยวกับ JSON ให้เพิ่ม serde/serde_json ใน Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"Output (example):
(no output — you updated Cargo.toml)
อ่านแบบ Python:
serde≈ เครื่องมือ serialize/deserialize ให้ struct/enum (เหมือนคุณทำ dataclass + (de)serialize)features = ["derive"]คือการ “เปิดความสามารถ” เพื่อใช้#[derive(Deserialize, Serialize)]ได้
Python:
print("Hello")Output (example):
Hello
Rust:
fn main() {
println!("Hello");
}Output (example):
Hello
มุมมองคน Python:
fn main()คือจุดเริ่มของโปรแกรม (คล้ายif __name__ == "__main__": ...ในเชิงบทบาท)println!มี!เพราะมันเป็น macro (ไม่ใช่ฟังก์ชันธรรมดา)
ทำไม println! เป็น macro?
- รองรับรูปแบบการ format หลายแบบ
- ช่วยตรวจความสัมพันธ์ระหว่าง placeholder
{}กับจำนวนค่าที่ส่งเข้าไปได้ตั้งแต่ compile-time
อ่านต่อแบบละเอียด: 18-rust-macros.md
Python:
x = 1 # ชนิดข้อมูลถูกตรวจ/ยืนยันตอน runtimeRust:
let x = 1; // compiler อนุมาน type ตอน compile-time
let x: i32 = 1; // ถ้าจะกำหนดชัดจุดสำคัญสำหรับคนเริ่ม:
- Rust ไม่ได้ “ยืดหยุ่นน้อย” เพื่อกวนใจ แต่เพื่อให้ระบบใหญ่แล้วไม่พังเงียบ ๆ
- การมี type ที่ชัด ทำให้ refactor ได้มั่นใจขึ้นมาก (compiler เป็นเหมือน reviewer ที่เฝ้าอยู่ตลอด)
Python เปลี่ยนค่าได้ตลอด (ชื่อชี้ object เปลี่ยนได้ง่าย)
Rust default เป็น immutable (ต้องเขียน mut ถ้าตั้งใจให้เปลี่ยนค่า):
let x = 1;
// x = 2; // error
let mut y = 1;
y = 2; // okOutput (example):
(no output — this snippet only assigns variables)
ทำไม Rust ทำแบบนี้?
- เพราะ “state ที่เปลี่ยนได้” เป็นแหล่ง bug ใหญ่สุดในระบบจริง
- Rust ทำให้จุดที่ state เปลี่ยนเป็นจุดที่คุณตั้งใจ (อ่านโค้ดแล้วไล่เหตุผลได้ง่าย)
ทิป practical:
- เวลาอ่านโค้ด Rust ให้มอง
mutเป็น “ป้ายเตือน” ว่าตรงนี้มี state change - ถ้าเจอ
mutเยอะผิดปกติ ให้ถามว่า “กำลังออกแบบให้ข้อมูลถูกแก้หลายที่เกินไปไหม”
นี่คือจุดที่คนมาจาก Python มักสะดุด แต่ถ้าจับหลักได้ Rust จะลื่นมาก เพราะ compiler จะกันบั๊กหนัก ๆ ให้ตั้งแต่แรก
คำอธิบายสั้นแบบจับต้องได้:
- ใน Rust “ข้อมูลก้อนหนึ่ง” ต้องมีคนรับผิดชอบเรื่องอายุการใช้งาน (ใคร drop)
- การยืมคือการขอสิทธิ์ใช้ข้อมูลช่วงสั้น ๆ แบบที่ปลอดภัย (ยืมอ่าน/ยืมแก้)
- ค่าหนึ่งมี owner เดียว
- เมื่อ assign/ส่งค่าไปฟังก์ชัน บาง type จะเกิด move (ย้ายความเป็นเจ้าของ)
ตัวอย่าง:
fn main() {
let s1 = String::from("hi");
let s2 = s1; // move
// println!("{}", s1); // error: s1 ถูกย้ายไปแล้ว
println!("{}", s2);
}Output (example):
hi
อ่านแบบ Python:
- Python:
s2 = s1คือให้ชื่อใหม่ชี้ object เดิม (alias) - Rust: บาง type ไม่ยอมให้ “เผลอแชร์ความเป็นเจ้าของ” เพราะมันกัน bug ระดับหน่วยความจำ (เช่น double-free)
&T= ยืมอ่าน&mut T= ยืมแก้ (มีได้ทีละหนึ่งในช่วงเดียวกัน)
Python (แนวคิดคล้ายกัน):
def len_of(s: str) -> int:
return len(s)
def main() -> None:
s = "hello"
n = len_of(s)
print(s, n)
main()Output (example):
hello 5
Rust:
fn len_of(s: &str) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let n = len_of(&s);
println!("{} {}", s, n);
}Output (example):
hello 5
สรุป mindset:
- Python: ส่ง object ไปมาแล้วแก้ได้ง่าย แต่บางทีแก้ “ไกลเกินที่ตั้งใจ”
- Rust: ต้องชัดว่า “จุดไหนอ่าน” และ “จุดไหนแก้” เพื่อให้ความปลอดภัยชัดตั้งแต่ต้น
อ่านลึกขึ้น (บทที่สำคัญที่สุดของชุดนี้): 02-ownership-and-borrowing.md
แก่นของแนวคิดนี้คือ “ทำให้เคสไม่มีค่า/เคสพังอยู่ใน type” เพื่อให้คนอ่าน + compiler เห็นตั้งแต่แรก
Python ที่เจอบ่อยมาก (เริ่มง่าย ๆ แล้วค่อย ๆ กลายเป็น bug):
- parse พัง → คืนค่า default → โปรแกรมยังรันต่อเหมือนไม่มีอะไรเกิดขึ้น
ตัวอย่าง (Python):
def parse_port(text: str) -> int:
try:
return int(text)
except ValueError:
# BUG: กลืน error แล้ว “ทำให้เหมือนสำเร็จ”
return 0
port = parse_port("not-a-number")
print(port) # 0 (แต่คุณอาจไม่รู้ว่า parse พัง)ใน Rust คุณมักเขียนให้ “ความเป็นไปได้ที่พัง” อยู่ใน type ตั้งแต่แรก:
fn parse_port(text: &str) -> Result<u16, std::num::ParseIntError> {
text.parse::<u16>()
}ผลที่ได้ (สำคัญมาก):
- คนเรียก ต้อง เลือกว่า “จะ handle error ยังไง” (เช่น return ต่อ / log / fallback แบบตั้งใจ)
- bug แบบ “ค่าผิดแต่ไหลต่อเงียบ ๆ” ลดลงเยอะ
Option<T> คือการบอกอย่างตรงไปตรงมาว่า “ค่านี้อาจไม่มี”
- ข้อดีคือคนอ่าน/IDE/compiler จะเห็นตั้งแต่แรกว่ามีเคส missing value ต้อง handle
Python:
x = NoneOutput (example):
(no output — assignment only)
Rust:
let x: Option<i32> = None;
let y: Option<i32> = Some(3);Output (example):
(no output — declarations only)
ใช้ match เพื่อจัดการ:
match y {
Some(v) => println!("v={}", v),
None => println!("no value"),
}Output (example):
v=3
Result<T, E> คือการบอกว่า “การทำงานนี้สำเร็จหรือพัง”
Ok(T)= สำเร็จErr(E)= พัง พร้อมข้อมูล error
Python:
def parse_num(text: str) -> int:
try:
return int(text)
except ValueError:
return 0Output (example):
(no output — function definition only)
Rust:
fn parse_num(text: &str) -> Result<i32, std::num::ParseIntError> {
text.parse::<i32>()
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let n = parse_num("42")?;
println!("{}", n);
Ok(())
}Output (example):
42
อ่านให้ลึกขึ้น:
?คือ “ถ้า Err ให้ return ออกไปทันที” (early return)- ในโปรเจกต์จริง
mainมักเป็นจุดตัดสินใจว่า “log ยังไง / exit code เท่าไร” แทนการกลืน error
อ่านต่อแบบเป็นระบบ: 03-error-handling.md
ปัญหาคลาสสิกใน Python: ใช้ action เป็น string แล้วเขียน if/elif ยาว ๆ ซึ่งโตขึ้นแล้วพลาดเคสได้ง่าย
Rust แนะนำ:
- แปลง string →
enum Action - ใช้
matchเพื่อบังคับให้ handle ครบทุกกรณี (ถ้าลืม compiler จะเตือน)
ตัวอย่างแนวคิด (ใช้เป็น placeholder ของ subcommand):
enum Action {
Status,
Version,
}
fn parse_action(s: &str) -> Option<Action> {
match s {
"status" => Some(Action::Status),
"version" => Some(Action::Version),
_ => None,
}
}Output (example):
(no output — this snippet defines types/functions)
แนวคิดที่อยากให้เห็น:
- สตริงพิมพ์ผิด = runtime bug (และบางทีหลุดไป production)
- enum + match ทำให้ “คำสั่งที่อนุญาต” ถูก encode อยู่ใน type (อ่านง่ายและ refactor ง่าย)
Python มักเริ่มจาก dict แล้วค่อยไล่ validate key ทีหลัง ซึ่งใช้ได้ แต่เมื่อ schema โตขึ้นจะเริ่มหลุดง่าย
แนวทางแบบ Rust (ที่ช่วยคุม bug ได้ดี):
- ทำ
struct EndpointConfigให้ schema ชัด - อ่านไฟล์ → parse JSON → ได้ค่า typed
- ถ้า schema ไม่ตรง: deserialize fail แบบ explicit (fail fast)
Skeleton:
use serde::Deserialize;
#[derive(Deserialize)]
struct EndpointConfig {
addrlists: Vec<String>,
// เติม field เพิ่มตามไฟล์จริง
}
fn load_endpoint_config(path: &str) -> Result<EndpointConfig, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&text)?)
}Output (example):
(no output — this snippet only defines types/functions)
จุดที่ควรจับ:
- “พิมพ์ key ผิด” หรือ “type ไม่ตรง” ควร fail ให้เร็ว ไม่ใช่ default เงียบ ๆ
- ยิ่งแปลงเป็น typed struct เร็วเท่าไร ขอบเขตของ bug จะเล็กลงเท่านั้น
อ่านต่อแบบละเอียด: 09-config-and-serde-json.md
- ทำให้คอมไพล์ผ่านก่อน แล้วค่อยปรับ: compiler message คือ feedback ที่ดีที่สุดช่วงแรก
- หลีกเลี่ยง global mutable ถ้าไม่จำเป็น: ใช้
struct AppStateแล้วส่งผ่าน function parameters - ทำ workflow ให้เป็นนิสัย:
cargo check→cargo test→cargo fmt→cargo clippy
(ดูบท workflow แบบรวมใน 06-tooling-and-workflow.md)
- เขียน
enum Action+parse_action()ให้รองรับ action เพิ่มอีก 2 ตัว - สร้าง
struct AppStateที่มี field อย่างน้อย 2 อัน (เช่นmessages_count: u64,last_error: Option<String>) - ทำ JSON string สั้น ๆ แล้วลอง deserialize เป็น struct (เริ่มจาก field เดียวก่อน)
- TRPL: https://doc.rust-lang.org/book/
- Ownership: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
Arc: https://doc.rust-lang.org/std/sync/struct.Arc.htmlMutex: https://doc.rust-lang.org/std/sync/struct.Mutex.htmlRwLock: https://doc.rust-lang.org/std/sync/struct.RwLock.htmlserde_json: https://docs.rs/serde_json