เนื้อหา

Collections, Iterators และ Error Handling

การเขียนโปรแกรมใน Rust ไม่ได้หยุดอยู่แค่การเขียนฟังก์ชันและโครงสร้างข้อมูลพื้นฐานเท่านั้น แต่ยังต้องเข้าใจกลไกการจัดการข้อมูลที่ซับซ้อนขึ้น เช่น คอลเลกชันต่างๆ ที่ช่วยให้เราจัดการข้อมูลหลายๆ ชิ้นได้อย่างมีประสิทธิภาพ รวมถึงการใช้อิเทอร์เรเตอร์เพื่อประมวลผลข้อมูลเหล่านั้นอย่างมีประสิทธิภาพ และที่สำคัญที่สุดคือการจัดการข้อผิดพลาดอย่างเหมาะสมด้วยระบบ Result และตัวดำเนินการ ? ซึ่งเป็นหัวใจสำคัญของการเขียนโค้ดที่ปลอดภัยและน่าเชื่อถือใน Rust บทความนี้เป็นส่วนที่ 4 ของซีรีส์ Rust โดยต่อเนื่องจาก Part 3 ที่พูดถึง Structs, Enums และ Pattern Matching และจะนำไปสู่ Part 5 ที่จะพูดถึง Traits และ Generics ต่อไป เราจะเจาะลึกแต่ละหัวข้อด้วยคำอธิบายอย่างละเอียดและตัวอย่างโค้ดที่สามารถนำไปใช้ได้จริง

Vec

Vec<T> คือเวกเตอร์แบบไดนามิกที่เก็บข้อมูลหลายชนิดไว้ด้วยกันบนฮีป เหมือนกับ ArrayList ใน Java หรือ slice ใน Go มันสามารถขยายขนาดได้โดยอัตโนมัติเมื่อต้องการเก็บข้อมูลเพิ่ม และเป็นคอลเลกชันที่ใช้บ่อยที่สุดใน Rust เนื่องจากความยืดหยุ่นและประสิทธิภาพ

// สร้างเวกเตอร์ว่างของจำนวนเต็ม i32
let mut numbers: Vec<i32> = Vec::new();
// เพิ่มค่าเข้าไปในเวกเตอร์
numbers.push(1);
numbers.push(2);
numbers.push(3);

// อ่านค่าดัชนีแรก - จะเกิด panic หากอยู่นอกขอบเขต
println!("First: {}", numbers[0]); // First: 1
// ตรวจสอบความยาวของเวกเตอร์
println!("Length: {}", numbers.len()); // Length: 3

// ใช้ macro vec! เพื่อสร้างเวกเตอร์พร้อมค่าเริ่มต้น
let zeros = vec![0; 5]; // สร้างเวกเตอร์ [0, 0, 0, 0, 0]

การอ่านค่าจากเวกเตอร์มีสองวิธีหลัก ได้แก่ การใช้ดัชนี [index] ซึ่งจะทำให้เกิด panic หากอยู่นอกขอบเขต และการใช้เมธอด .get() ซึ่งคืนค่าเป็น Option<&T> ทำให้สามารถจัดการกับกรณีที่อยู่นอกขอบเขตได้อย่างปลอดภัยโดยไม่ทำให้โปรแกรมหยุดทำงานอย่างกะทันหัน

let fruits = vec!["apple", "banana", "cherry"];

// วิธีที่ 1: การเข้าถึงด้วยดัชนี - จะ panic หากดัชนีอยู่นอกขอบเขต
let first = fruits[0]; // "apple"

// วิธีที่ 2: การใช้ get - ปลอดภัย คืนค่าเป็น Option
match fruits.get(10) {
    Some(fruit) => println!("Found: {}", fruit),
    None => println!("No fruit at index 10"),
}

// ยังสามารถใช้ if let เพื่อความกระชับได้อีกด้วย
if let Some(fruit) = fruits.get(1) {
    println!("Second fruit: {}", fruit);
} else {
    println!("No second fruit");
}

