เริ่มต้นกับ Rust

เริ่มต้นกับ Rust
จุดเริ่มต้นของซีรีส์นี้มาจากโพสต์บน Facebook ที่เห็นจาก rayrag.com ที่แสดงให้เห็นว่าเราสามารถเล่น Ragnarok Online (RO) บนเว็บเบราว์เซอร์ได้เลย ผ่านการเชื่อมต่อ WebSocket ไปยัง emulator server ของเกม นั่นทำให้ผมอยากรู้ว่ามันทำงานยังไง และอยากลองสร้าง WebSocket-to-TCP proxy ของตัวเองด้วยภาษาที่เร็วและปลอดภัย — นั่นคือจุดของการกลับมาทบทวน Rust อีกครั้ง และโปรเจกต์ rs-wsProxy
ทำไมต้อง Rust? เพราะมันให้ทั้งประสิทธิภาพระดับระบบ (ระดับ C/C++) ความปลอดภัยด้านหน่วยความจำโดยไม่ต้องใช้ garbage collector และรองรับการเขียนโปรแกรมแบบพร้อมกันได้อย่างปลอดภัย นี่เป็นบทแรกของซีรีส์ 8 ตอนที่จะพาคุณทบทวน Rust ไปจนถึงสร้าง WebSocket-to-TCP Proxy ที่ใช้งานจริงกับ roBrowser
ติดตั้ง Rust
วิธีที่แนะนำคือการใช้ rustup ซึ่งเป็นเครื่องมือจัดการเวอร์ชันและเครื่องมือของ Rust
เปิดเทอร์มินัลและรันคำสั่งต่อไปนี้:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shสคริปต์นี้จะติดตั้ง rustup ซึ่งจะจัดการการติดตั้ง rustc (คอมไพเลอร์), cargo (ตัวจัดการแพ็กเกจ และเครื่องมืออื่นๆ)
หลังจากติดตั้งเสร็จ ให้รีสตาร์ทเทอร์มินัลหรือรัน source $HOME/.cargo/env เพื่อให้ cargo และ rustc อยู่ใน PATH ของคุณ
ตรวจสอบการติดตั้ง:
rustc --version
cargo --versionคุณควรเห็นเวอร์ชันของ Rust และ Cargo ที่ติดตั้งไว้
เพื่ออัปเดต Rust เป็นเวอร์ชันล่าสุดในอนาคต ให้รัน:
rustup updateHello, Cargo
Cargo คือระบบสร้างและจัดการแพ็กเกจของ Rust มันจัดการการสร้างโค้ด การดาวน์โหลดไลบรารี่ที่จำเป็น และการอัปโหลดแพ็กเกจของคุณไปยัง crates.io
มาเริ่มโครงการแรกกัน:
cargo new hello_rust
cd hello_rustคำสั่ง cargo new จะสร้างไดเรกทอรีใหม่ชื่อ hello_rust พร้อมด้วยโครงสร้างต่อไปนี้:
hello_rust
├── Cargo.toml
└── src
└── main.rsCargo.toml: ไฟล์ตั้งค่าโครงการ ซึ่งประกอบด้วยข้อมูลเมทาดาต้าเช่นชื่อโครงการ เวอร์ชัน และขึ้นต่อกับไลบรารี่อื่นๆsrc/main.rs: ไฟล์โค้ดหลักของโปรแกรม
เปิดไฟล์ src/main.rs คุณจะเห็น:
fn main() {
println!("Hello, world!");
}รันโปรแกรมด้วยคำสั่ง:
cargo runคุณจะเห็นผลลัพธ์:
Compiling hello_rust v0.1.0 (/path/to/hello_rust)
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
Running `target/debug/hello_rust`
Hello, world!คำสั่ง cargo build จะคอมไพล์โปรแกรมโดยไม่รัน มันจะสร้างไฟล์ exécutable ที่ target/debug/hello_rust
สำหรับการสร้างเวอร์ชันที่ปรับประสิทธิภาพสูงสุด ให้ใช้:
cargo build --releaseไฟล์ exécutable จะถูกวางไว้ที่ target/release/hello_rust
ในไฟล์ Cargo.toml คุณจะเห็นเนื้อหาพื้นฐานดังนี้:
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
# ดูเพิ่มเติมเกี่ยวกับคีย์อื่น ๆ ได้ที่ https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]ส่วน [package] มีข้อมูลเกี่ยวกับแพ็กเกจ ส่วน [dependencies] คือที่ที่คุณระบุไลบรารี่ภายนอกที่โครงการของคุณต้องการ
ตัวแปรและความไม่แน่นอน
ใน Rust ตัวแปรโดยค่าเริ่มต้นจะไม่สามารถเปลี่ยนแปลงได้ (immutable) หากคุณต้องการให้สามารถเปลี่ยนแปลงได้ คุณต้องใช้คีย์เวิร์ด mut
let x = 5; // x เป็น immutable ไม่สามารถเปลี่ยนค่าได้
let mut y = 10; // y เป็น mutable สามารถเปลี่ยนค่าได้
y = 15; // ถูกต้อง เพราะ y เป็น mutable
// x = 6; // ผิด! x เป็น immutable ไม่สามารถเปลี่ยนค่าได้
Rust ยังสนับสนุนแนวคิดที่เรียกว่า shadowing ซึ่งคุณสามารถประกาศตัวแปรใหม่ด้วยชื่อเดียวกันได้ และตัวแปรใหม่นี้จะบังแสง (shadow) ตัวแปรเก่า
let x = 5;
let x = x + 1; // ตอนนี้ x คือ 6
let x = x * 2; // ตอนนี้ x คือ 12
// ตัวแปร x ตัวแรกและตัวที่สองถูกบังแสงไปแล้ว
สำหรับค่าคงที่ (constant) ใช้คีย์เวิร์ด const ค่าคงที่ต้องมีการระบุประเภทข้อมูลอย่างชัดเจน และค่าของมันต้องเป็นค่าคงที่ที่รู้ได้ในเวลาคอมไพล์
const MAX_POINTS: u32 = 100_000;
// ตัวอย่างการใช้ขีดล่างเพื่อเพิ่มความอ่านง่าย
ตัวแปรที่ประกาศด้วย let มีช่วงอายุ (scope) อยู่ในบล็อกที่มันถูกประกาศ ในขณะที่ const มีช่วงอายุตลอดทั้งโปรแกรม
ประเภทข้อมูล
Rust เป็นภาษาที่มีการกำหนดประเภทแบบ static แต่มีการอนุมานประเภท (type inference) ที่ยอดเยี่ยม ซึ่งหมายความว่าคุณไม่จำเป็นต้องระบุประเภทเสมอไป เว้นแต่ว่าคอมไพเลอร์ไม่สามารถอนุมานได้
ประเภทจำนวนเต็ม (Integers)
| ความยาว | ที่มีเครื่องหมาย | ไม่มีเครื่องหมาย |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| arch | isize | usize |
ตัวอย่าง:
let a: i8 = -128;
let b: u64 = 4294967295;จำนวนจุดลอยตัว (Floating-Point Numbers)
Rust มีสองประเภทสำหรับตัวเลขจุดลอยตัวซึ่งเป็นไปตามมาตรฐาน IEEE-754:
f32: จำนวนจุดลอยตัวความแม่นยำเดี่ยว 32 บิตf64: จำนวนจุดลอยตัวความแม่นยำคู่ 64 บิต (ค่าเริ่มต้น)
let x = 2.0; // f64
let y: f32 = 3.0; // f32
บูลีน (Boolean)
ประเภท bool มีสองค่า: true และ false
let t = true;
let f: bool = false; // ระบุประเภทอย่างชัดเจนได้หากจำเป็น
อักขระ (Character)
ประเภท char แทนอักขระ Unicode สกัลลาร์เดียว ซึ่งหมายความว่ามันสามารถแทนอักขระได้มากกว่าแค่ ASCII เช่น ตัวอักษรภาษาไทย อีโมจิ เป็นต้น
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';ทูเพิล (Tuples)
ทูเพิลคือวิธีการจัดกลุ่มค่าหลายชนิดเข้าด้วยกันเป็นหนึ่งคอมโพเนนต์หลัก ทูเพิลมีความยาวคงที่: เมื่อประกาศแล้ว พวกมันจะไม่สามารถขยายหรือย่อขนาดได้
let tup: (i32, f64, u8) = (500, 6.4, 1);
// การแยกส่วนทูเพิล (destructuring)
let (x, y, z) = tup;
println!("The value of y is: {y}");
// การเข้าถึงโดยใช้อินเด็กซ์ (จุด)
println!("The value of x is: {}", tup.0);อาร์เรย์ (Arrays)
ต่างจากทูเพิล อาร์เรย์ใน Rust มีทุกองค์ประกอบเป็นประเภทเดียวกัน และมีความยาวคงที่
let a = [1, 2, 3, 4, 5];
// ประเภทของ a คือ [i32; 5] หมายถึงอาร์เรย์ของ i32 จำนวน 5 องค์ประกอบ
// การเข้าถึงองค์ประกอบแรกและที่สอง
let first = a[0];
let second = a[1];
// คุณสามารถกำหนดค่าเริ่มต้นให้กับทุกองค์ประกอบได้ด้วย:
// let a = [3; 5]; // เทียบเท่ากับ [3, 3, 3, 3, 3]
หมายเหตุ: การเข้าถึงดัชนีที่อยู่นอกขอบเขตจะทำให้เกิด panic ในเวลาทำงาน (เพราะ Rust ตรวจสอบขอบเขตของอาร์เรย์)
ฟังก์ชัน
ฟังก์ชันใน Rust ประกาศด้วยคีย์เวิร์ด fn พารามิเตอร์ต้องระบุประเภท และถ้าฟังก์ชันคืนค่า ก็ต้องระบุประเภทของค่าที่คืนหลังจาก ->
fn add(a: i32, b: i32) -> i32 {
a + b // นิพจน์โดยไม่มีจุดคั่นจะถูกส่งกลับเป็นค่าที่คืนมา
}เรียกใช้ฟังก์ชัน:
fn main() {
let sum = add(5, 10);
println!("The sum is {sum}");
}นิพจน์และคำสั่ง
- นิพจน์ (Expression): คืนค่าอะไรบางอย่าง และไม่ลงท้ายด้วยจุดคั่น (ยกเว้นเมื่อใช้เป็นส่วนหนึ่งของคำสั่งที่ใหญ่กว่า)
- คำสั่ง (Statement): ทำการกระทำบางอย่าง แต่ไม่คืนค่า
ตัวอย่างของนิพจน์: x + 1, if condition { 5 } else { 6 }, loop { break 5; }
ตัวอย่างของคำสั่ง: let x = 5; (การประกาศตัวแปรเป็นคำสั่ง), x = 5; (การกำหนดค่าเป็นคำสั่ง)
การคืนค่าจากฟังก์ชันโดยใช้คีย์เวิร์ด return นั้นทำได้ แต่โดยทั่วไปแล้วเราจะใช้การทิ้งจุดคั่นทิ้งไปที่บรรทัดสุดท้ายเพื่อให้มันเป็นนิพจน์และคืนค่า
fn add_with_return(a: i32, b: i32) -> i32 {
return a + b; // ทำได้แต่ไม่จำเป็น
}
fn add_without_return(a: i32, b: i32) -> i32 {
a + b // นิพจน์นี้จะถูกส่งกลับมา
}การควบคุมการทำงาน
นิพจน์ if
if เป็นนิพจน์ใน Rust หมายความว่ามันสามารถคืนค่าได้
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");คุณสามารถมี else if ได้หลายเงื่อนไข
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}ลูป
Rust มีสามประเภทของลูปหลักๆ: loop, while, และ for
loop
ลูป loop จะทำงานไปเรื่อยๆ อย่างไม่มีเงื่อนไขจนกว่าจะเจอคำสั่ง break
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // คืนค่าจากลูป
}
};
println!("The result is {result}");while
ลูป while จะทำงานต่อไปตราบเท่าที่เงื่อนไขเป็นจริง
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");for
ลูป for ใช้สำหรับวนซ้ำไอเท็มในคอลเลกชัน เช่น อาร์เรย์ หรือเรนจ์ (range)
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
// การใช้เรนจ์ (range)
for number in 1..4 {
println!("{number}!");
}
// ผลลัพธ์: 1, 2, 3 (ไม่รวม 4)
// เรนจ์แบบปิด (รวมค่าสุดท้าย)
for number in 1..=4 {
println!("{number}!");
}
// ผลลัพธ์: 1, 2, 3, 4
// เรนจ์แบบย้อนกลับ
for number in (1..4).rev() {
println!("{number}!");
}
// ผลลัพธ์: 3, 2, 1
คอมเมนต์
คอมเมนต์ใน Rust คล้ายกับหลายๆ ภาษา
//สำหรับคอมเมนต์หนึ่งบรรทัด/* */สำหรับคอมเมนต์หลายบรรทัด
ตัวอย่าง:
// นี่คือคอมเมนต์หนึ่งบรรทัด
/*
นี่คือคอมเมนต์
หลายบรรทัด
*/คอมเมนต์สำหรับเอกสารประกอบ (documentation comments) ใช้ /// สำหรับการอธิบายไอเท็มถัดไป (เช่น ฟังก์ชัน หรือสตรัคต์) และ //! สำหรับการอธิบายไอเท็มที่บรรจุ (เช่น โมดูล หรือเครต)
/// เพิ่มตัวเลขหนึ่งเข้ากับอีกตัวเลขหนึ่ง.
///
/// # ตัวอย่าง
///
/// ```
/// let result = add(1, 2);
/// assert_eq!(result, 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
//! ไฟล์นี้มีฟังก์ชันที่เป็นประโยชน์สำหรับการคำนวณทางคณิตศาสตร์พื้นฐาน
สรุป
ในบทนี้เราได้:
- ติดตั้ง Rust ด้วย
rustup - สร้างและรันโปรเจกต์แรกด้วย Cargo
- เรียนรู้เกี่ยวกับตัวแปร ความไม่แน่นอน และการบังแสง
- สำรวจประเภทข้อมูลพื้นฐาน: จำนวนเต็ม จุดลอยตัว บูลีน อักขระ ทูเพิล และอาร์เรย์
- เขียนฟังก์ชันและเข้าใจความแตกต่างระหว่างนิพจน์และคำสั่ง
- ควบคุมการทำงานของโปรแกรมด้วย
if,loop,while, และfor - เขียนคอมเมนต์และคอมเมนต์สำหรับเอกสารประกอบ
ในส่วนถัดไป เราจะดำดิ่งลงไปในแนวคิดที่สำคัญที่สุดของ Rust นั่นคือ การเป็นเจ้าของ (Ownership) ซึ่งเป็นหัวใจของการรับประกันความปลอดภัยด้านหน่วยความจำโดยไม่ต้องใช้ garbage collector