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

การจัดเก็บข้อมูลเป็นรายการด้วยเวกเตอร์ (Storing Lists of Values with Vectors)

คอลเลกชันประเภทแรกที่เราจะมาดูกันคือ Vec<T> หรือที่รู้จักกันในชื่อเวกเตอร์ (vector) เวกเตอร์ช่วยให้คุณสามารถจัดเก็บข้อมูลได้มากกว่าหนึ่งค่าไว้ในโครงสร้างข้อมูลเดียว โดยระบบจะจัดเก็บค่าทั้งหมดให้อยู่ตำแหน่งติดกันในหน่วยความจำ เวกเตอร์สามารถจัดเก็บได้เฉพาะค่าที่เป็นประเภทข้อมูล (type) เดียวกันเท่านั้น เวกเตอร์จะมีประโยชน์มากเมื่อคุณมีรายการสิ่งของต่าง ๆ เช่น บรรทัดข้อความในไฟล์ หรือราคาสินค้าในรถเข็นช้อปปิ้ง

การสร้างเวกเตอร์ใหม่ (Creating a New Vector)

ในการสร้างเวกเตอร์ใหม่ที่ยังไม่มีข้อมูล เราจะเรียกใช้ฟังก์ชัน Vec::new ดังที่แสดงในตัวอย่างที่ 8-1 (Listing 8-1)

fn main() {
    let v: Vec<i32> = Vec::new();
}

สังเกตว่าเราได้เพิ่มการระบุประเภทข้อมูล (type annotation) ไว้ที่นี่ เนื่องจากเรายังไม่ได้ใส่ค่าใด ๆ ลงไปในเวกเตอร์นี้ Rust จึงไม่รู้ว่าเราต้องการจัดเก็บข้อมูลประเภทใด นี่คือจุดสำคัญ เวกเตอร์ถูกเขียนขึ้นโดยใช้เจเนอริก (generics) ซึ่งเราจะมาเรียนรูวิธีกำหนดเจเนอริกกับประเภทข้อมูลของเราเองในบทที่ 10 สำหรับตอนนี้ ขอให้ทราบว่าประเภทข้อมูล Vec<T> ที่จัดเตรียมไว้โดยไลบรารีมาตรฐาน (standard library) สามารถเก็บข้อมูลประเภทใดก็ได้ เมื่อเราสร้างเวกเตอร์เพื่อจัดเก็บประเภทข้อมูลเฉพาะเจาะจง เราสามารถระบุประเภทข้อมูลนั้นไว้ในวงเล็บสามเหลี่ยม (angle brackets) ได้ ในตัวอย่างที่ 8-1 เราบอก Rust ว่า Vec<T> ใน v จะเก็บข้อมูลประเภท i32

แต่ส่วนใหญ่แล้ว คุณมักจะสร้าง Vec<T> พร้อมกับค่าเริ่มต้น และ Rust จะสามารถคาดเดา (infer) ประเภทข้อมูลที่คุณต้องการจัดเก็บได้เอง ดังนั้นคุณจึงไม่จำเป็นต้องระบุประเภทข้อมูลนี้บ่อยนัก Rust ได้จัดเตรียมมาโคร vec! เพื่อความสะดวก ซึ่งจะช่วยสร้างเวกเตอร์ใหม่พร้อมเก็บค่าที่คุณกำหนดให้ ตัวอย่างที่ 8-2 จะสร้าง Vec<i32> ใหม่ที่เก็บค่า 1, 2 และ 3 ประเภทของจำนวนเต็มคือ i32 เนื่องจากเป็นประเภทจำนวนเต็มเริ่มต้น ดังที่เราได้พูดคุยกันในส่วน “ประเภทข้อมูล (Data Types)” ของบทที่ 3

fn main() {
    let v = vec![1, 2, 3];
}

เนื่องจากเราได้ให้ค่าเริ่มต้นที่เป็น i32 แล้ว Rust จึงสามารถคาดเดาได้ว่าประเภทข้อมูลของ v คือ Vec<i32> และไม่จำเป็นต้องระบุประเภทข้อมูลอีก ถัดไปเราจะมาดูวิธีการแก้ไขข้อมูลในเวกเตอร์