การลบค่าออกจากเวกเตอร์สามารถทำได้หลายวิธี เช่น pop() เพื่อลบและคืนค่าตัวสุดท้าย remove(index) เพื่อลบและคืนค่าตามดัชนีที่กำหนด หรือ clear() เพื่อลบทั้งหมด นอกจากนี้ยังสามารถลูปผ่านเวกเตอร์ได้โดยใช้อิเทอร์เรเตอร์ต่างๆ เช่น .iter() เพื่ออ่านค่าแบบอ้างอิง .iter_mut() เพื่อแก้ไขค่า หรือ .into_iter() เพื่อเป็นเจ้าของค่าและทำให้เวกเตอร์ว่างเปล่าหลังการลูป

let mut numbers = vec![1, 2, 3, 4, 5];

// ลบและคืนค่าตัวสุดท้าย
let last = numbers.pop(); // Some(5)
// ลบและคืนค่าตามดัชนีที่ 0
let first = numbers.remove(0); // 1
// ตอนนี้ numbers คือ [2, 3, 4]

// ลูปผ่านเวกเตอร์เพื่อพิมพ์ค่าทั้งหมด
for number in &numbers {
    println!("{}", number);
}

// ลูปเพื่อแก้ไขค่าในเวกเตอร์
for number in &mut numbers {
    *number *= 2; // คูณแต่ละค่าเป็น 2 เท่า
}
// ตอนนี้ numbers คือ [4, 6, 8]

// ลูปด้วย into_iter เพื่อเป็นเจ้าของค่า (ทำให้เวกเตอร์ว่างหลังลูป)
let sum: i32 = numbers.into_iter().sum();
// หลังจากนี้ numbers จะว่างเปล่าเพราะ into_iter ได้ย้ายความเป็นเจ้าของไป

HashMap<K, V>

HashMap<K, V> คือตารางแฮชที่เก็บข้อมูลเป็นคู่ key-value โดยที่ key จะต้องเป็นแบบที่สามารถแฮชได้ (implements the Hash trait) และเทียบเท่าได้ (implements the Eq trait) มันให้ประสิทธิภาพในการค้นหา เฉลี่ย O(1) ทำให้เหมาะกับการเก็บข้อมูลที่ต้องการค้นหาโดยใช้ key บ่อยๆ คล้ายกับ Dictionary ใน Python หรือ Object ใน JavaScript แต่มีการรับประกันความปลอดภัยของหน่วยความจำตามแบบฉบับของ Rust

use std::collections::HashMap;

// สร้าง HashMap ว่างที่เก็บ String เป็น key และ i32 เป็น value
let mut scores: HashMap<String, i32> = HashMap::new();

// เพิ่มคู่ key-value เข้าไปใน HashMap
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// อ่านค่าจาก key - คืนค่าเป็น Option<&V>
let blue_score = scores.get("Blue");
// จะพิมพ์ว่า Blue's score: 10
match blue_score {
    Some(score) => println!("Blue's score: {}", score),
    None => println!("No score for Blue"),
}

// อัปเดตค่าของ key ที่มีอยู่แล้ว
scores.insert(String::from("Blue"), 25);
// ตอนนี้คะแนนของ Blue คือ 25

// ใช้ entry API เพื่อใส่ค่าเริ่มต้นหาก key ยังไม่มีอยู่
scores.entry(String::from("Green")).or_insert(30);
// หากไม่มี key "Green" จะใส่ค่า 30 เข้าไป
// หากมีอยู่แล้วจะไม่เปลี่ยนแปลงค่าเดิม

