Yineleyicilerle Bir Öğe Serisini İşleme

Yineleyici deseni, sırayla bir dizi öğe üzerinde bazı görevleri gerçekleştirmenize olanak tanır. Bir yineleyici, her bir öğe üzerinde yineleme mantığından ve dizinin ne zaman bittiğini belirlemekten sorumludur. Yineleyicileri kullandığınızda, bu mantığı kendiniz yeniden uygulamak zorunda kalmazsınız.

Rust'ta yineleyiciler tembeldir, yani siz onu kullanmak için yineleyiciyi tüketen metodları çağırana kadar hiçbir etkileri yoktur. Örneğin, Liste 13-10'daki kod, Vec<T> üzerinde tanımlanan iter metodunu çağırarak v1 vektöründeki öğeler üzerinde bir yineleyici oluşturur. Bu kod kendi başına yararlı bir şey yapmaz.

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

    let v1_iter = v1.iter();
}

Liste 13-10: Yineleyici oluşturmak

Bir yineleyici oluşturduktan sonra, onu çeşitli şekillerde kullanabiliriz. Bölüm 3'teki Liste 3-5'te, öğelerinin her birinde bazı kodlar çalıştırmak için for döngüsü kullanarak bir dizi üzerinde yineleme yaptık. Bu dolaylı olarak bir yineleyici oluşturdu ve sonra üzerinden geçti, ancak şimdiye kadar bunun tam olarak nasıl çalıştığını geçtik.

Liste 13-11'deki örnek, yineleyicinin oluşturulmasını for döngüsündeki yineleyici kullanımından ayırır. Yineleyici v1_iter değişkeninde saklanır ve o sırada herhangi bir yineleme gerçekleşmez. v1_iter içindeki yineleyici kullanılarak for döngüsü çağrıldığında, yineleyicideki her öğe döngünün bir yinelemesinde kullanılır ve her değer yazdırılır.

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

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {}", val);
    }
}

Liste 13-11: Bir for döngüsünde yineleyici kullanma

Standart kütüphaneleri tarafından sağlanan yineleyicilere sahip olmayan dillerde, muhtemelen aynı fonksiyonu 0 indeksinde bir değişken başlatarak, bir değer elde etmek için vektörü indekslemek üzere bu değişkeni kullanarak ve vektördeki toplam öğe sayısına ulaşana kadar değişken değerini bir döngü içinde artırarak yazarsınız.

Yineleyiciler tüm bu mantığı sizin için halleder ve potansiyel olarak karıştırabileceğiniz tekrarlayan kodu azaltır. Yineleyiciler, aynı mantığı yalnızca vektörler gibi indeksleyebileceğiniz veri yapılarıyla değil, birçok farklı türde diziyle kullanmanız için size daha fazla esneklik sağlar. Yineleyicilerin bunu nasıl yaptığını inceleyelim.

Iterator Tanımı ve next Metodu

Tüm yineleyiciler, standart kütüphanede tanımlanan Iterator adlı tanımı sürekler. Tanımın tanımı şu şekildedir:


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

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

    // methods with default implementations elided
}
}

Bu tanımın bazı yeni söz dizimleri kullandığına dikkat edin: Item ve Self::Item türleri bu özellik ile ilişkili bir tür tanımlamaktadır. İlişkili türler hakkında Bölüm 19'da derinlemesine konuşacağız. Şimdilik bilmeniz gereken tek şey, bu kodun Iterator tanımını süreklemenin Item türünü de tanımlamanızı gerektirdiğini ve bu Item türünün bir sonraki metodun dönüş türünde kullanıldığını söylediğidir. Başka bir deyişle, Item türü yineleyiciden döndürülen tür olacaktır.

Iterator tanımı, sürekleyicilerin yalnızca bir metod tanımlamasını gerektirir: Some içine sarılmış olarak her seferinde yineleyicinin bir öğesini döndüren ve yineleme sona erdiğinde None döndüren next metodu.