การอัปเดตเวกเตอร์ (Updating a Vector)

ในการสร้างเวกเตอร์แล้วเพิ่มข้อมูลลงไป เราสามารถใช้เมธอด push ได้ ดังที่แสดงในตัวอย่างที่ 8-3

fn main() {
    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);
}

เช่นเดียวกับตัวแปรอื่น ๆ หากเราต้องการแก้ไขค่าของมันได้ เราจำเป็นต้องทำให้มันสามารถเปลี่ยนแปลงค่าได้ (mutable) โดยใช้คีย์เวิร์ด mut ดังที่กล่าวไปในบทที่ 3 ตัวเลขที่เราใส่ไว้ข้างในล้วนเป็นประเภท i32 ซึ่ง Rust คาดเดาได้จากข้อมูล ดังนั้นเราจึงไม่จำเป็นต้องใส่การระบุประเภท Vec<i32>

การอ่านข้อมูลจากเวกเตอร์ (Reading Elements of Vectors)

มีสองวิธีในการอ้างอิงถึงค่าที่จัดเก็บอยู่ในเวกเตอร์ ได้แก่ การใช้ดัชนี (indexing) หรือการใช้เมธอด get ในตัวอย่างต่อไปนี้ เราได้ระบุประเภทข้อมูลของค่าที่ส่งกลับมาจากฟังก์ชันเหล่านี้เพื่อความชัดเจนยิ่งขึ้น

ตัวอย่างที่ 8-4 แสดงทั้งสองวิธีในการเข้าถึงค่าในเวกเตอร์ โดยใช้ไวยากรณ์ดัชนี (indexing) และเมธอด 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."),
    }
}

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

Rust ได้จัดเตรียมสองวิธีนี้ในการอ้างอิงข้อมูลเพื่อให้คุณสามารถเลือกได้ว่า จะให้โปรแกรมทำงานอย่างไรเมื่อพยายามใช้ค่าดัชนีที่อยู่นอกช่วงข้อมูลที่มีอยู่ ตัวอย่างเช่น มาดูกันว่าจะเกิดอะไรขึ้นเมื่อเรามีเวกเตอร์ที่มีสมาชิก 5 ตัว แล้วเราพยายามเข้าถึงข้อมูลที่ดัชนี 100 ด้วยแต่ละวิธี ดังที่แสดงในตัวอย่างที่ 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) เนื่องจากมีการอ้างอิงถึงข้อมูลที่ไม่มีอยู่จริง วิธีนี้เหมาะอย่างยิ่งเมื่อคุณต้องการให้โปรแกรมของคุณทำงานล้มเหลว (crash) หากมีการพยายามเข้าถึงข้อมูลที่เกินขอบเขตของเวกเตอร์

แต่เมื่อเราส่งดัชนีที่อยู่นอกขอบเขตของเวกเตอร์ให้กับเมธอด get มันจะส่งกลับค่า None โดยไม่เกิด panic คุณควรเลือกใช้วิธีนี้หากการเข้าถึงข้อมูลนอกช่วงของเวกเตอร์อาจเกิดขึ้นได้ในสถานการณ์ปกติทั่วไป จากนั้นโค้ดของคุณก็จะมีตรรกะสำหรับจัดการกับกรณีที่เป็น Some(&element) หรือ None ตามที่คุยกันในบทที่ 6 ตัวอย่างเช่น ดัชนีดังกล่าวอาจมาจากผู้ใช้ป้อนตัวเลขเข้ามา หากพวกเขาเผลอป้อนตัวเลขที่มากเกินไปและโปรแกรมได้รับค่า None คุณก็สามารถแจ้งผู้ใช้ได้ว่าในเวกเตอร์มีจำนวนสมาชิกทั้งหมดเท่าใด และเปิดโอกาสให้พวกเขากรอกค่าใหม่อีกครั้ง ซึ่งวิธีนี้จะดูเป็นมิตรกับผู้ใช้มากกว่าการปล่อยให้โปรแกรมพังไปเพียงเพราะผู้ใช้พิมพ์ผิด!