การทำงานกับคีย์และค่าใน HashMap มีเรื่องของการเป็นเจ้าของ (ownership) ที่สำคัญ เมื่อเราใส่ค่าแบบ String ลงใน HashMap HashMap จะเป็นเจ้าของค่านั้น หมายความว่าเราจะไม่สามารถใช้ตัวแปรเดิมที่เคยถือค่านั้นได้อีกต่อไปหลังจากที่ใส่เข้าไปแล้ว หากต้องการรักษาการเป็นเจ้าของไว้เราสามารถใช้อ้างอิง (&String) แทนได้ แต่ต้องแน่ใจว่าข้อมูลที่อ้างอิงนั้นมีอายุยืนยาวกว่า HashMap

use std::collections  HashMap;

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
// การใส่ String เข้าไปจะทำให้ HashMap เป็นเจ้าของ field_name และ field_value
map.insert(field_name, field_value);
// field_name และ field_value ไม่สามารถใช้งานได้อีกต่อไปที่นี่
// println!("{}", field_name); // จะเกิด error เพราะถูกย้ายไปแล้ว

// หากต้องการใช้ค่าเดิมต่อได้ ให้ใช้การอ้างอิงแทน
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
// ใส่การอ้างอิงเข้าไป - แต่ต้องแน่ใจว่าข้อมูลที่อ้างอิงอยู่นานกว่า HashMap
map.insert(&field_name, &field_value);
// field_name และ field_value ยังสามารถใช้งานได้ต่อไป
println!("{}", field_name); // ปลอดภัย

การลูปผ่าน HashMap สามารถทำได้สามรูปแบบเหมือนกับเวกเตอร์ แต่จะได้คู่ key-value กลับมาเป็นทูเปิล เราสามารถเลือกลูปแบบอ้างอิงของทั้งคู่ อ้างอิงแบบเปลี่ยนแปลงได้ หรือเป็นเจ้าของทั้งคู่ ขึ้นอยู่กับความต้องการในการใช้งาน

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

// ลูปแบบอ้างอิงเพื่ออ่านค่าเท่านั้น
for (key, value) in &scores {
    println!("{}: {}", key, value);
}

// ลูปแบบอ้างอิงที่เปลี่ยนแปลงได้เพื่อแก้ไขค่า
for (key, value) in &mut scores {
    if key == "Blue" {
        *value += 5; // เพิ่มคะแนน Blue ขึ้น 5
    }
}

// ลูปแบบเป็นเจ้าของ - จะทำให้ HashMap ว่างหลังจากลูปจบ
for (key, value) in scores {
    println!("{}: {}", key, value);
}
// ตอนนี้ scores ว่างเปล่าแล้ว

String และ &str

ใน Rust มีสองประเภทหลักสำหรับการจัดการสตริง ได้แก่ String ซึ่งเป็นสตริงที่ถูกเป็นเจ้าของ เก็บไว้บนฮีป สามารถขยายขนาดได้ และสามารถแก้ไขได้ และ &str ซึ่งเป็นสตริงสไลซ์ที่เป็นการอ้างอิงไปยังส่วนหนึ่งของสตริงที่อยู่ที่ใดที่หนึ่ง ไม่ว่าจะเป็นในไบนารี่ (string literal) หรือใน String ที่ถูกเป็นเจ้าของอยู่ การเข้าใจความแตกต่างระหว่างสองประเภทนี้เป็นสิ่งสำคัญเพื่อหลีกเลี่ยงข้อผิดพลาดเกี่ยวกับการเป็นเจ้าของและการยืมข้อมูล

// สร้าง String ใหม่ที่ว่างเปล่า
let mut s = String::new();
// เพิ่มข้อความเข้าไปใน String
s.push_str("hello");
s.push('!'); // เพิ่มตัวอักษรเดียว
// ตอนนี้ s คือ "hello!"

// สร้าง String จาก literal โดยใช้ to_string() หรือ String::from
let hello = String::from("hello");
// หรือ
let hello = "hello".to_string();

