เก็บ List ของค่าด้วย Vector
Collection type แรกที่เราจะดูคือ Vec<T> หรือที่รู้จักในชื่อ vector
Vector ให้คุณเก็บค่ามากกว่าหนึ่งค่าในโครงสร้างข้อมูลเดียวที่ใส่ค่าทั้งหมด
ติดกันในหน่วยความจำ Vector เก็บได้แค่ค่าของ type เดียวกัน พวกมันมีประโยชน์
เมื่อคุณมี list ของ item เช่น บรรทัดข้อความในไฟล์ หรือราคาของ item ใน
รถเข็นช็อปปิ้ง
สร้าง Vector ใหม่
ในการสร้าง vector ว่างใหม่ เราเรียกฟังก์ชัน Vec::new ดังที่แสดงใน
Listing 8-1
fn main() {
let v: Vec<i32> = Vec::new();
}
i32หมายเหตุว่าเราเพิ่ม type annotation ที่นี่ เพราะเราไม่ได้แทรกค่าใด ๆ เข้า
vector นี้ Rust ไม่รู้ว่าเราตั้งใจเก็บ element ชนิดอะไร นี่เป็นจุดสำคัญ
Vector ถูก implement โดยใช้ generic เราจะครอบคลุมวิธีใช้ generic กับ type
ของคุณเองในบทที่ 10 ตอนนี้ รู้ว่า type Vec<T> ที่ standard library ให้
เก็บ type ใดก็ได้ เมื่อเราสร้าง vector เพื่อเก็บ type เฉพาะ เราระบุ type
ภายใน angle bracket ใน Listing 8-1 เราบอก Rust ว่า Vec<T> ใน v จะ
เก็บ element ของ type i32
บ่อยกว่านั้น คุณจะสร้าง Vec<T> ด้วยค่าเริ่มต้น และ Rust จะ infer type
ของค่าที่คุณอยากเก็บ คุณจึงไม่ค่อยต้องทำ type annotation นี้ Rust สะดวก
ให้ macro vec! ซึ่งจะสร้าง vector ใหม่ที่เก็บค่าที่คุณให้ Listing 8-2
สร้าง Vec<i32> ใหม่ที่เก็บค่า 1, 2 และ 3 integer type คือ i32
เพราะนั่นคือ default integer type ดังที่เราพูดถึงในส่วน
“ชนิดข้อมูล” ของบทที่ 3
fn main() {
let v = vec![1, 2, 3];
}
เพราะเราให้ค่า i32 เริ่มต้น Rust infer ได้ว่า type ของ v คือ
Vec<i32> และไม่จำเป็นต้องมี type annotation ถัดไป เราจะดูวิธีแก้
vector
Update Vector
ในการสร้าง vector แล้วเพิ่ม element ให้มัน เราใช้เมธอด push ได้ ดังที่
แสดงใน Listing 8-3
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
push เพื่อเพิ่มค่าให้ vectorเช่นเดียวกับตัวแปรใด ๆ ถ้าเราอยากเปลี่ยนค่า เราต้องทำให้มัน mutable โดย
ใช้ keyword mut ดังที่พูดถึงในบทที่ 3 ตัวเลขที่เราใส่ภายในเป็น type
i32 ทั้งหมด และ Rust infer สิ่งนี้จากข้อมูล เราจึงไม่ต้องมี annotation
Vec<i32>
อ่าน Element ของ Vector
มีสองวิธีในการอ้างถึงค่าที่เก็บใน vector — ผ่าน indexing หรือใช้เมธอด
get ในตัวอย่างต่อไปนี้ เรา annotate type ของค่าที่ return จากฟังก์ชัน
เหล่านี้เพื่อความชัดเจนเพิ่ม
Listing 8-4 แสดงทั้งสองเมธอดของการเข้าถึงค่าใน vector ด้วย indexing
syntax และเมธอด get
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
get เข้าถึง item ใน vectorหมายเหตุรายละเอียดบางอย่างที่นี่ เราใช้ค่า index 2 เพื่อรับ element ที่
สาม เพราะ vector ถูก index ด้วยตัวเลข เริ่มที่ศูนย์ การใช้ & และ []
ให้ reference ของ element ที่ค่า index เมื่อเราใช้เมธอด get ด้วย index
ที่ส่งเป็น argument เราได้ Option<&T> ที่เราใช้กับ match ได้
Rust ให้สองวิธีในการอ้างถึง element เพื่อให้คุณเลือกว่าโปรแกรมทำอย่างไร เมื่อคุณพยายามใช้ค่า index นอก range ของ element ที่มีอยู่ ตัวอย่าง มาดู ว่าเกิดอะไรขึ้นเมื่อเรามี vector ห้า element แล้วเราพยายามเข้าถึง element ที่ index 100 ด้วยแต่ละเทคนิค ดังที่แสดงใน Listing 8-5
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
เมื่อเรารันโค้ดนี้ เมธอด [] แรกจะทำให้โปรแกรม panic เพราะมันอ้างถึง
element ที่ไม่มี เมธอดนี้ใช้ดีที่สุดเมื่อคุณอยากให้โปรแกรมของคุณ crash
ถ้ามีการพยายามเข้าถึง element หลังท้าย vector
เมื่อเมธอด get ถูกส่ง index ที่อยู่นอก vector มัน return None โดยไม่
panic คุณจะใช้เมธอดนี้ถ้าการเข้าถึง element นอก range ของ vector อาจ
เกิดเป็นครั้งคราวภายใต้สถานการณ์ปกติ จากนั้นโค้ดของคุณจะมี logic จัดการ
การมี Some(&element) หรือ None ดังที่พูดถึงในบทที่ 6 เช่น index
อาจมาจากคนที่ป้อนตัวเลข ถ้าเขาเผลอป้อนตัวเลขที่ใหญ่เกินไป และโปรแกรมได้
ค่า None คุณบอก user ได้ว่ามีกี่ item ใน vector ปัจจุบัน และให้โอกาส
อีกครั้งในการป้อนค่าที่ valid นั่นจะเป็นมิตรกับ user มากกว่าการ crash
โปรแกรมเพราะ typo!
เมื่อโปรแกรมมี reference ที่ valid borrow checker บังคับใช้กฎ ownership และ borrowing (ครอบคลุมในบทที่ 4) เพื่อรับประกันว่า reference นี้และ reference อื่นใดของเนื้อหา vector ยัง valid จำกฎที่ระบุว่าคุณมี mutable และ immutable reference ใน scope เดียวไม่ได้ กฎนั้นใช้ใน Listing 8-6 ที่ เราถือ immutable reference ของ element แรกใน vector และพยายามเพิ่ม element ที่ท้าย โปรแกรมนี้จะไม่ทำงานถ้าเราพยายามอ้างถึง element นั้นทีหลัง ในฟังก์ชันด้วย
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
การ compile โค้ดนี้จะให้ error นี้:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {first}");
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
โค้ดใน Listing 8-6 อาจดูเหมือนน่าจะทำงานได้ — ทำไม reference ของ element แรกควรห่วงเรื่องการเปลี่ยนที่ท้ายของ vector? Error นี้เกิดจากวิธีที่ vector ทำงาน — เพราะ vector ใส่ค่าติดกันในหน่วยความจำ การเพิ่ม element ใหม่ที่ท้าย vector อาจต้องการ allocate หน่วยความจำใหม่ และคัดลอก element เก่าไปยังพื้นที่ใหม่ ถ้าไม่มีที่พอที่จะใส่ element ทั้งหมดติดกันที่ vector ถูกเก็บปัจจุบัน ในกรณีนั้น reference ของ element แรกจะชี้ไปยัง หน่วยความจำที่ deallocate กฎ borrowing ป้องกันโปรแกรมจากการจบลงในสถานการณ์ นั้น
หมายเหตุ: สำหรับข้อมูลเพิ่มเติมเรื่องรายละเอียด implementation ของ type
Vec<T>ดู “The Rustonomicon”
Iterate ผ่านค่าใน Vector
ในการเข้าถึงแต่ละ element ใน vector ทีละตัว เราจะ iterate ผ่าน element
ทั้งหมด แทนการใช้ index เข้าถึงทีละตัว Listing 8-7 แสดงวิธีใช้ for loop
เพื่อรับ immutable reference ของแต่ละ element ใน vector ของค่า i32
และพิมพ์พวกมัน
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
for loopเรายัง iterate ผ่าน mutable reference ของแต่ละ element ใน vector
mutable เพื่อทำการเปลี่ยนกับ element ทั้งหมดได้ for loop ใน Listing 8-8
จะเพิ่ม 50 ให้แต่ละ element
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
ในการเปลี่ยนค่าที่ mutable reference อ้างถึง เราต้องใช้ dereference
operator * เพื่อรับค่าใน i ก่อนเราใช้ operator += ได้ เราจะพูดถึง
dereference operator เพิ่มในส่วน
“ตาม Reference ไปยังค่า” ของบทที่ 15
การ iterate ผ่าน vector ไม่ว่า immutable หรือ mutable ปลอดภัยเพราะกฎ
ของ borrow checker ถ้าเราพยายามแทรกหรือลบ item ใน body ของ for loop
ใน Listing 8-7 และ Listing 8-8 เราจะได้ compiler error คล้ายกับที่เราได้
กับโค้ดใน Listing 8-6 reference ของ vector ที่ for loop ถือ ป้องกัน
การแก้ทั้ง vector พร้อมกัน
ใช้ Enum เก็บ Type หลายตัว
Vector เก็บได้แค่ค่าของ type เดียวกัน นี่อาจไม่สะดวก มีแน่นอน use case ที่ต้องการเก็บ list ของ item ที่ต่างกัน โชคดี variant ของ enum ถูก ประกาศใต้ enum type เดียวกัน ดังนั้นเมื่อเราต้องการ type หนึ่งแทน element ของ type ต่างกัน เราประกาศและใช้ enum ได้!
เช่น สมมติเราอยากรับค่าจาก row ใน spreadsheet ที่บาง column ใน row มี integer บางอันมี floating-point number และบางอันมี string เราประกาศ enum ที่ variant จะเก็บ value type ต่างกันได้ และ variant ของ enum ทั้งหมดจะถือเป็น type เดียวกัน คือของ enum จากนั้นเราสร้าง vector เก็บ enum นั้น และสุดท้ายเก็บ type ต่างกันได้ เราแสดงสิ่งนี้ใน Listing 8-9
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
Rust ต้องรู้ว่า type อะไรจะอยู่ใน vector ตอน compile time เพื่อให้รู้ว่า
ต้องการหน่วยความจำเท่าไรบน heap ในการเก็บแต่ละ element เราต้อง explicit
ด้วยว่า type อะไรอนุญาตใน vector นี้ ถ้า Rust อนุญาตให้ vector เก็บ type
ใด ๆ จะมีโอกาสที่ type หนึ่งหรือมากกว่าจะทำให้เกิด error กับ operation
ที่ทำบน element ของ vector การใช้ enum บวก match expression หมายความ
ว่า Rust จะรับประกันที่ compile time ว่าทุกกรณีที่เป็นไปได้ถูกจัดการ ดัง
ที่พูดถึงในบทที่ 6
ถ้าคุณไม่รู้ชุด exhaustive ของ type ที่โปรแกรมจะได้ตอน runtime เพื่อเก็บ ใน vector เทคนิค enum จะไม่ทำงาน แทน คุณใช้ trait object ได้ ซึ่งเราจะ ครอบคลุมในบทที่ 18
ตอนนี้เราพูดถึงวิธีที่ใช้ vector บ่อยที่สุดบางตัวแล้ว อย่าลืมทบทวน
API documentation สำหรับเมธอดที่มีประโยชน์
มากมายทั้งหมดที่ประกาศบน Vec<T> โดย standard library เช่น เพิ่มเติม
จาก push เมธอด pop ลบและ return element สุดท้าย
การ Drop Vector ทำให้ Drop Element ของมัน
เหมือนกับ struct อื่นใด vector ถูก free เมื่อมันออกจาก scope ดังที่
annotate ใน Listing 8-10
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
เมื่อ vector ถูก drop เนื้อหาทั้งหมดของมันก็ถูก drop ด้วย หมายความว่า integer ที่มันเก็บจะถูก cleanup borrow checker รับประกันว่า reference ใด ๆ ของเนื้อหา vector ถูกใช้แค่ขณะที่ vector เองยัง valid
มาไปที่ collection type ถัดไป — String!