เมื่อโปรแกรมมีข้อมูลอ้างอิงที่ถูกต้อง ระบบตรวจสอบการยืม (borrow checker) จะบังคับใช้กฎความเป็นเจ้าของและการยืม (ซึ่งได้อธิบายไว้ในบทที่ 4) เพื่อให้มั่นใจได้ว่าข้อมูลอ้างอิงนี้และข้อมูลอ้างอิงอื่น ๆ ที่ชี้ไปยังข้อมูลในเวกเตอร์ยังคงถูกต้องสมบูรณ์ ย้อนกลับไปดูกฎที่ว่า คุณไม่สามารถมีข้อมูลอ้างอิงแบบเปลี่ยนค่าได้ (mutable reference) และอ้างอิงแบบเปลี่ยนค่าไม่ได้ (immutable reference) อยู่ในขอบเขต (scope) เดียวกันได้ กฎนี้ถูกนำมาใช้ในตัวอย่างที่ 8-6 ที่เราถือครองข้อมูลอ้างอิงแบบเปลี่ยนค่าไม่ได้ของสมาชิกตัวแรกของเวกเตอร์ แล้วพยายามเพิ่มข้อมูลตัวใหม่เข้าไปที่ท้ายเวกเตอร์ โปรแกรมนี้จะทำงานไม่ได้ หากเราพยายามอ้างอิงถึงสมาชิกตัวแรกนั้นอีกครั้งในส่วนหลังจากนั้นของฟังก์ชัน

fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {first}");
}

การคอมไพล์โค้ดนี้จะส่งผลให้เกิดข้อผิดพลาดต่อไปนี้:

$ 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

โค้ดในตัวอย่างที่ 8-6 อาจดูเหมือนควรจะทำงานได้: ทำไมข้อมูลอ้างอิงถึงสมาชิกตัวแรกถึงต้องมาสนใจกับการเปลี่ยนแปลงที่ท้ายเวกเตอร์ด้วยล่ะ? ข้อผิดพลาดนี้เกิดจากหลักการทำงานของเวกเตอร์: เนื่องจากเวกเตอร์จะวางข้อมูลเรียงต่อกันในหน่วยความจำ การเพิ่มสมาชิกใหม่เข้าไปที่ท้ายเวกเตอร์อาจทำให้ต้องจัดสรรพื้นที่หน่วยความจำใหม่ (allocating new memory) และคัดลอกสมาชิกตัวเก่าไปยังพื้นที่ใหม่ หากพื้นที่เดิมที่เวกเตอร์จัดเก็บอยู่ไม่มีที่ว่างเพียงพอที่จะวางสมาชิกทั้งหมดเรียงต่อกันได้ ในกรณีเช่นนั้น ข้อมูลอ้างอิงถึงสมาชิกตัวแรกก็จะไปชี้ตำแหน่งหน่วยความจำที่ถูกคืนสิทธิ์ไปแล้ว (deallocated memory) กฎการยืมจึงเข้ามาเพื่อป้องกันไม่ให้โปรแกรมต้องไปตกอยู่ในสถานการณ์เช่นนั้น

หมายเหตุ: หากต้องการดูรายละเอียดเพิ่มเติมเกี่ยวกับการทำงานเบื้องหลังของประเภทข้อมูล Vec<T> โปรดดูที่ “The Rustonomicon”

การวนลูปผ่านค่าต่าง ๆ ในเวกเตอร์ (Iterating Over the Values in a Vector)

เพื่อเข้าถึงแต่ละสมาชิกในเวกเตอร์ตามลำดับ เราจะวนลูปผ่านสมาชิกทั้งหมดแทนที่จะใช้ดัชนีเข้าถึงทีละตัว ตัวอย่างที่ 8-7 แสดงวิธีการใช้ลูป for เพื่อรับข้อมูลอ้างอิงแบบแก้ไขไม่ได้ไปยังแต่ละสมาชิกของเวกเตอร์ประเภท i32 แล้วนำมาแสดงผล

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}