// การต่อสตริงด้วย + หรือ format!
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 ถูกย้ายไปและไม่สามารถใช้งานได้อีกต่อไป
// หรือใช้ format! ซึ่งไม่ทำให้เกิดการย้าย
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = format!("{}{}", s1, s2); // s1 และ s2 ยังสามารถใช้งานได้ต่อไป

// การตัดสตริงเป็นสไลซ์ &str
let hello = String::from("hello world");
// สร้างสไลซ์จากดัชนี 0 ถึง 5 (ไม่รวม 5)
let hello = &hello[0..5]; // "hello"
// หรือจากดัชนี 6 ถึงสิ้นสุด
let world = &hello[6..]; // "world"

เหตุผลที่เราต้องมีทั้ง String และ &str ก็เพื่อให้ Rust สามารถจัดการหน่วยความจำได้อย่างปลอดภัยและมีประสิทธิภาพ String เป็นเจ้าของข้อมูลและรับผิดชอบในการปล่อยหน่วยความจำเมื่อหมดขอบเขต ในขณะที่ &str เป็นเพียงการอ้างอิงที่ไม่มีความรับผิดชอบในการปล่อยหน่วยความจำ ทำให้เราสามารถส่งต่อการอ้างอิงไปยังฟังก์ชันต่างๆ ได้โดยไม่ต้องกังวลเรื่องการย้ายหรือการคัดลอกข้อมูลที่มีขนาดใหญ่

// ฟังก์ชันที่รับ &str เป็นพารามิเตอร์สามารถรับได้ทั้ง String และ string literal
fn print_str(s: &str) {
    println!("{}", s);
}

// การใช้งาน
let owned = String::from("Hello");
let literal = "World";

print_str(&owned); // ต้องใช้ & เพื่อยืมการอ้างอิง
print_str(literal); // ส่งตรงได้เพราะ string literal เป็น &str อยู่แล้ว

Iterators

อิเทอร์เรเตอร์ใน Rust เป็นวิธีที่ทรงพลังในการประมวลผลชุดข้อมูลแบบลำดับโดยไม่ต้องกังวลเกี่ยวกับการจัดการดัชนีหรือเงื่อนไขการหยุดลูป มันขึ้นอยู่กับทเรท Iterator ที่กำหนดเมธอด next() ซึ่งจะคืนค่า Option<Item> โดยเมื่อหมดองค์ประกอบจะคืนค่า None ความสวยงามของอิเทอร์เรเตอร์อยู่ที่การเป็น zero-cost abstraction หมายความว่าการใช้งานมันจะเร็วเท่ากับการเขียนลูปด้วยมือ แต่มีความปลอดภัยและความกระชับมากกว่า

let v1 = vec![1, 2, 3];

// สร้างอิเทอร์เรเตอร์แบบอ้างอิงเพื่ออ่านค่า
let v1_iter = v1.iter();

// ลูปผ่านอิเทอร์เรเตอร์ด้วย for loop (ซึ่งจะเรียก .into_iter() ภายใต้ฝากระโปรงสำหรับคอลเลกชัน)
for val in v1_iter {
    println!("Got: {}", val);
}

// หรือใช้เมธอดของอิเทอร์เรเตอร์โดยตรงผ่านการเรียกเมธอดซ้อนกัน (method chaining)
let v2: Vec<i32> = v1.iter()
    .map(|x| x + 1)     // เพิ่มค่าแต่ละตัวขึ้น 1
    .filter(|x| *x % 2 == 0) // กรองเฉพาะเลขคู่
    .collect();         // รวบรวมผลลัพธ์เป็นเวกเตอร์ใหม่
// v2 จะเป็น [2, 4]

