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

ประมวลผลชุด Item ด้วย Iterator

pattern iterator ให้คุณทำงานบางอย่างบนลำดับของ item ทีละตัว iterator รับผิดชอบ logic ของการ iterate ผ่านแต่ละ item และกำหนดเมื่อลำดับจบ เมื่อคุณใช้ iterator คุณไม่ต้อง reimplement logic นั้นเอง

ใน Rust iterator เป็น lazy หมายความว่าพวกมันไม่มีผลจนกว่าคุณจะ เรียกเมธอดที่ consume iterator เพื่อใช้มันให้หมด ตัวอย่างเช่น โค้ดใน Listing 13-10 สร้าง iterator ผ่าน item ใน vector v1 โดยเรียก เมธอด iter ที่นิยามบน Vec<T> โค้ดนี้โดยตัวมันเองไม่ทำอะไรที่มี ประโยชน์

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();
}
Listing 13-10: สร้าง iterator

iterator ถูกเก็บในตัวแปร v1_iter เมื่อเราสร้าง iterator แล้ว เรา ใช้มันได้หลายวิธี ใน Listing 3-5 เรา iterate ผ่าน array โดยใช้ loop for เพื่อ execute โค้ดบางอย่างบนแต่ละ item ของมัน เบื้องหลัง นี่สร้างและจากนั้น consume iterator โดยปริยาย แต่เรามองข้ามว่ามัน ทำงานอย่างไรจนถึงตอนนี้

ในตัวอย่างใน Listing 13-11 เราแยกการสร้าง iterator จากการใช้ iterator ใน loop for เมื่อ loop for ถูกเรียกโดยใช้ iterator ใน v1_iter แต่ละ element ใน iterator ถูกใช้ใน iteration หนึ่งของ loop ซึ่ง print ออกแต่ละค่า

Filename: src/main.rs
fn main() {
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {val}");
    }
}
Listing 13-11: ใช้ iterator ใน loop for

ในภาษาที่ไม่มี iterator ที่ standard library ของพวกมันให้มา คุณคงจะ เขียน functionality เดียวกันนี้โดยเริ่มตัวแปรที่ index 0 ใช้ตัวแปร นั้น index เข้า vector เพื่อรับค่า และเพิ่มค่าตัวแปรใน loop จนกว่า มันถึงจำนวนรวมของ item ใน vector

iterator จัดการ logic ทั้งหมดนั้นให้คุณ ตัดโค้ดที่ซ้ำที่คุณอาจทำพัง iterator ให้คุณยืดหยุ่นมากขึ้นในการใช้ logic เดียวกันกับลำดับหลาย ประเภทต่างกัน ไม่ใช่แค่โครงสร้างข้อมูลที่คุณ index เข้าได้ เช่น vector มาตรวจสอบว่า iterator ทำอย่างนั้นได้อย่างไร

Trait Iterator และเมธอด next

iterator ทั้งหมด implement trait ชื่อ Iterator ที่นิยามใน standard library นิยามของ trait ดูแบบนี้:

#![allow(unused)]
fn main() {
pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // methods with default implementations elided
}
}

สังเกตว่านิยามนี้ใช้ syntax ใหม่บางอย่าง — type Item และ Self::Item ซึ่งกำลังนิยาม associated type กับ trait นี้ เราจะพูด ถึง associated type อย่างลึกในบทที่ 20 ตอนนี้ ทั้งหมดที่คุณต้องรู้ คือโค้ดนี้บอกว่าการ implement trait Iterator ต้องการให้คุณนิยาม type Item ด้วย และ type Item นี้ใช้ใน return type ของเมธอด next อีกแง่หนึ่ง type Item จะเป็น type ที่ return จาก iterator

trait Iterator ต้องการให้ผู้ implement นิยามเพียงหนึ่งเมธอด — เมธอด next ซึ่ง return หนึ่ง item ของ iterator ในแต่ละครั้ง wrapped ใน Some และเมื่อ iteration จบ return None