นอกจากนี้ เรายังสามารถวนลูปผ่านข้อมูลอ้างอิงแบบแก้ไขได้ (mutable reference) ของแต่ละสมาชิกในเวกเตอร์ที่สามารถแก้ไขได้ เพื่อเปลี่ยนแปลงข้อมูลของสมาชิกทุกตัวได้ด้วย ลูป for ในตัวอย่างที่ 8-8 จะบวกค่าเพิ่มเข้าไป 50 ในสมาชิกแต่ละตัว

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

ในการเปลี่ยนค่าที่ข้อมูลอ้างอิงแบบแก้ไขได้ชี้อยู่ เราจำเป็นต้องใช้ตัวดำเนินการหาค่าจากตำแหน่งอ้างอิง (dereference operator) * เพื่อเข้าถึงค่าจริงในตัวแปร i ก่อน จากนั้นเราจึงจะสามารถใช้ตัวดำเนินการ += ได้ เราจะอธิบายรายละเอียดเกี่ยวกับตัวดำเนินการ dereference เพิ่มเติมในส่วน “การตามรอยข้อมูลอ้างอิงไปยังค่าจริง (Following the Reference to the Value)” ของบทที่ 15

การวนลูปเข้าถึงข้อมูลในเวกเตอร์ ไม่ว่าจะเป็นแบบอ่านอย่างเดียวหรือแบบแก้ไขได้ จะมีความปลอดภัยสูงเนื่องจากกฎของ borrow checker หากเราพยายามใส่ข้อมูลเพิ่มหรือนำข้อมูลออกภายในโครงสร้างของลูป for ในตัวอย่างที่ 8-7 และตัวอย่างที่ 8-8 เราจะเจอกับข้อผิดพลาดของคอมไพเลอร์ที่คล้ายกับข้อผิดพลาดที่พบจากโค้ดในตัวอย่างที่ 8-6 ข้อมูลอ้างอิงของเวกเตอร์ที่ลูป for ถือครองไว้นั้นจะช่วยป้องกันไม่ให้เกิดการแก้ไขตัวเวกเตอร์ทั้งหมดไปพร้อม ๆ กัน

การใช้ Enum เพื่อเก็บข้อมูลหลายประเภท (Using an Enum to Store Multiple Types)

เวกเตอร์สามารถจัดเก็บได้เฉพาะข้อมูลที่เป็นประเภทเดียวกันเท่านั้น ซึ่งอาจจะไม่สะดวกในบางกรณี เพราะมีสถานการณ์ที่เราต้องการจัดเก็บรายการของข้อมูลที่มีหลายประเภทผสมกันอย่างแน่นอน โชคดีที่ค่าตัวแปรย่อย (variants) ของ enum นั้นถูกกำหนดไว้ภายใต้ประเภท enum เดียวกัน ดังนั้นเมื่อเราต้องการประเภทข้อมูลเดียวเพื่อเป็นตัวแทนของข้อมูลหลายประเภท เราจึงสามารถกำหนดและใช้งาน enum ได้!