อิเทอร์เรเตอร์มีเมธอดที่เรียกว่าอะแดปเตอร์ (adapters) ซึ่งสามารถนำมาเรียงกันเป็นลำดับเพื่อทำการแปลงข้อมูลที่ซับซ้อนได้โดยยังคงประสิทธิภาพสูง เมธอดเหล่านี้ได้แก่ .map() สำหรับการแปลงแต่ละองค์ประกอบ .filter() สำหรับการกรองเงื่อนไข .take() สำหรับการจำกัดจำนวนองค์ประกอบ และ .collect() สำหรับการรวบรวมผลลัพธ์ลงในคอลเลกชัน เช่น เวกเตอร์ หรือแฮชแมป

let numbers = vec![1, 2, 3, 4, 5, 6];

// หาผลรวมของเลขคู่ที่ยกกำลังสองที่น้อยกว่า 20
let sum_of_squares: i32 = numbers.iter()
    .filter(|&x| x % 2 == 0)   // เลือกเฉพาะเลขคู่
    .map(|x| x * x)            // ยกกำลังสอง
    .take_while(|&x| x < 20)   // หยุดเมื่อค่ามากกว่าหรือเท่ากับ 20
    .sum();                    // คำนวณผลรวม
// ขั้นตอน: [2,4,6] -> [4,16,36] -> [4,16] (เพราะ 36 >= 20) -> ผลรวม = 20

เมธอด .into_iter() จะทำให้เราเป็นเจ้าของค่าในคอลเลกชัน ซึ่งเหมาะสมเมื่อเราต้องการเปลี่ยนแปลงหรือใช้ค่าที่ได้จากการลูปโดยไม่ต้องกังวลเรื่องการยืมอ้างอิง ในขณะที่ .iter_mut() จะให้เราสามารถแก้ไขค่าในคอลเลกชันได้โดยตรงผ่านการอ้างอิงที่เปลี่ยนแปลงได้

let mut names = vec!["Alice".to_string(), "Bob".to_string(), "Charlie".to_string()];

// ใช้ iter_mut เพื่อเปลี่ยนชื่อทั้งหมดให้เป็นตัวพิมพ์ใหญ่
for name in names.iter_mut() {
    *name = name.to_uppercase();
}
// ตอนนี้ names คือ ["ALICE", "BOB", "CHARLIE"]

// ใช้ into_iter เพื่อเป็นเจ้าของชื่อและสร้างเวกเตอร์ใหม่ของความยาวชื่อ
let name_lengths: Vec<usize> = names.into_iter()
    .map(|name| name.len())
    .collect();
// หลังจากนี้ names จะไม่สามารถใช้งานได้อีกต่อไปเพราะถูกย้ายไปแล้ว
// name_lengths คือ [5, 3, 7]

Error Handling ด้วย Result<T, E>

ใน Rust การจัดการข้อผิดพลาดทำผ่านประเภท Result<T, E> ซึ่งเป็นเอ็นัมที่มีสองแขน: Ok(T) สำหรับกรณีสำเร็จที่มีค่าผลลัพธ์ของประเภท T และ Err(E) สำหรับกรณีล้มเหลวที่มีข้อผิดพลาดของประเภท E วิธีนี้บังคับให้ผู้เขียนโค้ดต้องจัดการกับกรณีที่อาจล้มเหลวอย่างชัดเจน ป้องกันการละเลยข้อผิดพลาดโดยไม่ได้ตั้งใจ ซึ่งแตกต่างจากการใช้ข้อยกเว้นในภาษาอื่นๆ ที่อาจถูกมองข้ามได้

use std::fs::File;

// ฟังก์ชันที่อ่านไฟล์และคืนค่า Result<File, std::io::Error>
fn open_file(filename: &str) -> Result<File, std::io::Error> {
    let f = File::open(filename);
    f
}

// การใช้งานฟังก์ชันข้างต้นด้วยการจับคู่รูปแบบ (pattern matching)
match open_file("hello.txt") {
    Ok(file) => println!("File opened successfully"),
    Err(e) => println!("Failed to open file: {}", e),
}

