Yineleyiciler ile Bir Dizi Öğeyi İşlemek

Bir koleksiyonun tüm elemanları tükenene kadar her bir elemanı üzerinde sırasıyla belirli işlemleri gerçekleştirmekten yineleyiciler sorumludurlar. Yineleyici kullandığınızda bütün bu işlemlerin her birini tekrar tekrar gerçekleştirmek zorunda kalmazsınız.

Rust'ta yineleyiciler tembel olduklarından, kendilerini tüketen yöntemler çağırılana kadar programlarınızı etkilemezler. Örneğin aşağıdaki kod; Vec<T> üzerinde tanımlanan iter yöntemini çağırarak v1 vektöründeki öğeler üzerinde bir yineleyici oluşturur. Bu kod tek başına anlamlı bir şey gerçekleştirmez.

Dosya adı: src/main.rs

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

    let v1_iter = v1.iter();
}

Örnek 13-13: Bir yineleyici oluşturmak

Bir yineleyici oluşturduktan sonra artık bunu çeşitli şekillerde kullanabiliriz. Bölüm 3'ün sonlarında yer alan örnek 3-5'te, her eleman üzerinde bir takım işlemleri gerçekleştirmek amacıyla for döngüsünün tüketimine verilen iter tanımlamasıyla yineleyici kullanmış, ancak yineleyicilere şu ana kadar derinlemesine odaklanamamıştık.

Aşağıdaki örnekte, yineleyicinin oluşturulması ile for döngüsünde kullanımı birbirinden bağımsız olarak sunulmaktadır. Kendi başına v1_iter değişkeninde saklanan yineleyicinin tanımlandığı satırda hiç bir işlem gerçekleşmezken, bu değişkene adapte edilmiş yineleyiciyi kullanan bir for döngüsü ile çağrıldığında, kendisine geçirilmiş olan her öğenin değerini ekrana yazdıran bir döngü yineleyicisi olarak kullanışlı hale gelir.

Dosya adı: src/main.rs

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

    let v1_iter = v1.iter();

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

Örnek 13-14: Bir yineleyici for döngüsünde kullanmak

Standart kitaplıklarında yineleyici bulunmayan dillerde bu tarz bir işlev, olasılıkla dizinin ilk elemanıyla değerinin arttırıldığı bir sayaç değişkeniyle başlatılacak, dizinin sonuna erişilene kadar her eleman için değişken birer birer güncellenerek işletilecektir.

Oysa yineleyiciler bütün bu karmaşık sayım sürecindeki mantığı sizin için üstlenebilir, muhtemel kod tekrarlarını azaltarak potansiyel karışıklıkların üstesinden gelebilirler. Yineleyiciler sadece vektörler gibi indekslenebilir veri yapılarıyle değil, aynı mantığın uygulandığı pekçok farklı koleksiyon türüyle kullanılılırken de fazlasıyla esnektirler. Haydi yineleyicilerin bunu nasıl yaptığını birlikte inceleyelim.

Iterator Özelliği ve next Metodu

Tüm yineleyiciler standart kitaplıkta tanımlanan Iterator adlı bir özelliği uygular. Özelliğin tanımı şuna benzer:

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

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

        // Şu an için varsayılan uygulamaları gösterilmeyen metodlar
    }
}

Bu tanımda type Item ve Self::Item gibi bu özelliklerle ilişkilendirilmiş türü bildiren yeni söz dizimleri kullanıldığına dikkat edin. İlişkili türlerden bölüm 19’da ayrıntılı olarak bahsedeceğimizden şimdilik bilmeniz gereken tek şey; bu kodun yineleyici özelliğini (Itarator trait) uygulayabilmek için bir öğe türü (Item type) tanımlanması gerektiği ve bu öğe türünün next metodunun dönüş türünde kullanıldığını belirtmesidir. Başka bir deyişle öğe türü yineleyiciden döndürülen tür olacaktır.

