C Sharp Programlama Dili/İteratörler ve Ardıllar
Bize art arda birden fazla nesne sunabilen tiplere ardıl denir. C# programlama dilinde ardıllar IEnumerable ve IEnumerable<T> arayüzleriyle temsil edilirler. Bu arayüzleri uygulayan tiplere ardıl özelliği gösteriyor denir. İteratörler ise ardılların art arda birden fazla nesne döndürebilmeleri için kullanılan yapılardır. Bir ardılı bir kitap gibi düşünürsek iteratör kitap ayracıdır. Kitap bir ardıldır. Çünkü her seferinde başka bir nesneyi (Sayfa) döndürebilir. Ancak her seferinde başka bir nesnenin döndürülebilmesi için kitap ayracına ihtiyacımız vardır. Kitap ayracı hangi sayfada kalındığı, sıradaki sayfanın talep edilmesi durumunda hangi sayfanın döndürüleceği bilgisini tutar. Ayrıca bir sayfa talep edildiğinde sıradaki sayfa varsa o sayfayı döndürüp kendini bir sonraki sayfaya atar. Eğer kitabın sonuna gelindiyse geriye sayfa döndürmez ve kitabın bittiği bilgisini döndürür. Aynı anda bir kitap üzerinde birden fazla ayraç dönebilir ve bunlar birbirinden bağımsız çalışır. Ayraçların hareketi kitapta ve diğer ayraçlarda herhangi bir değişikliğe neden olmaz. İteratörler .NET kütüphanesinde IEnumerator ve IEnumerator<T> arayüzleriyle temsil edilir. Şimdi bu verdiğimiz analojileri C# koduna dönüştürelim.
using System;
using System.Collections.Generic;
using System.Collections;
class Sayfa
{
public int No;
public Sayfa(int no)
{
No = no;
}
}
class Kitap : IEnumerable<Sayfa>
{
public string Ad;
public int SayfaSayisi;
public Kitap(string ad, int sayfaSayisi)
{
Ad = ad;
SayfaSayisi = sayfaSayisi;
}
public IEnumerator<Sayfa> GetEnumerator()
{
return new Ayrac(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
class Ayrac : IEnumerator<Sayfa>
{
int Indeks;
Kitap Kitap;
public Ayrac(Kitap kitap)
{
Kitap = kitap;
Indeks = 0;
}
public void Dispose() { }
public void Reset()
{
Indeks = 0;
}
public bool MoveNext()
{
Indeks++;
if (Indeks <= Kitap.SayfaSayisi)
return true;
else
return false;
}
public Sayfa Current
{
get
{
return new Sayfa(Indeks);
}
}
object IEnumerator.Current
{
get
{
return Current;
}
}
}
class MainMetodu
{
static void Main()
{
Kitap k = new Kitap("Çalıkuşu", 10);
foreach (Sayfa s in k)
{
Console.WriteLine(s.No);
}
}
}
Bu program 1'den 10'a kadar sayfa numaralarını teker teker ekrana yazacaktır. IEnumerable<T> arayüzü IEnumerable arayüzünden, IEnumerator<T> arayüzü ise IEnumerator arayüzünden türemiştir. Dolayısıyla IEnumerable<T> arayüzünü uygulayan bir sınıfın IEnumerable sınıfındaki üyeleri de, IEnumerator<T> arayüzünü uygulayan bir sınıfın da IEnumerator arayüzündeki üyeleri de uygulaması gerekmektedir. IEnumerable arayüzünde IEnumerator döndüren GetEnumerator() metodu bulunaktadır. IEnumerable<T> arayüzünde IEnumerator<T> döndüren GetEnumerator() metodu bulunmaktadır. IEnumerable<T> arayüzü IEnumerable arayüzünden türediğine göre burada IEnumerable arayüzünden türeyen GetEnumerator() metodunu da uygulamalıyız. Ancak her iki metodun da imzası aynı, sadece geri dönüş tipleri farklı. O yüzden aynı sınıfta her iki metodu da klasik şekilde uygulayamayız. Bu nedenle ana arayüzden devralınan üyeyi açık arayüz uygulama yöntemiyle uygularız. Aynı durum IEnumerator ve IEnumerator<T> arayüzleri için de geçerlidir. IEnumerator<T> arayüzünde iki kere tanımlanan üye Current özelliğidir.
IEnumerator<T> arayüzü iteratörü temsil eder. Yapıcı metodunda üzerinde dönülecek asıl nesneyi alır. Ayracın hangi kitap üzerinde döneceğini bilmesi gerekir. MoveNext() metodu hem iteratörün bir sonraki ögeye gidip gidemeyeceğini döndürür hem de bir sonraki ögeye gidilebiliyorsa bir sonraki ögeye gitmeyi sağlayan kodları içerir. Current özelliği sıradaki ögeyi döndürür, bir sonraki ögeye gitme işlemi yapmaz. Reset() metodu iteratörü ilk ögeye döndürme için gerekli kodları içerir. Dispose() metodu iteratörle ilgili boşaltılması gereken kaynaklar varsa bunları boşaltmaya yarar. Örneğimizde dispose edilecek bir şey yoktur. Ancak bu her zaman böyle olmayabilir. Örneğin her iterasyonda belirli bir klasördeki dosyalar sırayla açılıp içeriği ekrana yazılıyor olabilir. Eğer dosya açıldıktan sonra ve kapatılmadan önce herhangi bir istisna oluşursa dosyanın düzgünce kapatılması gerekir. Böyle bir durumda Dispose() metoduna dosyanın kapatılmasıyla ilgili kodlar eklenir.
foreach döngüsü
değiştirArdılların bir özelliği de örneğimizde görüldüğü gibi foreach döngüsüyle kullanılabilmeleridir. Bir tipin foreach döngüsünde kullanılabilmesi için IEnumerable veya IEnumerable<T> arayüzlerini uygulaması yeterlidir. foreach döngüsüyle her iterasyonda edindiğimiz değer aslında az önce Current özelliği ile döndürülen değerdir. foreach döngüsünün bir sonraki iterasyona geçip geçmemesine MoveNext() metodu ile karar verilir. foreach döngüsü aslında C# programlama dilinin bize sağladığı bir kısayoldur. Şu kod:
foreach (Sayfa s in k)
{
Console.WriteLine(s.No);
}
C# derleyicisi tarafından IL'e dönüştürülmeden önce şu koda dönüştürülür:
using (IEnumerator<Sayfa> enumerator = k.GetEnumerator())
{
while (enumerator.MoveNext())
{
Sayfa s = enumerator.Current;
Console.WriteLine(s.No);
}
}
Buradaki kodların using bloğu ile çevrili olması önemlidir. Eğer bloğun içinde herhangi bir istisna oluşursa enumerator nesnesine ilişkin Dispose() metodu çalışacaktır.
İteratörlerin daha kolay uygulanması
değiştirC# 1'de iteratörler yukarıda bahsedildiği gibi uygulanmaktaydı. Ancak bu kadar basit bir görev için bu kadar çok kod yazılmasına gerek olması iteratörleri C# 1'de kullanmamamıza neden oluyordu. Kütüphanede tanımlı ardılları foreach döngüsüyle rahatlıkla kullanabiliyorduk. Ancak kendi ardıllarımızı yazmıyorduk. C# 2 bu durumu kolaylaştırdı. C# 2'de artık çok daha rahatça IEnumerable<T> arayüzünü uygulayabiliyoruz. C# 2'de yukarıdaki Kitap sınıfındaki IEnumerator<Sayfa> döndüren GetEnumerator() metodunu şöyle yazabiliriz:
public IEnumerator<Sayfa> GetEnumerator()
{
for(int i=1;i<=SayfaSayisi;i++)
yield return new Sayfa(i);
}
Bu metot normal bir metot değildir, bir iteratör metodudur ve kodların sırayla çalıştığı ve kodlar bitince veya return anahtar sözcüğüyle karşılaşıldığında çıkılan normal bir metoda benzemez. Bu metodun normal bir metot olmadığını geri dönüş tipinden anlayabilirsiniz. Geri dönüş tipi IEnumerator<Sayfa> arayüzüdür. Hepimiz biliriz ki arayüzler soyut tiplerdir. Gerçek anlamda bir arayüz nesnesi oluşturulamaz. Mutlaka o arayüzü uygulayan bir sınıfımız olması ve o sınıf türünden nesne döndürmemiz gerekir. Peki burada neler dönüyor?
Burada neler döndüğünden açıklamadan önce metodun içindeki yield return
ifadesine odaklanalım. Bu ifade "sıradaki öge talep edildiğinde şimdilik bunu döndür, ama foreach döngüsündeki komutlar bittiğinde ve bir sonraki öge talep edildiğinde geri buraya döneceksin" demektedir. Bir de bunun gibi yield break
ifadesi vardır. Bu da "döndüreceğim ögeler bitti. foreach döngüsünün bir sonraki iteasyonuna girme" demektedir. Yani programın akışı foreach döngüsünün içeriği ile burası arasında gidip gelmektedir. foreach döngüsünün her iterasyonunda bir sonraki yield return çalışmaktadır. Bu, normal metotlarda olmayan bir şeydir.
yield return ve yield break ifadeleri için bazı kısıtlamalar bulunmaktadır. yield return, catch bloğu olan try bloklarında kullanılamaz. finally bloklarında yield return veya yield break kullanılamaz.
İteratör metotlarının geri dönüş tipi IEnumerator, IEnumerator<T>, IEnumerable veya IEnumerable<T> olabilir. Ayrıca metotlar yanında özellikler de iteratör olabilir. Yani özelliklerin içinde de yield return ve yield break kullanılabilir. yield return'ün döndürdüğü değer metodun (veya özelliğin) geri dönüş tipine uyumlu olmalıdır. Geri dönüş tipi IEnumerable veya IEnumerator ise yield return object döndürmelidir. Eğer bu arayüzlerin jenerik eşdeğerleri kullanılmışsa yield return tip parametresindeki tipten döndürmelidir. İteratör metotları normal return ifadesi içeremez. IEnumerable<T> tipinden bir örnek verelim:
IEnumerable<int> BasitArdil()
{
yield return 1;
yield return 2;
yield return 3;
}
Şimdiye kadar ardıl özelliği gösteren tiplerden konuşmuştuk. Halbuki bu metot direkt ardıl döndürüyor. Direkt bir arayüz nesnesi döndüremeyeceğimize göre burada da birtakım numaralar olmalı. Bu metot geriye 1, 2 ve 3 değerlerinden oluşan bir ardıl döndürür.
Bir nesnenin foreach döngüsünde döndürülebilmesi için bu nesnenin sınıfının/yapısının System.Collections isim alanındaki IEnumerable veya System.Collections.Generic isim alanındaki IEnumerable<T> arayüzünü uygulaması gerekir. IEnumerable arayüzünün içeriği şöyledir:
IEnumerator GetEnumerator();
Yani GetEnumerator() isminde tek bir metot içerir. Bu metodun geri dönüş tipi System.Collections isim alanındaki IEnumerator'dur. IEnumerable<T> arayüzünün içeriği ise şöyledir:
IEnumerator<T> GetEnumerator();
IEnumerator<T>, System.Collections.Generic isim alanındaki bir arayüzdür. Bu arayüz geri dönüş tipi IEnumerator olan ve parametre almayan GetEnumerator() metodunu içerir. IEnumerator yine System.Collections isim alanında bulunan bir arayüzdür. Bu metodun geriye bir IEnumerator nesnesi döndürebilmesi için bizim başka bir sınıfta IEnumerator arayüzünü uygulamamız ve bu metodun geriye bu sınıf türünden nesne döndürmesi gerekmektedir. IEnumerator arayüzü Reset() ve MoveNext() metotlarını ve Current sahte özelliğini içerir.
Bir nesne foreach döngüsünde döndüren konumuna koyulursa C# derleyicisi bu nesnenin GetEnumerator() metodunu çalıştırarak IEnumerator nesnesini elde eder. Bu nesnenin MoveNext() metodu döngüye girilmeden ve döngüde her bir iterasyondan sonra çalıştırılır ve sıradaki iterasyona girilip girilmeyeceğini döndürür. Current sahte özelliği foreach döngü bloğunun içinde dönen değişkenin ne döndüreceğini döndürür, sadece get bloğu olmalıdır, geri dönüş tipi object'tir. foreach döngüsünün parantezleri içine yazılan dönen tipine göre foreach döngü bloğunun içinde Current sahte özelliğinin döndürdüğü object nesneye unboxing yapılır. foreach döngüsünün her bir iterasyonunda dönenin farklı olmasını sağlayan şey MoveNext() metodudur. IEnumerator nesnesi döngünün hayatı boyunca bellekte kalır, döngüden çıkılınca bellekten silinir.
Şimdi dönenin kendi türümüz olmasını sağlayalım:
using System;
class A
{
public int Ozellik;
public A(int a)
{
Ozellik=a;
}
}
class Ana
{
static void Main()
{
A[] dizi=new A[3];
dizi[0]=new A(10);
dizi[1]=new A(20);
dizi[2]=new A(50);
foreach(A i in dizi)
Console.WriteLine(i.Ozellik);
}
}
Gördüğünüz gibi kendi oluşturduğumuz sınıf türünden nesnelerle diziler oluşturabiliyoruz. Nasıl ki int[] ile int türündeki veriler bir araya geliyorsa bizim örneğimizde de A türünden nesneler bir araya geldi.