เพื่อให้การเขียนโค้ดที่จัดการกับข้อผิดพลาดสะดวกุล่ิน Rust มีตัวดำเนินการ ? ซึ่งสามารถใช้ได้เฉพาะในฟังก์ชันที่คืนค่าเป็น Result หรือ Option ตัวดำเนินการนี้จะทำหน้าที่เหมือนกับการจับคู่รูปแบบแต่ในรูปแบบที่กระชับกว่า: หากผลลัพธ์เป็น Ok จะคืนค่าที่อยู่ภายในออกมา แต่หากเป็น Err จะคืนค่าข้อผิดพลาดนั้นออกจากฟังก์ชันทันที (early return) ทำให้โค้ดอ่านและเขียนได้ง่ายขึ้นมาก

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    // เปิดไฟล์ หากล้มเหลวจะคืนค่าข้อผิดพลาดทันที
    let mut file = File::open("username.txt")?;
    // อ่านข้อมูลจากไฟล์ลงใน string หากล้มเหลวจะคืนค่าข้อผิดพลาดทันที
    file.read_to_string(&mut username)?;
    // หากทุกอย่างสำเร็จ จะคืนค่า Ok(username)
    Ok(username)
}

แม้ว่า ? จะสะดวก แต่ก็ยังมีเมธอด unwrap() และ expect() ที่สามารถใช้ได้เมื่อเรามั่นใจว่าการดำเนินการนั้นจะไม่ล้มเหลว เช่น เมื่อเราแน่ใจว่าไฟล์มีอยู่จริงในช่วงเวลาทำงาน อย่างไรก็ตามการใช้เมธอดเหล่านี้ควรทำด้วยความระมัดระวังเพราะหากเกิดข้อผิดพลาดขึ้นจริง ๆ จะทำให้เกิด panic และหยุดการทำงานของโปรแกรมทันที expect() ช่วยให้เราสามารถระบุข้อความข้อผิดพลาดที่ต้องการแสดงได้เมื่อเกิด panic ซึ่งช่วยในการดีบักได้ดีกว่า unwrap() ที่ไม่มีข้อความอธิบาย

use std::fs::File;

// สมมติว่าเราแน่ใจว่าไฟล์นี้มีอยู่จริงในระบบไฟล์ของเรา
let f = File::open("essential_config.txt").unwrap();
// หรือพร้อมข้อความอธิบายเมื่อเกิดข้อผิดพลาด
let f = File::open("essential_config.txt")
    .expect("Failed to open essential_config.txt");

ในกรณีที่เราต้องการแปลงข้อผิดพลาดจากประเภทหนึ่งไปเป็นอีกประเภทหนึ่ง เช่น จาก std::io::Error เป็นข้อผิดพลาดเฉพาะของแอปพลิเคชันของเรา เราสามารถใช้เมธอด .map_err() บน Result เพื่อแปลงข้อผิดพลาดได้ นอกจากนี้ยังสามารถนิยามประเภทข้อผิดพลาดเฉพาะของตัวเองโดยใช้เอ็นัมและทำการแปลงโดยใช้ From trait เพื่อให้สามารถใช้ตัวดำเนินการ ? ได้อย่างไร้รอยต่อ

use std::fmt;
use std::fs::File;
use std::io;

// นิยามประเภทข้อผิดพลาดเฉพาะของแอปพลิเคชัน
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    NotFound,
}

// ทำให้ AppError สามารถแปลงจาก io.::Error ได้โดยอัตโนมัติ
impl From<io::Error> for AppError {
    fn from(err: io::Error) -> Self {
        AppError::Io(err)
    }
}

// ทำให้ App สามารถแสดงผลได้ด้วย fmt::Display
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "I/O error: {}", e),
            AppError::NotFound => write!(f, "Resource not found"),
        }
    }
}

// ตอนนี้เราสามารถใช้ ? กับ AppError ได้โดยตรง
fn read_config_file() -> Result<String, AppError> {
    let mut content = String::new();
    File::open("config.txt")?.read_to_string(&mut content)?;
    Ok(content)
}