เราเรียกเมธอด next บน iterator โดยตรงได้ Listing 13-12 สาธิตค่า ที่ return จากการเรียก next ซ้ำบน iterator ที่สร้างจาก vector

Filename: src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];

        let mut v1_iter = v1.iter();

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
        assert_eq!(v1_iter.next(), None);
    }
}
Listing 13-12: เรียกเมธอด next บน iterator

สังเกตว่าเราต้องทำให้ v1_iter เป็น mutable — การเรียกเมธอด next บน iterator เปลี่ยน state ภายในที่ iterator ใช้เพื่อตามว่ามันอยู่ที่ ไหนในลำดับ อีกแง่หนึ่ง โค้ดนี้ consume หรือใช้ iterator หมด แต่ละ การเรียก next กิน item หนึ่งจาก iterator เราไม่ต้องทำให้ v1_iter เป็น mutable เมื่อเราใช้ loop for เพราะ loop รับ ownership ของ v1_iter และทำให้มัน mutable เบื้องหลัง

สังเกตด้วยว่าค่าที่เราได้จากการเรียก next เป็น immutable reference ของค่าใน vector เมธอด iter สร้าง iterator ผ่าน immutable reference ถ้าเราต้องการสร้าง iterator ที่รับ ownership ของ v1 และ return ค่าที่ own เราเรียก into_iter แทน iter ได้ ในทำนองเดียวกัน ถ้า เราต้องการ iterate ผ่าน mutable reference เราเรียก iter_mut แทน iter ได้

เมธอดที่ Consume Iterator

trait Iterator มีหลายเมธอดต่างกันที่มี implementation เริ่มต้นที่ standard library ให้มา — คุณค้นหาเกี่ยวกับเมธอดเหล่านี้ได้โดยดูใน API documentation ของ standard library สำหรับ trait Iterator บางเมธอดเหล่านี้เรียกเมธอด next ในนิยามของพวกมัน ซึ่งเป็นเหตุผล ที่คุณต้อง implement เมธอด next เมื่อ implement trait Iterator

เมธอดที่เรียก next ถูกเรียก consuming adapter เพราะการเรียก พวกมันใช้ iterator หมด ตัวอย่างหนึ่งคือเมธอด sum ซึ่งรับ ownership ของ iterator และ iterate ผ่าน item โดยเรียก next ซ้ำ ดังนั้น consume iterator เมื่อมัน iterate ผ่าน มันเพิ่มแต่ละ item ให้ running total และ return total เมื่อ iteration เสร็จ Listing 13-13 มีเทสแสดงการใช้เมธอด sum

Filename: src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }
}
Listing 13-13: เรียกเมธอด sum เพื่อรับ total ของ item ทั้งหมดใน iterator

เราไม่ได้รับอนุญาตให้ใช้ v1_iter หลังการเรียก sum เพราะ sum รับ ownership ของ iterator ที่เราเรียกมันบน

เมธอดที่ผลิต Iterator อื่น

Iterator adapter คือเมธอดที่นิยามบน trait Iterator ที่ไม่ consume iterator แทน พวกมันผลิต iterator ต่างกันโดยเปลี่ยนแง่มุม บางอย่างของ iterator เดิม

Listing 13-14 แสดงตัวอย่างของการเรียก iterator adapter เมธอด map ซึ่งรับ closure ที่จะเรียกบนแต่ละ item ขณะที่ item ถูก iterate เมธอด map return iterator ใหม่ที่ผลิต item ที่ถูกแก้ closure ที่นี่ สร้าง iterator ใหม่ที่แต่ละ item จาก vector จะถูกเพิ่มขึ้น 1

Filename: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);
}
Listing 13-14: เรียก iterator adapter map เพื่อสร้าง iterator ใหม่

อย่างไรก็ตาม โค้ดนี้สร้าง warning:

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
4 |     let _ = v1.iter().map(|x| x + 1);
  |     +++++++