Yineleyicilerde next metodunu doğrudan çağırabiliriz; Liste 13-12, vektörden oluşturulan yineleyicide next metodunun tekrarlanan çağrılarından hangi değerlerin döndürüldüğünü gösterir.

Dosya adı: 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);
    }
}

Liste 13-12: Yineleyici üzerinde next metodunun çağrılması

v1_iter'ı değiştirilebilir yapmamız gerektiğine dikkat edin: bir yineleyici üzerinde next metodunu çağırmak, yineleyicinin sıralamada nerede olduğunu takip etmek için kullandığı dahili durumu değiştirir. Başka bir deyişle, bu kod yineleyiciyi tüketir ya da kullanır. Her next çağrısı yineleyiciden bir öğe tüketir. Bir for döngüsü kullandığımızda v1_iter'ı değiştirilebilir yapmamıza gerek yoktu çünkü döngü v1_iter'ın sahipliğini aldı ve onu perde arkasında değiştirilebilir yaptı.

Ayrıca next çağrısından elde ettiğimiz değerlerin vektördeki değerlere değişmez referanslar olduğuna dikkat edin. iter metodu, değişmez referanslar üzerinde bir yineleyici üretir. Eğer v1'in sahipliğini alan ve sahip olunan değerleri döndüren bir yineleyici oluşturmak istiyorsak, iter yerine into_iter'ı çağırabiliriz. Benzer şekilde, değiştirilebilir referanslar üzerinde yineleme yapmak istiyorsak, iter yerine iter_mut metodunu çağırabiliriz.

Yineleyici Kullanan Metodlar

Iterator tanımı, standart kütüphane tarafından sağlanan varsayılan süreklemelere sahip bir dizi farklı metoda sahiptir; Iterator tanımı için standart kütüphane API dokümantasyonuna bakarak bu metodlar hakkında bilgi edinebilirsiniz. Bu metodlardan bazıları tanımlarında next metodunu çağırır, bu nedenle Iterator tanımını süreklerken next metodunu da süreklememiz gerekir.

next metodunu çağıran metodlara tüketim uyarlayıcıları denir, çünkü bunları çağırmak yineleyiciyi kullanır. Bunun bir örneği, yineleyicinin sahipliğini alan ve next metodunu tekrar tekrar çağırarak öğeler arasında yineleme yapan ve böylece yineleyiciyi tüketen sum metodudur. Yineleme sırasında, her öğeyi çalışan bir toplama ekler ve yineleme tamamlandığında toplamı döndürür. Liste 13-13, sum metodunun kullanımını gösteren bir test içerir:

Dosya adı: 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);
    }
}

Liste 13-13: Yineleyicideki tüm öğelerin toplamını almak için sum metodunu çağırma

sum çağrısından sonra v1_iter kullanmamıza izin verilmez çünkü sum, çağırdığımız yineleyicinin sahipliğini alır.

Diğer Yineleyicileri Üreten Metodlar

Iterator tanımı üzerinde tanımlanan ve yineleyici adaptorü olarak bilinen diğer metodlar, yineleyiciyi farklı yineleyici türlerine dönüştürmenize olanak tanır. Karmaşık eylemleri okunabilir bir şekilde gerçekleştirmek için yineleyici uyarlayıcılarına birden fazla çağrıyı zincirleyebilirsiniz. Ancak tüm yineleyiciler tembel olduğundan, yineleyici uyarlayıcılarına yapılan çağrılardan sonuç almak için tüketen uyarlayıcı metodlardan birini çağırmanız gerekir.

Liste 13-14, yeni bir yineleyici üretmek için her öğede çağrılacak bir kapanış alan yineleyici uyarlayıcı yöntemi olan map'i çağırmanın bir örneğini göstermektedir. Buradaki kapanış, vektördeki her öğenin 1 artırıldığı yeni bir yineleyici oluşturur. Ancak, bu kod bir uyarı üretir:

Dosya adı: src/main.rs

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

    v1.iter().map(|x| x + 1);
}

Liste 13-14: Yeni bir yineleyici oluşturmak için map yineleyici uyarlayıcısını çağırma

Aldığımız uyarı şudur:

$ 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: `#[warn(unused_must_use)]` on by default
  = note: iterators are lazy and do nothing unless consumed

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

Liste 13-14'teki kod hiçbir şey yapmaz; belirttiğimiz kapanış hiçbir zaman çağrılmaz. Uyarı bize nedenini gösteriyor: yineleyici uyarlayıcıları tembeldir ve burada yineleyiciyi tüketmemiz gerekir.

Bunu düzeltmek ve yineleyiciyi tüketmek için, Bölüm 12'de Liste 12-1'de env::args ile kullandığımız collect metodunu kullanacağız. Bu metod yineleyiciyi tüketir ve elde edilen değerleri bir koleksiyon veri türünde toplar.

Liste 13-15'te, map çağrısından döndürülen yineleyici üzerinde yineleme sonuçlarını bir vektörde topluyoruz. Bu vektör, orijinal vektördeki her bir öğenin 1 ile artırılmış halini içerecektir.

Dosya adı: 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]);
}

Liste 13-15: Yeni bir yineleyici oluşturmak için map metodunu çağırmak ve ardından yeni yineleyiciyi tüketmek ve bir vektör oluşturmak için collect metodunu çağırmak

map bir kapanış aldığı için, her bir öğe üzerinde gerçekleştirmek istediğimiz herhangi bir işlemi belirtebiliriz. Bu, kapanışların Iterator tanımını sağladığı yineleme davranışını yeniden kullanırken bazı davranışları özelleştirmenize nasıl izin verdiğinin harika bir örneğidir.

Ortamlarını Yakalayan Kapanışları Kullanma

Yineleyicileri tanıttığımıza göre, filter yineleyici adaptörünü kullanarak çevrelerini yakalayan kapanışların yaygın bir kullanımını gösterebiliriz. Bir yineleyici üzerindeki filter metodu, yineleyicideki her bir öğeyi alan ve bir Boole döndüren bir kapanış alır. Eğer kapanış true döndürürse, değer filter tarafından üretilen yineleyiciye dahil edilecektir. Kapanış false döndürürse, değer elde edilen yineleyiciye dahil edilmez.

Liste 13-16'da, Shoe struct örneklerinden oluşan bir koleksiyon üzerinde yineleme yapmak için ortamından shoe_size değişkenini yakalayan bir kapanış ile filter'ı kullanırız. Yalnızca belirtilen boyutta olan ayakkabıları döndürür.

Dosya adı: 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")
                },
            ]
        );
    }
}

Liste 13-16: shoe_size öğesini yakalayan bir kapanış ile filter metodunu kullanma

shoes_in_size fonksiyonu, parametre olarak bir ayakkabı vektörüne ve bir shoe_size'a yani ayakkabı boyutuna sahip olur. Yalnızca belirtilen boyuttaki ayakkabıları içeren bir vektör döndürür.

shoes_in_size'ın gövdesinde, vektörün sahipliğini alan bir yineleyici oluşturmak için into_iter'ı çağırıyoruz. Daha sonra bu yineleyiciyi yalnızca kapanışın true döndürdüğü öğeleri içeren yeni bir yineleyiciye uyarlamak için filter'ı çağırıyoruz.

Kapanış, shoe_size parametresini ortamdan alır ve değeri her shoe_size'ı karşılaştırarak yalnızca belirtilendekileri tutar. Son olarak, collect çağrısı, uyarlanmış yineleyici tarafından döndürülen değerleri, fonksiyon tarafından döndürülen bir vektörde toplar.

Test, shoes_in_size öğesini çağırdığımızda, yalnızca belirttiğimiz değerle aynı boyuta sahip ayakkabıları geri aldığımızı gösterir.