เชื่อมโยงกับ rs-wsProxy

ในโครงการจริงอย่าง rs-wsProxy เราจะเห็นการประยุกต์ใช้แนวคิดทั้งหมดที่ได้พูดถึงไปแล้ว ตัวอย่างเช่น ในฟังก์ชัน build_allowed_list เราจะเห็นการใช้งานอิเทอร์เรเตอร์อย่างต่อเนื่องเพื่อแปลงและกรองข้อมูลจากสตริงที่คั่นด้วยเครื่องหมายจุลภาค รายการที่ว่างเปล่าจะถูกกรองออกและช่องว่างรอบๆ จะถูกตัดออก ก่อนจะเก็บผลลัพธ์ลงในเวกเตอร์ของสตริง

fn build_allowed_list(hosts: &str) -> Vec<String> {
    hosts.split(',')                    // แบ่งสตริงด้วยเครื่องหมายจุลภาค
        .map(|s| s.trim())              // ตัดช่องว่างที่อยู่ด้านหน้าและด้านหลัง
        .filter(|s| !s.is_empty())      // กรองออกสตริงที่ว่างเปล่า
        .map(|s| s.to_string())         // แปลงจาก &str เป็น String เพื่อเป็นเจ้าของข้อมูล
        .collect()                      // รวบรวมผลลัพธ์เป็น Vec<String>
}

ฟังก์ชัน build_redirects แสดงให้เห็นถึงการใช้งาน HashMap เพื่อสร้างความสัมพันธ์ระหว่างเส้นทางต้นทางและเส้นทางปลายทางในการตั้งค่าการเปลี่ยนเส้นทาง โดยใช้เมธอด insert เพื่อเพิ่มคู่ key-value ลงในแฮชแมป ซึ่งแสดงถึงการเป็นเจ้าของข้อมูลของทั้ง key และ value เมื่อพวกเขาเป็นประเภท String

use std::collections::HashMap;

fn build_redirects() -> HashMap<String, String> {
    let mut redirects = HashMap::new();
    redirects.insert(String::from("/old-path"), String::from("/new-target"));
    redirects.insert(String::from("/another/old"), String::from("/another/new"));
    redirects
}

ในส่วนของการจัดการข้อผิดพลาด ฟังก์ชัน connect_tcp แสดงให้เห็นถึงการใช้งาน Result และตัวดำเนินการ ? เพื่อจัดการกับข้อผิดพลาดจากการเชื่อมต่อ TCP โดยฟังก์ชันนี้คืนค่าเป็น Result<TcpStream, String> ซึ่งหมายถึงหากสำเร็จจะได้ TcpStream หากล้มเหลวจะได้ข้อความข้อผิดพลาดเป็น String การใช้ ? หลังจากการเรียก TcpStream::connect จะทำให้หากเกิดข้อผิดพลาดขึ้นฟังก์ชันจะคืนค่าข้อผิดพลาดนั้นทันทีโดยแปลงมันให้เป็น String ผ่านการใช้ to_string()

use std::net::TcpStream;
use std::io;

fn connect_tcp(addr: &str) -> Result<TcpStream, String> {
    // พยายามเชื่อมต่อ TCP หากล้มเหลวจะคืนค่า Err พร้อมข้อความข้อผิดพลาด
    TcpStream::connect(addr).map_err(|e| e.to_string())
}

สุดท้าย ฟังก์ชัน validate_tls_paths แสดงให้เห็นถึงการใช้งาน Result<(), String> เพื่อบ่งบอกว่าการตรวจสอบสำเร็จหรือล้มเหลว โดยไม่มีข้อมูลผลลัพธ์ใดๆ คืนกลับมาเมื่อสำเร็จ (ใช้หน่วยย่อย ()) แต่เมื่อล้มเหลวจะคืนข้อความอธิบายข้อผิดพลาด ฟังก์ชันนี้ตรวจสอบว่ามีไฟล์ใบรับรองและคีย์ส่วนตัวอยู่หรือไม่ โดยใช้การจับคู่รูปแบบบนผลลัพธ์ของ metadata() เพื่อตรวจสอบว่าเป็นไฟล์จริงหรือไม่