Yineleyici özelliği uygulayıcılara sadece bir metodu tanımlamak için ihtiyaç duyar. Tanımlanan next metodu yineleme devam ettiği sürece öğeleri Some ile sarmalayarak birer birer döndürürken, yineleme sona erdiğinde None döndürecektir.

Yineleyicideki next metodunu doğrudan kendimiz de çağırabiliriz. Örnek 13-15'te, v1 vektöründe oluşturulan yineleyiciye yapılan çağrılardan next metoduna hangi değerlerin döndürüldüğü gösterilmektedir.

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);
    }
}

fn main() {}

Örnek 13-15: Yineleyicideki next metodunu çağırmak

Öncelikle v1_iter değişmezinin mut anahtar sözcüğüyle değişebilir hale dönüştürülmesi gerektiğine dikkat edin. Bir yineleyicide next metodunun çağrılması, yineleyicinin dizide bulunduğu yeri izlemek için kullandığı iç konumu değiştirir. Başka bir deyişle, metodu çağıran kod yineleyiciyi tüketir veya kullanır. Her next metodu çağrısı yineleyicide bir öğenin tüketilmesine neden olur. Oysa v1_iter değişkeni for döngüsü ile kullanıldığında, değişkenin mülkiyeti döngüye aktarıldığından, durumu perde arkasında değişebilir olarak değiştirilir ve böylelikle v1_iter değişmezinin değişebilir olarak dönüştürülmesine gerek duyulmaz.

Ayrıca next metodu çağrılarından aldığımız değerlerin, vektör elemanlarının değişmez referansları olduğunu ve Iter metodunun değişmez referanslar üzerinde bir yineleyici oluşturduğunu unutmayın. Eğer v1 vektörünün mülkiyetini alarak, sahip olunan değerleri döndürecek bir yineleyici oluşturmak istiyorsanız, iter yerine into_iter metodunu çağırabilirsiniz. Benzer şekilde, değişebilir referanslar üzerinde yineleme yapmanız gerektiğinde, iter kullanmak yerine, iter_mut metodunu kullanabilirsiniz.

Yineleyici Tüketen Metodlar

Standart kitaplık tarafından sağlanan Iterator özelliğinin varsayılan uygulaması, halihazırda bir dizi metoda sahip olduğundan, bu metodlar hakkındaki bilgilere Iterator özelliğinin API belgelerini inceleyerek ulaşabilirsiniz. Bu metodlardan bazıları tanımlarında bulunan next metodunu çağırdığından Iterator özelliğini uygularken bu metodu da uygulamanız gerekmektedir.

Bu metodunu çağıran işlevler ise çağrıları sırasında yineleyiciyi kullandıklarından onlara tüketici adaptörleri adı verilir. Yineleyicinin mülkiyetini alarak her öğe için next() metod çağırısını yineleyen ve bu esnada yineleyiciyi tüketen sum metodu buna iyi bir örnektir. Bu metod yineleme süresince her elemanı toplama ekleyer ve yineleme sona erdiğinde bu toplamı döndürür. Örnek 13-16 sum metodu kullanımını örnekleyen 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);
    }
}

fn main() {}

Örnek 13-16: Yineleyicideki tüm öğelerin toplamını alan sum metodunu çağırmak

sum metodu, kendisine yapılan çağrı ile v1_iter değişmezindeki yineleyicinin mülkiyetini üzerine alacağından bu değişkenin kullanılmasına izin verilmez.

Diğer Yineleyicileri Üreten Metodlar

Iterator özelliğinde tanımlanan ve yineleyici adaptörleri adı verilen diğer yöntemler ise yineleyicileri farklı yineleyicilerle değiştirmenize olanak sağlarlar. Bu adaptörlere karmaşık eylemleri okunabilir şekilde gerçekleştirmek çok sayıda çağrı zincirleyebilirsiniz. Ancak tüm yineleyiciler tembel olduklarından, yineleyici adaptörlerine yapılan çağrılardan sonuç alabilmek için tüketici adaptörlerinden birini çağırmanız gerekir.