warning: `iterators` (bin "iterators") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

โค้ดใน Listing 13-14 ไม่ทำอะไร — closure ที่เราระบุไม่เคยถูกเรียก warning เตือนเราทำไม — iterator adapter เป็น lazy และเราต้อง consume iterator ที่นี่

เพื่อแก้ warning นี้และ consume iterator เราจะใช้เมธอด collect ที่เราใช้กับ env::args ใน Listing 12-1 เมธอดนี้ consume iterator และ collect ค่าที่ได้เข้า collection data type

ใน Listing 13-15 เรา collect ผลของการ iterate ผ่าน iterator ที่ return จากการเรียก map เข้า vector vector นี้จะลงเอยที่บรรจุแต่ละ item จาก vector เดิม เพิ่มขึ้น 1

Filename: src/main.rs
fn main() {
    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);
}
Listing 13-15: เรียกเมธอด map เพื่อสร้าง iterator ใหม่ และจากนั้นเรียกเมธอด collect เพื่อ consume iterator ใหม่และสร้าง vector

เพราะ map รับ closure เราระบุ operation ใดก็ได้ที่เราต้องการทำบน แต่ละ item นี่เป็นตัวอย่างยอดเยี่ยมของวิธีที่ closure ให้คุณกำหนด พฤติกรรมเองในขณะที่ใช้พฤติกรรม iteration ที่ trait Iterator ให้มา ซ้ำ

คุณ chain การเรียก iterator adapter หลายตัวเพื่อทำการกระทำที่ซับซ้อน ในแบบที่อ่านได้ แต่เพราะ iterator ทั้งหมดเป็น lazy คุณต้องเรียกหนึ่ง ในเมธอด consuming adapter เพื่อรับผลจากการเรียก iterator adapter

Closure ที่จับ Environment ของพวกมัน

iterator adapter หลายตัวรับ closure เป็นอาร์กิวเมนต์ และโดยทั่วไป closure ที่เราจะระบุเป็นอาร์กิวเมนต์ให้ iterator adapter จะเป็น closure ที่จับ environment ของพวกมัน

สำหรับตัวอย่างนี้ เราจะใช้เมธอด filter ที่รับ closure closure รับ item จาก iterator และ return bool ถ้า closure return true ค่า จะถูกรวมใน iteration ที่ผลิตโดย filter ถ้า closure return false ค่าจะไม่ถูกรวม

ใน Listing 13-16 เราใช้ filter กับ closure ที่จับตัวแปร shoe_size จาก environment ของมันเพื่อ iterate ผ่าน collection ของ instance struct Shoe มันจะ return เฉพาะรองเท้าที่เป็นขนาดที่ระบุ

Filename: src/lib.rs
#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}
Listing 13-16: ใช้เมธอด filter กับ closure ที่จับ shoe_size

ฟังก์ชัน shoes_in_size รับ ownership ของ vector ของรองเท้าและขนาด รองเท้าเป็น parameter มัน return vector ที่บรรจุเฉพาะรองเท้าของขนาด ที่ระบุ

ใน body ของ shoes_in_size เราเรียก into_iter เพื่อสร้าง iterator ที่รับ ownership ของ vector จากนั้น เราเรียก filter เพื่อ adapt iterator นั้นเป็น iterator ใหม่ที่บรรจุเฉพาะ element ที่ closure return true

closure จับ parameter shoe_size จาก environment และเปรียบเทียบ ค่ากับขนาดของแต่ละรองเท้า เก็บเฉพาะรองเท้าของขนาดที่ระบุ สุดท้าย การเรียก collect รวมค่าที่ return โดย iterator ที่ adapt เข้า vector ที่ return โดยฟังก์ชัน

เทสแสดงว่าเมื่อเราเรียก shoes_in_size เราได้คืนเฉพาะรองเท้าที่ มีขนาดเดียวกับค่าที่เราระบุ