use std::fs::metadata;
use std::io;

fn validate_tls_paths(cert_path: &str, key_path: &str) -> Result<(), String> {
    // ตรวจสอบไฟล์ใบรับรอง
    let cert_meta = metadata(cert_path).map_err(|e| format!("Failed to read cert: {}", e))?;
    if !cert_meta.is_file() {
        return Err("Cert path is not a file".to_string());
    }

    // ตรวจสอบไฟล์คีย์ส่วนตัว
    let key_meta = metadata(key_path).map_err(|e| format!("Failed to read key: {}", e))?;
    if !key_meta.is_file() {
        return Err("Key path is not a file".to_string());
    }

    Ok(())
}

สรุป

ในบทความนี้เราได้สำรวจแนวคิดหลักๆ ของ Rust อย่างลึกซึ้ง ตั้งแต่การทำงานกับคอลเลกชันพื้นฐานอย่าง Vec<T> และ HashMap<K, V> ซึ่งแสดงให้เห็นถึงการจัดการหน่วยความจำและการเป็นเจ้าของที่ปลอดภัย ไปจนถึงการใช้อิเทอร์เรเตอร์เพื่อประมวลผลข้อมูลแบบลำดับอย่างมีประสิทธิภาพและเป็น zero-cost abstraction เรายังได้พูดถึงความแตกต่างที่สำคัญระหว่าง String และ &str ซึ่งเป็นหัวใจสำคัญของการทำงานกับข้อความใน Rust อย่างปลอดภัยและมีประสิทธิภาพ

นอกจากนี้เรายังได้เจาะลึกระบบการจัดการข้อผิดพลาดของ Rust ด้วยประเภท Result<T, E> และตัวดำเนินการ ? ซึ่งบังคับให้เราต้องจัดการกับข้อผิดพลาดอย่างชัดเจน จึงช่วยลดโอกาสเกิดบั๊กจากการละเลยข้อผิดพลาด เราได้เห็นวิธีการแปลงข้อผิดพลาดและการสร้างประเภทข้อผิดพลาดเฉพาะเพื่อให้เหมาะกับบริบทของแอปพลิเคชัน

สุดท้ายเราได้เห็นการประยุกต์ใช้แนวคิดเหล่านี้ในโครงการจริงอย่าง rs-wsProxy ซึ่งแสดงให้เห็นว่าแนวคิดเหล่านี้ไม่ใช่เพียงทฤษฎีเท่านั้น แต่สามารถนำไปใช้แก้ปัญหาจริงๆ ได้อย่างมีประสิทธิภาพ ตั้งแต่การประมวลผลรายการโฮสต์ด้วยอิเทอร์เรเตอร์ การจัดการการเปลี่ยนเส้นทางด้วยแฮชแมป ไปจนถึงการจัดการข้อผิดพลาดในการเชื่อมต่อเครือข่ายและการตรวจสอบไฟล์ TLS

หากต้องการศึกษาต่อในหัวข้อต่อไปของซีรีส์นี้ โปรดดูบทความก่อนหน้าเกี่ยวกับ Structs, Enums และ Pattern Matching ที่ Structs, Enums และ Pattern Matching และเตรียมตัวพบกับบทความถัดไปเกี่ยวกับ Traits และ Generics ที่ Traits และ Generics ซึ่งจะพาเราไปสู่ความเข้าใจในระดับที่สูงขึ้นเกี่ยวกับระบบชนิดแบบทั่วไปและพอลิมอร์ฟิซึมใน Rust