Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

เก็บ 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();
}
Listing 8-1: สร้าง vector ว่างใหม่เพื่อเก็บค่า type 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];
}
Listing 8-2: สร้าง vector ใหม่ที่มีค่า

เพราะเราให้ค่า 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);
}
Listing 8-3: ใช้เมธอด 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."),
    }
}
Listing 8-4: ใช้ indexing syntax และใช้เมธอด 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);
}
Listing 8-5: พยายามเข้าถึง element ที่ index 100 ใน vector ที่มีห้า element

เมื่อเรารันโค้ดนี้ เมธอด [] แรกจะทำให้โปรแกรม 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}");
}
Listing 8-6: พยายามเพิ่ม element ให้ vector ขณะถือ reference ของ item

การ 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}");
    }
}
Listing 8-7: พิมพ์แต่ละ element ใน vector โดย iterate ผ่าน element โดยใช้ 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;
    }
}
Listing 8-8: Iterate ผ่าน mutable reference ของ element ใน vector

ในการเปลี่ยนค่าที่ 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),
    ];
}
Listing 8-9: ประกาศ enum เพื่อเก็บค่าของ type ต่างกันใน vector เดียว

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
}
Listing 8-10: แสดงตำแหน่งที่ vector และ element ของมันถูก drop

เมื่อ vector ถูก drop เนื้อหาทั้งหมดของมันก็ถูก drop ด้วย หมายความว่า integer ที่มันเก็บจะถูก cleanup borrow checker รับประกันว่า reference ใด ๆ ของเนื้อหา vector ถูกใช้แค่ขณะที่ vector เองยัง valid

มาไปที่ collection type ถัดไป — String!