เนื้อหา

เริ่มต้นกับ 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 update

Hello, Cargo

Cargo คือระบบสร้างและจัดการแพ็กเกจของ Rust มันจัดการการสร้างโค้ด การดาวน์โหลดไลบรารี่ที่จำเป็น และการอัปโหลดแพ็กเกจของคุณไปยัง crates.io

มาเริ่มโครงการแรกกัน:

cargo new hello_rust
cd hello_rust

คำสั่ง cargo new จะสร้างไดเรกทอรีใหม่ชื่อ hello_rust พร้อมด้วยโครงสร้างต่อไปนี้:

hello_rust
├── Cargo.toml
└── src
    └── main.rs
  • Cargo.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

ถัดไป: Ownership, Borrowing และ Lifetimes