Örnek 13-17'de yeni bir yineleyici üretmek üzere her öğenin çağrıldığı bir kapama işlevine sahip yineleyici adaptörü olan map metodunun bir örneği gösterilir. Buradaki kapama işlevi vektördeki her öğe değerinin 1 arttırıldığı yeni bir yineleyici oluşturacaktır. Ancak bu kod bir uyarı vermektedir.

Dosya adı: src/main.rs


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

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

Bir yineleyici adaptörü olan map metodunu yeni bir yineleyici oluşturmak üzere çağırmak

Aldığımız uyarının çıktısı aşağıdaki gibidir:


$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `std::iter::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

    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

Örnek 13-17'de yer alan kod hiçbir şey yapmadığı gibi bildirdiğimiz kapama işlevi de hiçbir zaman çağrılmaz. Yineleyici adaptörleri tembel olduklarından, derleyicinin uyarısı bize yinelecinin tüketilmesi gerektiği uyarısını yapıyor.

Bu durumu düzeltmek ve yineleyiciyi kullanabilmek için Bölüm 12, Örnek 12-1'de yer alan ve env::args ile kullandığımız collect metodundan yararlanacağız. Bu metod yineleyiciyi tüketerek elde ettiği değerleri bir koleksiyon veri türüne depolar.

Örnek 13-18'de map metoduna yapılan çağrı vasıtasıyla yineleyici üzerinde yapılan yinelemeden döndürülen sonuçları bir vektörde topluyoruz. Sonuçların depolandığu bu vektör, orijinal vektördeki her öğenin değerine 1 eklenmiş sayılardan oluşmaktadır.

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]);
}

Örnek 13-18: Yeni bir yineleyici oluşturmak üzere map yöntemini ve üretilen bu yineleyiciyi vektör oluştururken tüketen collect yöntemini çağırmak

map metodu bir kapama işlevi aldığından, her bir öğe için uygulamak istediğimiz herhangi bir işlemi belirtebiliriz. Bu örnek, Iterator özelliğinin sağladığı yineleme davranışını yeniden kullanırken, kapamaların bazı davranışları özelleştirmenize nasıl izin verdiğini gösteren harika bir örnektir.

Ortamlarını Yakalayan Kapamalar Kullanmak

Artık yineleyicileri kullanıma sunduğumuza göre, bir yineleyici adaptörünü olan filter metodu kullanarak ortamlarını yakalayan kapamaların yaygın bir kullanımını gösterebiliriz. Bir yineleyicideki filter metodu, yineleyiciden aldığı her öğe karşılığında Boolean döndüren bir kapama işlevini kullanır. Kapama true döndürdüğünde, değer filter tarafından üretilen yineleyiciye dahil edilecek, false döndürdüğündeyse yineleyiciye dahil edilmeyecektir.

Örnek 13-19'da Shoe adlı yapı örneklerinden oluşan koleksiyon üzerinde yineleme yapmak üzere shoe_size değişkenini ortamından elde eden bir kapama işleviyle filter metodunu birlikte kullanıyor ve sadece belirtilen boyuttaki ayakkabıları döndürüyoruz.

Dosya adı: src/lib.rs

#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_my_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("Spor ayakkabı"),
            },
            Shoe {
                size: 13,
                style: String::from("Sandalet"),
            },
            Shoe {
                size: 10,
                style: String::from("Bot"),
            },
        ];

        let in_my_size = shoes_in_my_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("Spor ayakkabı")
                },
                Shoe {
                    size: 10,
                    style: String::from("Bot")
                },
            ]
        );
    }
}

fn main() {}

Örnek 13-19: shoe_size değerini ortamından yakalayan bir kapama ile filter metodunu birlikte kullanmak

shoes_in_my_size işlevi, parametre olarak bir ayakkabı vektörü ve bir ayakkabı numarasının mülkiyetini alarak sadece belirtilen ölçüdeki ayakkabıları içeren bir yeni vektör döndürür.

shoes_in_my_size işlevinin gövdesinde vektörün mülkiyetini alacak bir yineleyici oluşturmak üzere into_iter metodunu çağırıyor, sonra bu yineleyiciyi, kapamanın sadece true döndürdüğü öğelerden oluşan yeni bir yineleyiciye uyarlayamak amacıyla filter metodunu kullanıyoruz.

Ortamdan shoe_size parametresini yakalayan kapama, bu değeri her bir ayakkabının numarasıyla karşılaştırarak yalnızca belirtilen ölçüdeki ayakkabıları tutar. Son olarak, collect çağrısı, uyarlanmış yineleyici tarafından döndürülen değerleri işlev tarafından döndürülen bir vektöre depolar.

Örneğimizdeki test, shoes_in_my_size işlevini çağırdığımızda, yalnızca belirttiğimiz ölçülere uygun ayakkabıların döndürüldüğünü göstermektedir.

Iterator Özelliği ile Kendi Yineleyicilerimizi Oluşturmak

Bir vektör üzerinde iter, into_iter veya iter_mut metodlarını çağırarak bir yineleyici oluşturabileceğinizi gösterdik. Tıpkı bu vektör için oluşturduğumuz yineleyici gibi, standart kütüphanedeki eşleme haritaları veya diğer koleksiyon türleri için de yineleyiciler hazırlayabilir, Iterator özelliğini kendi türlerinize uygulayarak dilediğiniz işlemleri gefçekleştiren yineleyiciler oluşturabilirsiniz. Daha önce de belirtildiği gibi, tanımlamanız gereken tek metod next metodu olduğundan, bu metodu tanımladığınızda, Iterator özelliği tarafından sağlanan varsayılan uygulamalara sahip metodların tümünü kullanabilirsiniz!

Bu bilgiyi pekiştirmek adına, sadece 1'den 5'e kadar sayacak bir yineleyici oluşturalım. Öncelikle bunun için, bazı değerleri tutan bir yapı oluşturacak, ardından bu yapıya Iterator özelliğini uygulayacak ve bu uygulamadaki değerler vasıtasıyla bu yapıyı bir yineleyici haline getireceğiz.

Örnek 13-20, Counter yapısının tanımını ve bu yapının örneklerini oluşturmak amacıyla ilişkilendirilmiş new() işlevini göstermektedir:

Dosya adı: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

fn main() {}

Örnek 13-20: Counter yapısı ve count alanı başlangıç değeri 0 olan yapı örnekleri başlatan new işlevini tanımlamak

Counter yapısının count adlı bir alanı vardır. Bu alan, 1'den 5'e kadar olan yineleme sürecinde nerede olduğumuzu takip edecek u32 türünden bir değer tuttuğundan ve count uygulamasının değerini yöneteceğinden özeldir. new işlevi ise her yeni örnek başlatıldığında, başlatılan bu örnekleri count alanı sayesinde daima 0 değeriyle ilklendirmeye çalışır.

Daha sonra, Örnek 13-21'de gösterildiği gibi, bu yineleyici kullanıldığında üstleneceği görevleri bildiren next metodunun gövdesini tanımlayarak Counter türüne Iterator özelliğini uygulayacağız:

Dosya adı: src/lib.rs


struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

fn main() {}

Örnek 13-21: Counter yapısına Iterator özelliğini uygulamak

Yineleyicimiz için ilişkili öğe türünü u32 olarak belirleyip type Item = u32; şeklinde ayarladığımızdan yineleyiciden u32 türünde değerler döndürülecektir. Bu noktada İlişkili Türler konusunu Bölüm 19'da ele alacağımızı hatırlatarak konuya devam ediyoruz.

Yineleyicimizin mevcut duruma 1 eklemesini istediğimizden, 1'i döndürebilmesi için count u 0 olarak başlattık. count değeri 5'ten küçük olduğu sürece next metodu count değerini artırarak Some içine sarılmış geçerli değeri döndürecek, count değeri 5 olduğundaysa, yineleyicimiz count değerini artırmayı sona erdirerek None döndürmeye başlayacaktır.

Counter Yineleyicisinin next Metodunu Kullanmak

Iterator özelliğini uyguladığımıza göre artık elimizde bir yineleyicimiz var demektir. Tıpkı Örnek 13-15'te yaptığımız bir vektörden oluşturulan yineleyicide olduğu gibi, aşağıda yer alan Örnek 13-22 de, Counter yapısının yineleme işlevini next metodunu doğrudan çağırarak kullanabileceğimizi gösteren bir test bölümü içerir.

Dosya adı: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

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

    #[test]
    fn calling_next_directly() {
        let mut counter = Counter::new();

        assert_eq!(counter.next(), Some(1));
        assert_eq!(counter.next(), Some(2));
        assert_eq!(counter.next(), Some(3));
        assert_eq!(counter.next(), Some(4));
        assert_eq!(counter.next(), Some(5));
        assert_eq!(counter.next(), None);
    }
}

fn main() {}

Örnek 13-22: next metodu uygulamasının işlevselliğini test etmek

Bu test, counter değişkeninde yeni bir Counter örneği oluşturur. Ardından next metodunu defalarca çağırıp bu yineleyicinin sahip olmasını istediğimiz davranışı uyguladığımızı doğrulayam 1'den 5'e kadar olan değerleri döndürür.

Diğer Iterator Özellik Metodlarını Kullanmak

Artık next metodunu tanımlayarak Iterator özelliğini uyguladığımıza ve hepsinin next metodunun işlevselliğini kullandıklarını bildiğimize göre, bundan böyle standart kitaplıkta tanımlanan tüm Iterator özellik metodlarının varsayılan uygulamalarını kullanabiliriz.

Örnek 13-23'teki testte de gösterildiği gibi, bir Counter örneği tarafından üretilen değerleri almak istediğimizi, bunları ilk değeri atladıktan sonra başka bir Counter örneği tarafından üretilen değerlerle eşleştirdiğimizi, her çifti birbiriyle çarptığımızı ve elde edilen değerlerin sadece 3'e bölünebilenlerini alarak birbiriyle topladığımız bir örneği düşünün:

Dosya adı: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

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

    #[test]
    fn calling_next_directly() {
        let mut counter = Counter::new();

        assert_eq!(counter.next(), Some(1));
        assert_eq!(counter.next(), Some(2));
        assert_eq!(counter.next(), Some(3));
        assert_eq!(counter.next(), Some(4));
        assert_eq!(counter.next(), Some(5));
        assert_eq!(counter.next(), None);
    }

    #[test]
    fn using_other_iterator_trait_methods() {
        let sum: u32 = Counter::new()
            .zip(Counter::new().skip(1))
            .map(|(a, b)| a * b)
            .filter(|x| x % 3 == 0)
            .sum();
        assert_eq!(18, sum);
    }
}

fn main() {}

Örnek 13-23: Counter yineleyicisinde farklı Iterator özellik metodlarını kullanmak

zip metodu, girdi yineleyicilerinden herhangi birisinin None döndürmesi halinde None varyantını döndüreceğinden, zip ifadesinin yalnızca dört çift oluşturabildiğini ve teorik olarak beşinci çift (5, None) olacağından, hiçbir zaman üretilmeyeceğini aklınızdan çıkarmayın.

Standart kitaplık next metodunu çağıran diğer yöntemler için varsayılan uygulamaları sağladığından, next metodunun nasıl çalıştığını belirledikten sonra metod çağrılarının tamamını kullanmamız mümkündür.