19 เมษายน 2568
Rust ได้รับความสนใจจากนักพัฒนาอย่างต่อเนื่อง โดยครองตำแหน่งภาษาโปรแกรมที่ “เป็นที่รักมากที่สุด” ในการสำรวจของ Stack Overflow ติดต่อกันหลายปี นี่ไม่ใช่แค่กระแส Rust นำเสนอการผสมผสานที่น่าสนใจระหว่างประสิทธิภาพ ความปลอดภัย และคุณสมบัติภาษาที่ทันสมัย ซึ่งช่วยแก้ปัญหาทั่วไปที่พบในภาษาโปรแกรมเชิงระบบอื่นๆ หากคุณสงสัยว่าอะไรทำให้ Rust พิเศษและต้องการเริ่มต้นการเดินทาง คู่มือสำหรับผู้เริ่มต้นนี้จะให้ความรู้พื้นฐานเพื่อให้คุณเริ่มต้นได้ เราจะสำรวจไวยากรณ์หลัก แนวคิดเฉพาะตัว เช่น ความเป็นเจ้าของ (ownership) และเครื่องมือที่จำเป็นซึ่งขับเคลื่อนระบบนิเวศของ Rust
Rust วางตำแหน่งตัวเองเป็นภาษาสำหรับการสร้างซอฟต์แวร์ที่เชื่อถือได้และมีประสิทธิภาพ ข้อได้เปรียบหลักอยู่ที่ความปลอดภัยของหน่วยความจำโดยไม่ต้องอาศัย garbage collector และการเปิดใช้งานการทำงานพร้อมกันที่ปลอดภัยไร้กังวล (fearless concurrency) นี่คือเหตุผลที่คุณควรพิจารณาเรียนรู้ Rust:
ก่อนที่คุณจะเขียนโค้ด Rust บรรทัดแรก คุณต้องตั้งค่าชุดเครื่องมือ Rust (Rust toolchain) วิธีมาตรฐานและแนะนำในการติดตั้ง Rust คือการใช้ rustup
ซึ่งเป็นตัวติดตั้งชุดเครื่องมือ Rust
rustup
: เครื่องมือบรรทัดคำสั่งนี้จัดการการติดตั้ง Rust ของคุณ ช่วยให้คุณสามารถติดตั้ง อัปเดต และสลับระหว่าง Rust เวอร์ชันต่างๆ (เช่น stable, beta หรือ nightly builds) ได้อย่างง่ายดาย ไปที่เว็บไซต์ทางการของ Rust (https://www.rust-lang.org/tools/install) สำหรับคำแนะนำในการติดตั้งเฉพาะสำหรับระบบปฏิบัติการของคุณrustc
: นี่คือคอมไพเลอร์ Rust หลังจากที่คุณเขียนซอร์สโค้ด Rust ในไฟล์ .rs
แล้ว rustc
จะคอมไพล์ไฟล์เหล่านั้นเป็นไฟล์ไบนารีที่เรียกใช้งานได้ (executable binaries) หรือไลบรารีที่คอมพิวเตอร์ของคุณเข้าใจได้ แม้ว่าจะสำคัญ แต่โดยปกติคุณจะไม่เรียกใช้ rustc
โดยตรงบ่อยนักในขั้นตอนการทำงานประจำวันของคุณcargo
: นี่คือระบบการสร้างและตัวจัดการแพ็กเกจของ Rust และเป็นเครื่องมือที่คุณจะโต้ตอบด้วยบ่อยที่สุด Cargo จัดการงานพัฒนาทั่วไปหลายอย่าง:
cargo new
)cargo build
)cargo run
)cargo test
)Cargo.toml
ของคุณโดยอัตโนมัติ)สำหรับการทดลองอย่างรวดเร็วโดยไม่ต้องติดตั้งในเครื่อง แพลตฟอร์มออนไลน์ เช่น Rust Playground อย่างเป็นทางการ หรือสภาพแวดล้อมการพัฒนาแบบรวม เช่น Replit เป็นตัวเลือกที่ยอดเยี่ยม
มาเริ่มกันด้วยโปรแกรม “Hello, world!” แบบดั้งเดิม ซึ่งเป็นพิธีการสำหรับการเรียนรู้ภาษาใหม่ สร้างไฟล์ชื่อ main.rs
และเพิ่มโค้ดต่อไปนี้:
fn main() {
// บรรทัดนี้พิมพ์ข้อความไปยังคอนโซล
println!("Hello, Rust!");
}
ในการคอมไพล์และรันโปรแกรมง่ายๆ นี้โดยใช้เครื่องมือพื้นฐาน:
main.rs
และรัน:
rustc main.rs
คำสั่งนี้เรียกใช้คอมไพเลอร์ Rust (rustc
) ซึ่งสร้างไฟล์ที่เรียกใช้งานได้ (เช่น main
บน Linux/macOS, main.exe
บน Windows)./main
# บน Windows ใช้: .\main.exe
อย่างไรก็ตาม สำหรับอะไรก็ตามที่นอกเหนือจากไฟล์เดียว การใช้ Cargo เป็นวิธีมาตรฐานและสะดวกกว่ามาก:
cargo new hello_rust
cd hello_rust
Cargo สร้างไดเรกทอรีใหม่ชื่อ hello_rust
ซึ่งมีไดเรกทอรีย่อย src
พร้อมไฟล์ main.rs
(มีโค้ด “Hello, Rust!” อยู่แล้ว) และไฟล์กำหนดค่าชื่อ Cargo.toml
cargo run
Cargo จะจัดการขั้นตอนการคอมไพล์แล้วจึงเรียกใช้โปรแกรมผลลัพธ์ โดยแสดง “Hello, Rust!” บนคอนโซลของคุณมาดูรายละเอียดโค้ดกัน:
fn main()
: นี่คือการกำหนดฟังก์ชันหลัก คำหลัก fn
หมายถึงการประกาศฟังก์ชัน main
เป็นชื่อฟังก์ชันพิเศษ เป็นจุดเริ่มต้นที่โปรแกรม Rust ที่เรียกใช้งานได้ทุกโปรแกรมเริ่มทำงาน วงเล็บ ()
บ่งชี้ว่าฟังก์ชันนี้ไม่รับพารามิเตอร์อินพุต{}
: วงเล็บปีกกา กำหนดบล็อกโค้ดหรือ ขอบเขต (scope) โค้ดทั้งหมดที่เป็นของฟังก์ชันจะอยู่ภายในวงเล็บปีกกาเหล่านี้println!("Hello, Rust!");
: บรรทัดนี้ทำหน้าที่พิมพ์ข้อความไปยังคอนโซล
println!
เป็น มาโคร (macro) ของ Rust มาโครคล้ายกับฟังก์ชัน แต่มีความแตกต่างที่สำคัญคือ ลงท้ายด้วยเครื่องหมายอัศเจรีย์ !
มาโครทำการสร้างโค้ด ณ เวลาคอมไพล์ ทำให้มีพลังและความยืดหยุ่นมากกว่าฟังก์ชันปกติ (เช่น การจัดการอาร์กิวเมนต์จำนวนไม่แน่นอน ซึ่ง println!
ทำได้) มาโคร println!
พิมพ์ข้อความที่ให้มาไปยังคอนโซลและเพิ่มอักขระขึ้นบรรทัดใหม่ที่ส่วนท้ายโดยอัตโนมัติ"Hello, Rust!"
คือ สตริงลิเทอรัล (string literal) – ลำดับอักขระคงที่ที่แสดงถึงข้อความ อยู่ในเครื่องหมายคำพูดคู่;
: เครื่องหมายอัฒภาค แสดงถึงจุดสิ้นสุดของคำสั่ง (statement) โค้ด Rust ที่ปฏิบัติการได้ส่วนใหญ่ (คำสั่ง) จะลงท้ายด้วยเครื่องหมายอัฒภาคตอนนี้ มาเจาะลึกองค์ประกอบพื้นฐานของภาษาโปรแกรม Rust กัน
ตัวแปรใช้เพื่อเก็บค่าข้อมูล ใน Rust คุณประกาศตัวแปรโดยใช้คำหลัก let
let apples = 5;
let message = "Take five";
แนวคิดหลักใน Rust คือ ตัวแปรเป็นแบบ ไม่เปลี่ยนรูป (immutable) โดยค่าเริ่มต้น ซึ่งหมายความว่าเมื่อค่าถูกผูกกับชื่อตัวแปรแล้ว คุณจะไม่สามารถเปลี่ยนค่านั้นได้ในภายหลัง
let x = 10;
// x = 15; // บรรทัดนี้จะทำให้เกิดข้อผิดพลาดเวลาคอมไพล์! ไม่สามารถกำหนดค่าให้ตัวแปร `x` ที่ไม่เปลี่ยนรูปได้สองครั้ง
println!("ค่าของ x คือ: {}", x); // {} เป็นตัวยึดตำแหน่งสำหรับค่าของ x
การไม่เปลี่ยนรูปโดยค่าเริ่มต้นนี้เป็นการตัดสินใจออกแบบโดยเจตนา ซึ่งช่วยให้คุณเขียนโค้ดที่ปลอดภัยและคาดการณ์ได้มากขึ้น โดยป้องกันการแก้ไขข้อมูลโดยไม่ได้ตั้งใจ ซึ่งอาจเป็นสาเหตุทั่วไปของข้อบกพร่อง หากคุณ ต้องการ ตัวแปรที่ค่าสามารถเปลี่ยนแปลงได้ คุณต้องระบุอย่างชัดเจนว่าเป็น mutable โดยใช้คำหลัก mut
ระหว่างการประกาศ
let mut count = 0; // ประกาศ 'count' เป็น mutable
println!("จำนวนเริ่มต้น: {}", count);
count = 1; // สิ่งนี้ทำได้เพราะ 'count' ถูกประกาศด้วย 'mut'
println!("จำนวนใหม่: {}", count);
Rust ยังอนุญาต การบังเงา (shadowing) คุณสามารถประกาศตัวแปร ใหม่ ที่มีชื่อเดียวกับตัวแปรก่อนหน้าภายในขอบเขตเดียวกัน ตัวแปรใหม่จะ “บังเงา” ตัวแปรเก่า ซึ่งหมายความว่าการใช้ชื่อนั้นในภายหลังจะอ้างถึงตัวแปรใหม่ สิ่งนี้แตกต่างจากการเปลี่ยนแปลงค่า (mutation) เพราะเรากำลังสร้างตัวแปรใหม่ทั้งหมด ซึ่งอาจมีประเภทข้อมูลแตกต่างกันได้ด้วยซ้ำ
let spaces = " "; // 'spaces' เริ่มต้นเป็นสตริงสไลซ์ (&str)
let spaces = spaces.len(); // 'spaces' ถูกบังเงาโดยตัวแปรใหม่ที่เก็บความยาว (เป็นจำนวนเต็ม, usize)
println!("จำนวนช่องว่าง: {}", spaces); // พิมพ์ค่าจำนวนเต็ม
Rust เป็นภาษา ประเภทข้อมูลคงที่ (statically typed) ซึ่งหมายความว่าประเภทของตัวแปรทุกตัวจะต้องเป็นที่รู้จักโดยคอมไพเลอร์ ณ เวลาคอมไพล์ อย่างไรก็ตาม Rust มี การอนุมานประเภทข้อมูล (type inference) ที่ยอดเยี่ยม ในหลายสถานการณ์ คุณไม่จำเป็นต้องเขียนประเภทข้อมูลออกมาอย่างชัดเจน คอมไพเลอร์มักจะสามารถอนุมานได้จากค่าและวิธีที่คุณใช้งาน
let quantity = 10; // คอมไพเลอร์อนุมานเป็น i32 (ประเภทจำนวนเต็มมีเครื่องหมายเริ่มต้น)
let price = 9.99; // คอมไพเลอร์อนุมานเป็น f64 (ประเภททศนิยมเริ่มต้น)
let active = true; // คอมไพเลอร์อนุมานเป็น bool (บูลีน)
let initial = 'R'; // คอมไพเลอร์อนุมานเป็น char (อักขระ)
หากคุณต้องการหรือจำเป็นต้องระบุอย่างชัดเจน (เช่น เพื่อความชัดเจน หรือเมื่อคอมไพเลอร์ต้องการความช่วยเหลือ) คุณสามารถให้ คำอธิบายประเภทข้อมูล (type annotations) โดยใช้เครื่องหมายโคลอน :
ตามด้วยชื่อประเภท
let score: i32 = 100; // ระบุชัดเจนว่าเป็นจำนวนเต็ม 32 บิตมีเครื่องหมาย
let ratio: f32 = 0.5; // ระบุชัดเจนว่าเป็นทศนิยมความแม่นยำเดี่ยว
let is_complete: bool = false; // ระบุชัดเจนว่าเป็นบูลีน
let grade: char = 'A'; // ระบุชัดเจนว่าเป็นอักขระ (ค่าสเกลาร์ Unicode)
Rust มีประเภทข้อมูล สเกลาร์ (scalar) (แทนค่าเดี่ยว) ในตัวหลายประเภท:
i8
, i16
, i32
, i64
, i128
, isize
) เก็บจำนวนเต็มทั้งบวกและลบ จำนวนเต็มไม่มีเครื่องหมาย (u8
, u16
, u32
, u64
, u128
, usize
) เก็บเฉพาะจำนวนเต็มที่ไม่เป็นลบ ประเภท isize
และ usize
ขึ้นอยู่กับสถาปัตยกรรมของคอมพิวเตอร์ (32 บิตหรือ 64 บิต) และใช้เป็นหลักสำหรับการทำดัชนีคอลเลกชันf32
(ความแม่นยำเดี่ยว) และ f64
(ความแม่นยำคู่) ค่าเริ่มต้นคือ f64
bool
มีสองค่าที่เป็นไปได้: true
หรือ false
char
แทนค่าสเกลาร์ Unicode เดียว (ครอบคลุมมากกว่าแค่อักขระ ASCII) อยู่ในเครื่องหมายคำพูดเดี่ยว (เช่น 'z'
, 'π'
, '🚀'
)String
กับ &str
การจัดการข้อความใน Rust มักเกี่ยวข้องกับประเภทข้อมูลหลักสองประเภท ซึ่งอาจทำให้ผู้มาใหม่สับสน:
&str
(อ่านว่า “สตริงสไลซ์”): นี่คือ การอ้างอิงแบบไม่เปลี่ยนรูป (immutable reference) ไปยังลำดับของไบต์ที่เข้ารหัสแบบ UTF-8 ซึ่งเก็บไว้ที่ใดที่หนึ่งในหน่วยความจำ สตริงลิเทอรัล (เช่น "Hello"
) เป็นประเภท &'static str
ซึ่งหมายความว่ามันถูกเก็บไว้โดยตรงในไบนารีของโปรแกรมและมีอยู่ตลอดระยะเวลาการทำงานของโปรแกรม สไลซ์ให้ มุมมอง (view) ข้อมูลสตริง โดยไม่ได้เป็นเจ้าของ (owning) มันมีขนาดคงที่String
: นี่คือประเภทสตริงที่เข้ารหัสแบบ UTF-8 ซึ่ง มีเจ้าของ (owned), ขยายได้ (growable), เปลี่ยนแปลงได้ (mutable) ข้อมูล String
ถูกเก็บไว้บน ฮีป (heap) ทำให้สามารถปรับขนาดได้ โดยทั่วไปคุณจะใช้ String
เมื่อคุณต้องการแก้ไขข้อมูลสตริง หรือเมื่อสตริงต้องการเป็นเจ้าของข้อมูลของตัวเองและจัดการช่วงชีวิตของตัวเอง (มักจะเมื่อคืนค่าสตริงจากฟังก์ชันหรือเก็บไว้ในสตรัค)// สตริงลิเทอรัล (เก็บไว้ในไบนารีของโปรแกรม, ไม่เปลี่ยนรูป)
let static_slice: &'static str = "ฉันไม่เปลี่ยนรูป";
// สร้าง String ที่มีเจ้าของและจัดสรรบนฮีปจากลิเทอรัล
let mut dynamic_string: String = String::from("เริ่มต้น");
// แก้ไข String (ทำได้เพราะเป็น mutable และเป็นเจ้าของข้อมูล)
dynamic_string.push_str(" และเติบโต");
println!("{}", dynamic_string); // ผลลัพธ์: เริ่มต้น และเติบโต
// สร้างสตริงสไลซ์ที่อ้างอิงส่วนหนึ่งของ String
// สไลซ์นี้ยืมข้อมูลจาก dynamic_string
let slice_from_string: &str = &dynamic_string[0..5]; // อ้างอิง "เริ่มต้น"
println!("สไลซ์: {}", slice_from_string);
ฟังก์ชันเป็นพื้นฐานสำหรับการจัดระเบียบโค้ดเป็นหน่วยที่มีชื่อและนำกลับมาใช้ใหม่ได้ เราได้เจอฟังก์ชันพิเศษ main
ไปแล้ว
// การกำหนดฟังก์ชัน
fn greet(name: &str) { // รับพารามิเตอร์หนึ่งตัว: 'name' ซึ่งเป็นสตริงสไลซ์ (&str)
println!("สวัสดี, {}!", name);
}
// ฟังก์ชันที่รับพารามิเตอร์ i32 สองตัวและคืนค่า i32
fn add(a: i32, b: i32) -> i32 {
// ใน Rust นิพจน์สุดท้ายในเนื้อหาฟังก์ชันจะถูกส่งคืนโดยอัตโนมัติ
// ตราบใดที่มันไม่ได้ลงท้ายด้วยเครื่องหมายอัฒภาค
a + b
// สิ่งนี้เทียบเท่ากับการเขียน: return a + b;
}
fn main() {
greet("อลิซ"); // เรียกใช้ฟังก์ชัน 'greet'
let sum = add(5, 3); // เรียกใช้ 'add', ผูกค่าที่ส่งคืนกับ 'sum'
println!("5 + 3 = {}", sum); // ผลลัพธ์: 5 + 3 = 8
}
ประเด็นสำคัญเกี่ยวกับฟังก์ชัน:
fn
เพื่อประกาศฟังก์ชัน->
หากฟังก์ชันไม่คืนค่า ประเภทข้อมูลที่ส่งคืนจะเป็น ()
โดยปริยาย (ทูเพิลว่างเปล่า มักเรียกว่า “unit type”);
) และอาจลงท้ายด้วย นิพจน์ (expression) (สิ่งที่ประเมินค่าได้)return
สามารถใช้สำหรับการส่งคืนค่าก่อนกำหนดอย่างชัดเจนจากที่ใดก็ได้ภายในฟังก์ชันRust มีโครงสร้างการควบคุมการไหลมาตรฐานเพื่อกำหนดลำดับการทำงานของโค้ด:
if
/else
/else if
: ใช้สำหรับการทำงานตามเงื่อนไขlet number = 6;
if number % 4 == 0 {
println!("number หารด้วย 4 ลงตัว");
} else if number % 3 == 0 {
println!("number หารด้วย 3 ลงตัว"); // สาขานี้ทำงาน
} else {
println!("number หารด้วย 4 หรือ 3 ไม่ลงตัว");
}
// ที่สำคัญ 'if' เป็นนิพจน์ใน Rust หมายความว่ามันประเมินค่าได้
// สิ่งนี้ทำให้คุณสามารถใช้มันโดยตรงในคำสั่ง 'let' ได้
let condition = true;
let value = if condition { 5 } else { 6 }; // value จะเป็น 5
println!("ค่าคือ: {}", value);
// หมายเหตุ: ทั้งสองสาขาของนิพจน์ 'if' ต้องประเมินค่าเป็นประเภทเดียวกัน
loop
: สร้างลูปไม่สิ้นสุด โดยทั่วไปคุณจะใช้ break
เพื่อออกจากลูป ซึ่งสามารถส่งคืนค่าได้while
: ทำงานตราบเท่าที่เงื่อนไขที่ระบุยังคงเป็นจริงfor
: วนซ้ำตามองค์ประกอบของคอลเลกชันหรือช่วง นี่เป็นประเภทลูปที่ใช้บ่อยที่สุดและมักจะปลอดภัยที่สุดใน Rust// ตัวอย่าง loop
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // ออกจากลูปและคืนค่า 'counter * 2'
}
};
println!("ผลลัพธ์ลูป: {}", result); // ผลลัพธ์: ผลลัพธ์ลูป: 20
// ตัวอย่าง while
let mut num = 3;
while num != 0 {
println!("{}!", num);
num -= 1;
}
println!("ปล่อยตัว!!!");
// ตัวอย่าง for (วนซ้ำในอาร์เรย์)
let a = [10, 20, 30, 40, 50];
for element in a.iter() { // .iter() สร้าง iterator เหนือองค์ประกอบของอาร์เรย์
println!("ค่าคือ: {}", element);
}
// ตัวอย่าง for (วนซ้ำในช่วง)
// (1..4) สร้างช่วงที่รวม 1, 2, 3 (ไม่รวม 4)
// .rev() กลับลำดับ iterator
for number in (1..4).rev() {
println!("{}!", number); // พิมพ์ 3!, 2!, 1!
}
println!("ปล่อยตัวอีกครั้ง!!!");
ใช้คอมเมนต์เพื่อเพิ่มคำอธิบายและบันทึกย่อในโค้ดของคุณ ซึ่งคอมไพเลอร์จะละเว้น
// นี่คือคอมเมนต์บรรทัดเดียว มันขยายไปจนถึงสิ้นสุดบรรทัด
/*
* นี่คือคอมเมนต์แบบบล็อกหลายบรรทัด
* มันสามารถครอบคลุมหลายบรรทัดและมีประโยชน์
* สำหรับคำอธิบายที่ยาวขึ้น
*/
let lucky_number = 7; // คุณสามารถวางคอมเมนต์ไว้ที่ท้ายบรรทัดได้เช่นกัน
ความเป็นเจ้าของ (Ownership) เป็นคุณสมบัติที่โดดเด่นและเป็นศูนย์กลางที่สุดของ Rust มันเป็นกลไกที่ช่วยให้ Rust รับประกันความปลอดภัยของหน่วยความจำ ณ เวลาคอมไพล์ โดยไม่จำเป็นต้องมี garbage collector การเข้าใจความเป็นเจ้าของเป็นกุญแจสำคัญในการทำความเข้าใจ Rust มันปฏิบัติตามกฎหลักสามข้อ:
{ // s ไม่สามารถใช้งานได้ที่นี่ ยังไม่ได้ประกาศ
let s = String::from("hello"); // s ใช้งานได้ตั้งแต่จุดนี้เป็นต้นไป;
// s 'เป็นเจ้าของ' ข้อมูล String ที่จัดสรรบนฮีป
// คุณสามารถใช้ s ที่นี่ได้
println!("{}", s);
} // ขอบเขตสิ้นสุดที่นี่ 's' ไม่สามารถใช้งานได้อีกต่อไป
// Rust เรียกใช้ฟังก์ชันพิเศษ 'drop' โดยอัตโนมัติสำหรับ String ที่ 's' เป็นเจ้าของ
// เพื่อปล่อยคืนหน่วยความจำฮีปของมัน
เมื่อคุณกำหนดค่าที่มีเจ้าของ (เช่น String
, Vec
, หรือสตรัคที่มีประเภทข้อมูลที่มีเจ้าของ) ให้กับตัวแปรอื่น หรือส่งผ่านไปยังฟังก์ชันตามค่า ความเป็นเจ้าของจะถูก ย้าย (moved) ตัวแปรเดิมจะไม่สามารถใช้งานได้อีกต่อไป
let s1 = String::from("original");
let s2 = s1; // ความเป็นเจ้าของข้อมูล String ถูก ย้าย จาก s1 ไปยัง s2
// s1 ไม่ถือว่าใช้งานได้อีกต่อไปหลังจากจุดนี้
// println!("s1 คือ: {}", s1); // ข้อผิดพลาดเวลาคอมไพล์! ค่าถูกยืมที่นี่หลังจากการย้าย
// s1 ไม่ได้เป็นเจ้าของข้อมูลอีกต่อไป
println!("s2 คือ: {}", s2); // s2 ตอนนี้เป็นเจ้าของและใช้งานได้
พฤติกรรมการย้ายนี้ป้องกันข้อผิดพลาด “double free” ซึ่งตัวแปรสองตัวอาจพยายามปล่อยคืนตำแหน่งหน่วยความจำเดียวกันโดยไม่ได้ตั้งใจเมื่อออกนอกขอบเขต ประเภทข้อมูลพื้นฐาน เช่น จำนวนเต็ม ทศนิยม บูลีน และอักขระ ใช้เทรต Copy
ซึ่งหมายความว่ามันจะถูกคัดลอกแทนที่จะถูกย้ายเมื่อกำหนดค่าหรือส่งผ่านไปยังฟังก์ชัน
จะทำอย่างไรถ้าคุณต้องการให้ฟังก์ชันใช้ค่า โดยไม่ต้อง โอนความเป็นเจ้าของ? คุณสามารถสร้าง การอ้างอิง (references) การสร้างการอ้างอิงเรียกว่า การยืม (borrowing) การอ้างอิงช่วยให้คุณเข้าถึงข้อมูลที่ตัวแปรอื่นเป็นเจ้าของได้โดยไม่ต้องรับความเป็นเจ้าของ
// ฟังก์ชันนี้รับการอ้างอิง (&) ไปยัง String
// มันยืม String แต่ไม่ได้รับความเป็นเจ้าของ
fn calculate_length(s: &String) -> usize {
s.len()
} // ที่นี่ s (การอ้างอิง) ออกนอกขอบเขต แต่เนื่องจากมันไม่ได้เป็นเจ้าของ
// ข้อมูล String ข้อมูลจึง ไม่ได้ ถูกทิ้งเมื่อการอ้างอิงออกนอกขอบเขต
fn main() {
let s1 = String::from("hello");
// เราส่งการอ้างอิงไปยัง s1 โดยใช้สัญลักษณ์ '&'
// s1 ยังคงเป็นเจ้าของข้อมูล String
let len = calculate_length(&s1);
// s1 ยังคงใช้งานได้ที่นี่เพราะความเป็นเจ้าของไม่เคยถูกย้าย
println!("ความยาวของ '{}' คือ {}.", s1, len);
}
การอ้างอิงเป็นแบบไม่เปลี่ยนรูปโดยค่าเริ่มต้น เช่นเดียวกับตัวแปร หากคุณต้องการแก้ไขข้อมูลที่ยืมมา คุณต้องมี การอ้างอิงแบบเปลี่ยนแปลงได้ (mutable reference) ซึ่งแสดงด้วย &mut
อย่างไรก็ตาม Rust บังคับใช้กฎที่เข้มงวดเกี่ยวกับการอ้างอิงแบบเปลี่ยนแปลงได้เพื่อป้องกัน data races:
กฎการยืม (The Borrowing Rules):
&mut T
)&T
)กฎเหล่านี้บังคับใช้โดยคอมไพเลอร์
// ฟังก์ชันนี้รับการอ้างอิงแบบเปลี่ยนแปลงได้ไปยัง String
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
fn main() {
// 's' ต้องประกาศเป็น mutable เพื่ออนุญาตการยืมแบบเปลี่ยนแปลงได้
let mut s = String::from("hello");
// ตัวอย่างการบังคับใช้กฎการยืม (ยกเลิกคอมเมนต์บรรทัดเพื่อดูข้อผิดพลาด):
// let r1 = &s; // การยืมแบบไม่เปลี่ยนรูป - OK
// let r2 = &s; // การยืมแบบไม่เปลี่ยนรูปอีกอัน - OK (อนุญาตการยืมแบบไม่เปลี่ยนรูปหลายรายการ)
// let r3 = &mut s; // ERROR! ไม่สามารถยืม `s` เป็น mutable ในขณะที่การยืมแบบไม่เปลี่ยนรูปยังใช้งานอยู่
// // Rust บังคับใช้: ผู้อ่านหลายคน (&T) หรือผู้เขียนคนเดียว (&mut T) อย่างใดอย่างหนึ่ง ไม่ใช่ทั้งคู่
// println!("{}, {}", r1, r2); // การใช้ r1/r2 ทำให้มันยังคงใช้งานอยู่ ซึ่งกระตุ้นให้เกิดข้อผิดพลาด
// // หากไม่มี println นี้ NLL (Non-Lexical Lifetimes) ของ Rust จะปล่อย r1/r2 ก่อนเวลา
// // ทำให้ &mut s ใช้งานได้ที่นี่
// การยืมแบบเปลี่ยนแปลงได้ได้รับอนุญาตที่นี่เพราะไม่มีการยืมอื่น ๆ ที่ใช้งานอยู่
change(&mut s);
println!("{}", s); // ผลลัพธ์: hello, world
}
Rust มีวิธีจัดกลุ่มค่าหลายค่าเป็นประเภทข้อมูลที่ซับซ้อนมากขึ้น
สตรัค (ย่อมาจาก structures) ช่วยให้คุณกำหนดประเภทข้อมูลแบบกำหนดเองโดยการจัดกลุ่มฟิลด์ข้อมูลที่เกี่ยวข้องเข้าด้วยกันภายใต้ชื่อเดียว
// กำหนดสตรัคชื่อ User
struct User {
active: bool,
username: String, // ใช้ประเภท String ที่มีเจ้าของ
email: String,
sign_in_count: u64,
}
fn main() {
// สร้าง instance ของสตรัค User
// instance ต้องให้ค่าสำหรับทุกฟิลด์
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// เข้าถึงฟิลด์สตรัคโดยใช้เครื่องหมายจุด
// instance ต้องเป็น mutable เพื่อเปลี่ยนค่าฟิลด์
user1.email = String::from("anotheremail@example.com");
println!("อีเมลผู้ใช้: {}", user1.email);
// ใช้ฟังก์ชันช่วยเหลือเพื่อสร้าง instance ของ User
let user2 = build_user(String::from("user2@test.com"), String::from("user2"));
println!("สถานะ active ของ User 2: {}", user2.active);
// ไวยากรณ์การอัปเดตสตรัค: สร้าง instance ใหม่โดยใช้บางฟิลด์
// จาก instance ที่มีอยู่สำหรับฟิลด์ที่เหลือ
let user3 = User {
email: String::from("user3@domain.com"),
username: String::from("user3"),
..user2 // รับค่า 'active' และ 'sign_in_count' จาก user2
};
println!("จำนวนการลงชื่อเข้าใช้ของ User 3: {}", user3.sign_in_count);
}
// ฟังก์ชันที่คืนค่า instance ของ User
fn build_user(email: String, username: String) -> User {
User {
email, // การเริ่มต้นฟิลด์แบบย่อ: ถ้าชื่อพารามิเตอร์ตรงกับชื่อฟิลด์
username,
active: true,
sign_in_count: 1,
}
}
Rust ยังรองรับ ทูเพิลสตรัค (tuple structs) ซึ่งเป็นทูเพิลที่มีชื่อ (เช่น struct Color(i32, i32, i32);
) และ สตรัคแบบหน่วย (unit-like structs) ซึ่งไม่มีฟิลด์และมีประโยชน์เมื่อคุณต้องการ implement trait บนประเภทข้อมูลแต่ไม่จำเป็นต้องเก็บข้อมูลใดๆ (เช่น struct AlwaysEqual;
)
อีนัม (enumerations) ช่วยให้คุณกำหนดประเภทข้อมูลโดยการแจกแจง variant ที่เป็นไปได้ ค่าของอีนัมสามารถเป็นได้เพียงหนึ่งใน variant ที่เป็นไปได้เท่านั้น
// อีนัมง่ายๆ ที่กำหนดชนิดของที่อยู่ IP
enum IpAddrKind {
V4, // Variant ที่ 1
V6, // Variant ที่ 2
}
// Variant ของอีนัมสามารถเก็บข้อมูลที่เกี่ยวข้องได้ด้วย
enum IpAddr {
V4(u8, u8, u8, u8), // Variant V4 เก็บค่า u8 สี่ค่า
V6(String), // Variant V6 เก็บ String
}
// อีนัมที่พบบ่อยและสำคัญมากในไลบรารีมาตรฐานของ Rust: Option<T>
// มันเข้ารหัสแนวคิดของค่าที่อาจมีอยู่หรือไม่มีอยู่
// enum Option<T> {
// Some(T), // แทนการมีอยู่ของค่าประเภท T
// None, // แทนการไม่มีอยู่ของค่า
// }
// อีนัมสำคัญอีกตัวในไลบรารีมาตรฐาน: Result<T, E>
// ใช้สำหรับการดำเนินการที่สามารถสำเร็จ (Ok) หรือล้มเหลว (Err) ได้
// enum Result<T, E> {
// Ok(T), // แทนความสำเร็จ ประกอบด้วยค่าประเภท T
// Err(E), // แทนความล้มเหลว ประกอบด้วยค่าข้อผิดพลาดประเภท E
// }
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// การสร้าง instance ของอีนัม IpAddr พร้อมข้อมูลที่เกี่ยวข้อง
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
// ตัวอย่างกับ Option<T>
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
// ตัวดำเนินการควบคุมการไหล 'match' เหมาะอย่างยิ่งสำหรับการทำงานกับอีนัม
// มันช่วยให้คุณสามารถเรียกใช้โค้ดที่แตกต่างกันตาม variant ของอีนัม
match some_number {
Some(i) => println!("ได้ตัวเลข: {}", i), // ถ้าเป็น Some ให้ผูกค่าภายในกับ i
None => println!("ไม่ได้อะไรเลย."), // ถ้าเป็น None
}
// 'match' ต้องครอบคลุมทุกกรณี (exhaustive): คุณต้องจัดการทุก variant ที่เป็นไปได้
// เครื่องหมายขีดล่าง '_' สามารถใช้เป็นรูปแบบ wildcard เพื่อจับ variant ใดๆ
// ที่ไม่ได้ระบุไว้อย่างชัดเจน
}
Option<T>
และ Result<T, E>
เป็นศูนย์กลางของแนวทางของ Rust ในการจัดการค่าที่อาจหายไปและข้อผิดพลาดอย่างมีเสถียรภาพ
Cargo เป็นส่วนที่ขาดไม่ได้ของระบบนิเวศ Rust ซึ่งช่วยปรับปรุงกระบวนการสร้าง ทดสอบ และจัดการโปรเจกต์ Rust ให้คล่องตัวขึ้น เป็นหนึ่งในคุณสมบัติที่นักพัฒนามักชื่นชม
Cargo.toml
: นี่คือไฟล์ manifest สำหรับโปรเจกต์ Rust ของคุณ เขียนในรูปแบบ TOML (Tom's Obvious, Minimal Language) ประกอบด้วยข้อมูลเมตาที่สำคัญเกี่ยวกับโปรเจกต์ของคุณ (เช่น ชื่อ เวอร์ชัน และผู้เขียน) และที่สำคัญคือ รายการ การพึ่งพา (dependencies) (เครตภายนอกอื่น ๆ ที่โปรเจกต์ของคุณต้องใช้)
[package]
name = "my_project"
version = "0.1.0"
edition = "2021" # ระบุ Rust edition ที่จะใช้ (มีผลต่อคุณสมบัติภาษา)
# การพึ่งพาจะอยู่ด้านล่าง
[dependencies]
# ตัวอย่าง: เพิ่มเครต 'rand' สำหรับการสร้างเลขสุ่ม
# rand = "0.8.5"
# เมื่อคุณ build, Cargo จะดาวน์โหลดและคอมไพล์ 'rand' และการพึ่งพาของมัน
cargo new <project_name>
: สร้างโครงสร้างโปรเจกต์แอปพลิเคชันไบนารี (เรียกใช้งานได้) ใหม่cargo new --lib <library_name>
: สร้างโครงสร้างโปรเจกต์ไลบรารีใหม่ (เครตที่ออกแบบมาเพื่อใช้โดยโปรแกรมอื่น)cargo build
: คอมไพล์โปรเจกต์ของคุณและการพึ่งพา โดยค่าเริ่มต้น จะสร้างบิลด์ debug ที่ไม่ได้ปรับให้เหมาะสม ผลลัพธ์จะอยู่ในไดเรกทอรี target/debug/
cargo build --release
: คอมไพล์โปรเจกต์ของคุณโดยเปิดใช้งานการปรับให้เหมาะสม เหมาะสำหรับการแจกจ่ายหรือการทดสอบประสิทธิภาพ ผลลัพธ์จะอยู่ในไดเรกทอรี target/release/
cargo run
: คอมไพล์ (หากจำเป็น) และรันโปรเจกต์ไบนารีของคุณcargo check
: ตรวจสอบโค้ดของคุณเพื่อหาข้อผิดพลาดในการคอมไพล์อย่างรวดเร็วโดยไม่ต้องสร้างไฟล์ปฏิบัติการจริง โดยทั่วไปจะเร็วกว่า cargo build
มากและมีประโยชน์ระหว่างการพัฒนาเพื่อรับข้อเสนอแนะอย่างรวดเร็วcargo test
: รันการทดสอบใดๆ ที่กำหนดไว้ภายในโปรเจกต์ของคุณ (โดยปกติจะอยู่ในไดเรกทอรี src
หรือในไดเรกทอรี tests
แยกต่างหาก)เมื่อคุณเพิ่มการพึ่งพาลงในไฟล์ Cargo.toml
ของคุณ แล้วรันคำสั่ง เช่น cargo build
หรือ cargo run
, Cargo จะจัดการดาวน์โหลดเครตที่ต้องการ (และ การพึ่งพา ของมัน) จากคลังเก็บส่วนกลาง crates.io และคอมไพล์ทุกอย่างเข้าด้วยกันโดยอัตโนมัติ
คู่มือเริ่มต้นนี้ได้ครอบคลุมพื้นฐานที่จำเป็นเพื่อให้คุณเริ่มต้นได้ ภาษา Rust ยังมีคุณสมบัติที่ทรงพลังอีกมากมายให้สำรวจเมื่อคุณก้าวหน้า:
Result
enum อย่างเชี่ยวชาญ, การส่งต่อข้อผิดพลาดโดยใช้ตัวดำเนินการ ?
, และการกำหนดประเภทข้อผิดพลาดแบบกำหนดเองVec<T>
(อาร์เรย์/เวกเตอร์แบบไดนามิก), HashMap<K, V>
(แฮชแมป), HashSet<T>
, ฯลฯ<T>
)'a
) อย่างชัดเจนMutex
และ Arc
, และไวยากรณ์ async
/await
ที่ทรงพลังของ Rust สำหรับการเขียนโปรแกรมแบบอะซิงโครนัสRust นำเสนอข้อเสนอที่ไม่เหมือนใครและน่าสนใจ: ประสิทธิภาพดิบที่คาดหวังจากภาษาระดับต่ำเช่น C++ ควบคู่ไปกับการรับประกันความปลอดภัย ณ เวลาคอมไพล์ที่แข็งแกร่ง ซึ่งช่วยขจัดข้อบกพร่องทั่วไปทั้งประเภท โดยเฉพาะอย่างยิ่งที่เกี่ยวกับ การจัดการหน่วยความจำและการทำงานพร้อมกัน แม้ว่าช่วงการเรียนรู้จะเกี่ยวข้องกับการทำความเข้าใจแนวคิดใหม่ๆ เช่น ความเป็นเจ้าของและการยืม (และการรับฟังข้อความแสดงข้อผิดพลาดของคอมไพเลอร์ที่บางครั้งเข้มงวด) แต่ผลตอบแทนที่ได้คือซอฟต์แวร์ที่มีความน่าเชื่อถือสูง มีประสิทธิภาพ และมักจะง่ายต่อการบำรุงรักษาในระยะยาว ด้วยเครื่องมือที่ยอดเยี่ยมผ่าน Cargo และชุมชนที่ให้การสนับสนุน Rust จึงเป็นภาษาที่คุ้มค่าแก่การเรียนรู้
เริ่มต้นเล็กๆ ทดลองใช้ Cargo ยอมรับข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์ (แม้บางครั้งจะยาว) ของคอมไพเลอร์เป็นแนวทาง แล้วคุณจะอยู่บนเส้นทางสู่การเรียนรู้ภาษาที่ทรงพลังและได้รับความนิยมเพิ่มขึ้นเรื่อยๆ นี้ ขอให้สนุกกับการเขียน Rust!