ตัวอย่างเช่น สมมติว่าเราต้องการดึงข้อมูลจากแถวหนึ่งในสเปรดชีต (spreadsheet) ซึ่งบางคอลัมน์ในแถวนั้นอาจเก็บจำนวนเต็ม บางคอลัมน์เก็บตัวเลขทศนิยม และบางคอลัมน์เก็บข้อความ เราสามารถกำหนด enum ที่มีตัวแปรย่อยต่าง ๆ เพื่อจัดเก็บข้อมูลประเภทที่แตกต่างกันเหล่านี้ได้ และตัวแปรย่อยทั้งหมดของ enum นั้นจะถูกมองว่าเป็นประเภทข้อมูลเดียวกัน นั่นคือประเภทข้อมูลของ enum นั้นเอง จากนั้น เราจะสามารถสร้างเวกเตอร์สำหรับจัดเก็บ enum ดังกล่าวได้ ซึ่งท้ายที่สุดแล้วก็จะช่วยให้เก็บข้อมูลหลากประเภทได้นั่นเอง ดังที่แสดงไว้ในตัวอย่างที่ 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 จำเป็นต้องรู้ว่าจะมีประเภทข้อมูลใดบ้างอยู่ในเวกเตอร์ในขั้นตอนการคอมไพล์ (compile time) เพื่อที่มันจะได้รู้ขนาดหน่วยความจำบนฮีป (heap) ที่ต้องจัดสรรได้อย่างแม่นยำสำหรับใช้จัดเก็บสมาชิกแต่ละตัว นอกจากนี้ เราต้องระบุอย่างชัดเจนว่าประเภทข้อมูลใดบ้างที่ได้รับอนุญาตให้เก็บอยู่ในเวกเตอร์นี้ หาก Rust ยอมให้เวกเตอร์เก็บข้อมูลประเภทใดก็ได้ ก็มีความเสี่ยงที่ประเภทข้อมูลบางตัวอาจก่อให้เกิดข้อผิดพลาดเมื่อทำตามคำสั่งกับสมาชิกต่าง ๆ ของเวกเตอร์นั้น การใช้งาน enum ร่วมกับนิพจน์ match จะช่วยให้ Rust มั่นใจได้ตั้งแต่ตอนคอมไพล์ว่าทุกกรณีที่เป็นไปได้จะถูกจัดการอย่างถูกต้อง ดังที่เราพูดคุยกันในบทที่ 6

หากคุณไม่ทราบประเภทข้อมูลทั้งหมดล่วงหน้าในระหว่างการพัฒนาเพื่อนำมาเก็บในเวกเตอร์ตอนรันโปรแกรม (runtime) เทคนิคการใช้ enum จะไม่สามารถตอบโจทย์ได้ ในกรณีนั้น คุณสามารถเลือกใช้งานทริตอ็อบเจกต์ (trait object) แทนได้ ซึ่งเราจะมาศึกษาในบทที่ 18

เมื่อเราได้พูดคุยเกี่ยวกับวิธีการยอดนิยมในการใช้งานเวกเตอร์กันไปแล้ว อย่าลืมเข้าไปศึกษา เอกสารอ้างอิง API (API documentation) เพิ่มเติมเกี่ยวกับเมธอดที่มีประโยชน์อื่น ๆ อีกมากมายสำหรับ Vec<T> ที่ไลบรารีมาตรฐานกำหนดไว้ ตัวอย่างเช่น นอกเหนือจากเมธอด push แล้ว ยังมีเมธอด pop ที่จะนำสมาชิกตัวสุดท้ายออกจากเวกเตอร์และส่งค่ากลับคืนมาให้ด้วย

การทำลายเวกเตอร์จะเป็นการทำลายสมาชิกในเวกเตอร์ไปด้วย (Dropping a Vector Drops Its Elements)

เช่นเดียวกับโครงสร้างข้อมูล (struct) อื่น ๆ เวกเตอร์จะถูกคืนพื้นที่หน่วยความจำเมื่อมันอยู่นอกขอบเขตการใช้งาน (out of scope) ดังที่ระบุไว้ในตัวอย่างที่ 8-10

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // do stuff with v
    } // <- v goes out of scope and is freed here
}

เมื่อเวกเตอร์ถูกทำลาย ข้อมูลทั้งหมดภายในเวกเตอร์จะถูกทำลายไปด้วย นั่นหมายความว่าค่าจำนวนเต็มต่าง ๆ ที่ถูกเก็บไว้จะถูกกวาดล้างและคืนหน่วยความจำ ระบบ borrow checker จะเป็นผู้รับประกันว่า ข้อมูลอ้างอิงใด ๆ ที่ชี้ไปยังข้อมูลของเวกเตอร์นั้นจะถูกใช้งานเฉพาะตอนที่ตัวเวกเตอร์เองยังมีสถานะใช้งานได้อยู่เท่านั้น

มาต่อกันที่คอลเลกชันประเภทถัดไปกันเลย นั่นก็คือ String!