Enine Boyuna Generics – Bölüm 1

2.543 defa okunmuş
Oy Verin:
(3 kişi oy kullanmış, ortalama: 5 üzerinden 5,00)
Loading ... Loading ...

Bir iki önceki makalemizde, Delphi gramerine katılan generics ve parametrize tipleri yüzeysel olarak tanıtmıştık. Generics, RAD Studio 2007′ nin çıkması ile artık sadece beta tester’larını değil, tüm Delphi programcılarını ilgilendiren bir konu olmuştur. Gerçi şu an için sadece .NET ortamında kullanabildiğimiz bu özellik bir sonraki sürümde Win32 için de kullanılabilir bir gramer olacaktır. Başlangıç için önceki makalemizi okuyabilir ya da direk buradan başlayabilirsiniz. Zira bu bölümde Generics ile ilgili temel bilgiler vereceğiz.

Giriş

Generics’i doğru bir şekilde anlayabilmek için, hangi problemleri çözmek için dizayn edildiğini anlamakta yarar var. Bunun için ilk başta temel bir konudan ve bununla ilgili bir problemden bahsedelim.

.Net ortamı için object tipini herhangi bir değer ya da değişken için referans olarak kullanabilirsiniz. Çünkü her bir referans tipi otomatik olarak System.Object sınıfından türetiilir. Delphi’nin özel tipleri bile .net ortamına geçtiğinde artık birer object olacaktır. Fakat bazı durumlarda explicit casting yapmadan object sınıfına atama yapmak derleyicinin hata üretmesine neden olabilir (Explicit ve Implicit terimlerine yabancı iseniz buradaki makalede ilgili başlığı bulabilirsiniz.). Bunun için AUTOBOX direktifini kullanmamız gerekebilir. Yani aşağıdaki gibi, Delphi’ye ait özel bir tipi, .NET’in object tipine implicit olarak atama yaptığımızda derleyici hata üretecektir.

var
  i: Integer;
  o: &Object;
begin
  o := i;

Derleyicinin hata üretmemesi için ya AUTOBOX kullanacağız ya da explicit casting uygulayacağız:

var
  i: Integer;
  o: &Object;
begin
  o := &Object(i); //Explicit casting
  {$AUTOBOX ON}
  o := i;

Buradan çıkardığımız sonuç, her halükarda eğer .Net ortamından söz ediyorsak, ister Delphi tipi olsun ister olmasın, herşey &Object (System.Object) sınıfından türemiştir. Ve bir şekilde tüm tipler, object nesnesine atanabiliyor.

Biz bu bilgiliyi kullanarak sınıflarımızı ve metodlarımızı bir çok noktada genelleştirebiliriz. Mesela VCL’den hatırlayacağınız TList sınıfına benzer bir liste sınıfı hazırlayabilir ve object‘in yukarıdaki anlattığımız özelliği sayesinde içinde istediğimiz tipte nesneler barındırabiliriz. System.Collections alanında bulunan Queue(kuyruk) sınıfı da bu tarz bir yapıya sahiptir(Kuyruğa ilk giren ilk çıkar.). Mesela aşağıdaki örnekte iki farklı tipi Queue nesnesinin bir elemanı yapıyoruz.

uses
  System.Collections, System.Xml;
  ...

{$AUTOBOX ON}
var
  b: TButton;
  s: XmlDocument;
  q: Queue;
begin
  q := Queue.Create();
  b := TButton.Create(nil);
  x := XmlDocument;
  q.Enqueue(b);
  q.Enqueue(x);

Queue sınıfının Enqueue metodu, kuyruğa yeni bir object eklemeye yarar. Enqueue metodu object tipinde parametre aldığından her çeşit nesne bu metod ile kuyruğumuza eklenebilir. Bu örneğimizde TButton ve XmlDocument tipinde iki farklı nesneyi kuyruğa ekledik. Kuyruktan bir eleman çıkarmak için ise Dequeue metodunu kullanmamız gerekecektir. Dequeue metodu da aynen Enqueue metodu gibi object nesneleri ile çalışır. Yani Dequeue metodunun çıktısı bir object olacaktır. İşte bu noktada, object sınıfının özelliğinden dolayı Queue sınıfı ve bu iki metodu genel çapta kullanılabilir bir yapıdadır.

Yukarıdaki kodlara şunları ekleyip derlemeye çalışalım:

var
  ...
  ...
  cikanB: TButton;
  cikanX: XmlDocument;
begin
  ....
  ....
  cikanB := q.Dequeue; //ilk button girmişti, ilk de o çıkacak.
  cikanX := q.Dequeue;

Bu kodu derlemeye kalkdığınızda “Incompatible types…” hatasını alacaksınız. Halbuki yukarıda AUTOBOX ile implicit çevirimi aktifleştirmiştik. Peki bu hatayı neden aldık?

Birincisi, AUTOBOX sadece object nesnelerine atama yapılacağında işe yarıyor, object nesneleri başka bir nesneye atama yapılacağında değil. Object sınıfndan türemiş olan bir sınıfın nesnesi object‘e atama yapacağınızda, derleyici bunu otomatik çevirebilir. Ama aksi durumda, object nesnesinin hangi sınıftaki bir nesneye dönüşeceğini kestiremez. Bu durumda explicit çevirim yapmak zorundayız. Bunun için sondaki iki satırı şu şekilde değiştirmeliyiz:

  ...
  cikanB := TButton(q.Dequeue);
  cikanX := XmlDocument(q.Dequeue);
  ShowMessage(cikanX.Name); //"#document"

Peki kuyruktan çıkış sırasını yanlışlıkla değiştirseydik ne olurdu? Biliyoruz ki, kuyruk’ta ilk giren ilk çıkar. Yani örneğimize göre ilk olarak button girmişti ve ilk olarak da o çıkacaktır. Bu durumda sıralarını değiştirelim deneyelim:

  ...
  cikanX := XmlDocument(q.Dequeue);
  cikanB := TButton(q.Dequeue);
  ShowMessage(cikanX.Name);

Bu kod derlenir fakat çalışma anında yanlış yaptığımızı gösteren bir hata alırız. Bu durumda object sınıfının bu genelleştirme özelliğini, bunun gibi yerlerde kullanmak, bir çok yanlışı ve dikkatsizliği de beraberinde getiriyor.

Genelleştirilmiş sınıf ve metodlar oluşturmak için kullanılan bu object yaklaşımının diğer bir dezavantajı ise, object sınıfının diğer bir nesneye çevirim yapılması(unbox işlemi ya da unboxing de denilir) yada başka bir sınıfın object sınıfına çevirim yapılması(box ya da boxing yapılması) esnasında gereken ek hafıza ve işlemci zamanıdır. Yani her defasında Enqueue yapıldığında bir nesne object tipine dönüştürülecek ve her Dequeue yapıldığında ise object nesnesinin tipi başka bir nesneye dönüştürülecektir. Bu da ek işlem gücü ve hafızayı gerektirir.

Sınıfları ve metodları genelleştirmede object yaklaşımının belki başka dezavantajlarda sayılabilir ama, bizim işimiz burada generics ve parametrize tipler. Object yaklaşımı kafamızın bir köşesinde yer etmişken, dilerseniz generics yaklaşımına bir göz atalım.

Generics ve Parametrize Tipler(Parameterized Types)

Aslında Delphi programcıları bunu çok uzun süreden beri bekliyorlardı. Belki de .NET 2.0′a böyle bir özellik gelmeseydi, uzun bir süre daha Delphi böyle bir özellikten yoksun kalacaktı. Borland ya da CodeGear’ın .NET ortamına yaptığı yatırımları eleştirenler olmuştur. Fakat bu yatırımlar sayesinde hem Win32 hem de .NET üzerinde Delphi bir çok yeniliğe gitmiştir. Buradaki makaleden bu yeni özellikleri kısaca tanıyabilirsiniz.

Gramere ve kullanışına geçmeden önce bir kaç terim üzerinde duralım. Terimler ilk başta kafa karıştırıcı gibi gelse de bu terimleri kullandıkça alışacağınıza inanıyorum. Bu terimleri önce veriyorum çünkü makale içerisinde bu terimler kullanılacak.

Terminoloji

Parametrize Tip (Parameterized Type:): Tip Paremetresine ihtiyaç duyan bir çeşit tiptir. Generic tip, parametrize tipin daha çok bilinen ismidir. Ama literatürde ikisi de kullanılmaktadır. Aşağıdaki kodda “Liste<eleman>” sınıfı, bir parametrize tiptir.

type
  Liste&lt;eleman&gt; = class
    ...
  end;

Tip Parametresi (Type Parameter): Bir parametrize tip veya metod başlık tanımlamasında, parametrize tipin veya metodun kodlarında kullanılacak olan bir tipi ifade eden bir parametredir. Daha sonra bu parametreye bir tip argümanı bağlanacaktır. Aşağıdaki kodda “eleman” bir tip parametresidir ve sınıfın kodlarında kullanılacak olan bir tipi ifade eder.

type
  Liste&lt;eleman&gt; = class
    ...
  end;

Oluşturulmuş Tip (Instantiated Type, Constructed type): Parametre kümesi tanımlanmış olan bir parametrize tiptir. Liste<Integer> gibi…

Tip Argümanı (Type Argument):Oluşturulmuş” bir tip yapmak için gereken bir çeşit tiptir. Mesela Liste<Integer> bir oluşturulmuş parametrize tip ise, Integer bunun tip argümanıdır.

Kapalı Oluşturulmuş Tip (Closed Constructed Type): Bir parametrize tipin tüm parametreleri, gerçek tip olan argümanlar ile çözümlenmiş ise bu tipe, Kapalı Oluşturulmuş Tip denir. Liste<Integer> kapalıdır, çünkü Integer tip argümanı gerçek bir tiptir.

Açık Oluşturulmuş Tip (Open Constructed Type): Eğer bir parametrize tipin en az bir parametresi bir tip parametresi ise, bu tip bir açık oluşturulmuş parametrize tiptir. Ayrıca eğer Liste<T> parametrize tipinde T parametresi bir sınıfı içeren parametre ise Liste<T> açık oluşturulmuş bir tiptir. İleride örnekler yaptığımızda bu daha da pekişecektir.

Oluşturma İşlemi (Instantiation): Derleyici, parametrize tiplerde tanımlı metodlar için gerçek işlem kodunu oluşturur ve kapalı tipler için gerçek virtual metod tablosunu oluşturur. Bu işlem ön derleme esnasında gerçekleşir. Bu işlem Win32′de *.dcu ve *.obj dosyalarını çıkarırken gereklidir. Fakat .Net ortamında gerekli değildir. Çünkü .Net oluşturulmuş tipler için gerekli tanımlamalara sahiptir. Bu yüzdendir ki, C++’da template oluşturduğunuzda, derleyici ilk başta template’li tipleri ayrı ayrı oluşturmak için ön derleme işlemine girişir. Anlaşılan o ki, Delphi’nin bir sonraki versiyonunda da Win32′de generics desteği verebilmek için buna benzer bir yönteme gidilecek. Her neyse…

Bunlar dışında bir kaç terim daha var. Fakat daha fazla kafa karıştırmamak için, o terimleri ayrı başlıklar halinde ileride görelim. Dilerseniz önceden object yaklaşımı ile yaptığımız işlemleri şimdi de generics ile yapmaya çalışalım.

Kullanımı

Kuyruk örneğimizi hatırlayacak olursanız, ekstradan tip çevirim işlemleri yapmıştık. Ayrıca tip güvenliğinin gerekli olduğundan ve bir dikkatsizlikte belli sorunların oluşabileceğinden bahsettik. Ayrıca fazladan gerçekleştirilen object’den bir başka tipe ve daha sonra da o tipden tekrar object’e çevirim yapılmasından dolayı(box / unbox), ekstra hafıza ve işlem gücü gerektiğinden bahsetmiştik.

Bütün bunlara gerek duymadan, genelleştirilmiş sınıflar oluşturabilmek için parametrize tipleri ve generic yapısını kullanabiliriz. Generic sınıflar, tip parametresine sahip olduğundan dolayı, sadece object gibi bir tipe bağlı kalmaz. Böylece yukarıda bahsettiğimiz sıkıntıları da bertaraf etmiş oluruz.

.Net 2.0 ile birlikte System.Collections.Generic alan adının altında bir çok yararlı generic sınıf tanımlanmıştır. Aşağıdaki kod parçasında parametrize Queue sınıf ile oluşturulmuş bir Integer kuyruğu ve bir string kuyruğu görmekteyiz.

uses
  System.Collections.Generic;

  ...
  ...

var
  SayiKuyruk: Queue&lt;Integer&gt;;
  YaziKuyruk: Queue&lt;string&gt;;
  Sayi: Integer;
  Yazi: string;
begin
  SayiKuyruk := Queue&lt;Integer&gt;.Create;
  YaziKuyruk := Queue&lt;string&gt;.Create;
  Sayi := 123;
  Yazi := 'Deneme';
  SayiKuyruk.Enqueue(Sayi);
  YaziKuyruk.Enqueue(Yazi);
  ...

Gördüğünüz gibi, ne bir çevirim işlemi var ne de tipler uyuşuyor mu diye bir endişe… “SayiKuyruk” ve “YaziKuyruk” nesnelerinin ikisi de Queue<T> gibi bir parametrize tip’den oluşturulmuş tipten oluşturulmuştur :) . Terimlerimizde hatırlarsanız “Kapalı oluşturulmuş tip“ten bahsetmiştik. Derleyici bu örneğimizde Queue<T> tipinden iki adet yeni tip oluşturmuştur. Bunlardan birisi Queue<Integer> ve diğeri ise Queue<string> tipidir. Ve ikisi de kapalı oluşturulmuş bir tiptir. Ayrıca iki tip tamamen bir birinden bağımsız iki farklı tiptir!

Bu örnekten ne öğrendik? Özet olarak, bir generic sınıfa atadığımız tip argümanlarına göre(Integer, string, vs..) yeni bağımsız bir tip oluşturuyoruz. Ve bu oluşturulan tip, parametre olarak verdiğimiz argümanlara göre işlem yapıyor.

Önceki Queue sınıfı, sadece object tipinde nesneler ile çalşıyordu. Bu sınıftan sadece bir adet vardı ve oluşturduğumuz nesneler hep aynı sınıftan oluşturuluyordu. Ama generic Queue sınıfında ise iş biraz değişiyor. Artık tanımladığımız farklı tip için ayrı bir sınıf oluşturuluyor. Bu yüzden örneğimizdeki SayiKuyruk ve YaziKuyruk nesnelerinin sınıfları aynı değildir! Derleyici her iki tip argümanı için de ayrı ayrı sınıflar oluşturmuştur. Tip argümanları gerçek tipler olduğu için de oluşturulan bu iki sınıf da kapalı oluşturulmuş tip olmuştur.

Tanımlanması

Generic tiplerin tanımlanması kullanımı kadar kolaydır. Delphi’de sınıflar, interface’ler ve record’lar tip parametreleri ile birlikte tanımlanabilirler. Ayrıca metodlar ve prosedürel tipler de parametrize yapılabilir. Aşağıdaki sınıf tanımlamasına göz önüne alalım:

type
  TBirSinif = class
  public
    procedure BirSeylerYap(Parametre: string); overload;
    procedure BirSeylerYap(Parametre: Integer); overload;
    procedure BirSeylerYap(Parametre: Boolean); overload;
  end;

Sadece bu sınıf ve metodları düşündüğümüzde aynı iş için üç farklı metod tanımlaması yapıyoruz. Ne için? Sadece bir tipin değişmesi yüzünden. Bir de ikinci bir parametre aldığını düşünün. Veya üç, dört… Ne yapacağız? 4′ün 3′lü kombinasyonunu alıp o kadar farklı metod tanımlaması mı yapacağız?

Bunun yerine bu işi derleyiciye bırakıp, biz sadece tipler ile uğraşıyoruz. Yukarıdaki sınıfımızı parametrize olarak yeniden tanımlayalım:

type
  TBirSinif&lt;T, K, L&gt; = class
  private
    FBirOzellik: K;
  protected
    procedure SetBirOzellik(const Value: K);
  public
    procedure BirSeylerYap(Parametre: T);
    property BirOzellik: K read FBirOzellik write SetBirOzellik;
  end;

Gördüğünüz gibi, Integer string gibi değişkenler yerine sınıfa eklediğim tip parametrelerini kullanıyorum. Bu parametrize tipimizde 3 adet tip parametresi mevuct. Gerçi bu örnekte sadece ilk ikisini kullanıyoruz.

Bir genel kültür bilgisi mahiyetinde, ek olarak, bu sınıf derlendikten sonra TBirGenericSinif`3 gibi bir şekle girecektir. Eğer tip parametre sayısı 4 olsaydı, son kısım “`4” olacaktı. Bu yüzden derlenmiş assembly’nizi ya da programınızı reflect ettiğinizde bu tarz şeyler görürseniz şaşırmayın. Her neyse…

Önceden de bahsettiğimiz gibi yukarıdaki TBirSinif sınıfı ile TBirSinif<T, K, L> generic sınıfı birbirinden çok farklı şeylerdir. İsimleri aynı olabilir ama kesinlikle aralarında bir bağlantı yoktur. Aynı şekilde TBirSinif<Integer, string, Boolean> gibi bir kapalı oluşturulmuş sınıf, TBirSinif<string, string, Integer> gibi başka bir kapalı sınıftan çok çok farklıdır ve bağlantıları yoktur. Çünkü derleyici, tip argümanlarına göre tipleri tekrar tekrar tanımlamaktadır.

Generic Metodlar

Sınıflar gibi metodlar ve prosedürel tipler de parametrize olarak tanımlanabilir. Mesela:

type
  TBirProsedur2&lt;T&gt; = procedure(Param1, Param2: T) of object;
  TNormalBirSinif = class
    procedure BirProsedur&lt;X&gt;(Param1, Param2: X);
  end;

procedure TNormalBirSinif.BirProsedur&lt;X&gt;(Param1, Param2: X);
begin

end;

Görüldüğü gibi parametrize olmayan bir tip içinde(TNormalSinif) parametrize bir metod tanımladık. Bunu yaparken aynen sınıflarda olduğu gibi metod isminden sonra tip parametrelerini giriyoruz. Aynı şeyi parametrize olan bir tip içinde de yapabilirdik. Ama burada karar size ait. Eğer generic bir sınıf içinde farklı parametrelerle tanımlamamız gereken metodlar olursa, bu şekilde parametrize edebiliriz. Yani:

type
  TBirSinif&lt;T&gt; = class
    ...
    procedure BirProc&lt;X&gt;(Param1: X; Param2: T);
  end;

Görüldüğü gibi sınıf tanımlamasında X parametresi tanımlı olmadığı halde, metod tanımlamamızda kullanabiliyoruz. Peki bu gibi bir şeyi nasıl kullanacağız?

Farkında iseniz bir kaç paragraf yukarıda verdiğimiz bir örneğin tam bir çözümlemesini yapmadık. Yani:

type
  TBirSinif = class
  public
    procedure BirSeylerYap(Parametre: string); overload;
    procedure BirSeylerYap(Parametre: Integer); overload;
    procedure BirSeylerYap(Parametre: Boolean); overload;
  end;

Bu sınıfta “BirSeylerYap” metodu overload edilmiştir. Bu sınıfı generic yaparken şu şekilde bir tanımlama yapmıştık:

type
  TBirSinif&lt;T&gt; = class
  public
    procedure BirSeylerYap(Parametre: T);
  end;

Burada her bir kapalı tip için sadece bir adet metodumuz olabiliyor. Yani mesela TBirSinif<Integer> gibi kapalı bir sınıf içinde sadece bir adet “BirSeylerYap” metodu vardır ve sadece Integer tipinde bir parametre almaktadır. Halbuki bizim esas yapmak istediğimiz her bir kapalı sınıf içinde overload yaptığımız tiplerin sınırlandırmasını kaldırmak. Sadece bir çeşit metod yerine birden fazla çeşitte parametre alan metodlarımız olması gerekiyor. Bu yüzden metodumuzu genelleştirmek için onu da parametrize ediyoruz:

type
  TBirSinif&lt;T&gt; = class
  public
    procedure BirSeylerYap&lt;X&gt;(Parametre: X);
  end;

gibi… Bu şekilde bir metodun, istediğimiz kadar çeşidini overload yapmadan oluşturabiliriz. Mesela:

var
  BirNesne: TBirSinif&lt;Integer&gt;;
begin
  BirNesne := TBirSinif&lt;Integer&gt;.Create;
  BirNesne.BirSeylerYap&lt;string&gt;('Deneme bir string');
  BirNesne.BirSeylerYap&lt;Integer&gt;(123);
  BirNesne.BirSeylerYap&lt;Boolean&gt;(True);
  BirNesne.BirSeylerYap&lt;TForm&gt;(Form1);
  BirNesne.BirSeylerYap&lt;TButton&gt;(Button1);

gibi…

Gördüğünüz gibi overload ile kısıtlı sayıda yaptığımız metod tanımlamalarını bu şekilde artırabiliriz. Bu da aynen sınıflarda olduğu gibi, metodlarımızı da genelleştirmemize imkan sağlıyor.

Gelecek Bölümde

Gelecek bölümde Sınırlandırıcıları ya da başka yerlede göreceğiniz tabir ile Kısıtlamalar ya da Constraints tabirlerini göreceğiz. Diğer bölüme geçmek için buraya tıklayabilirsiniz.

Her zamanki gibi yorum ve eleştirilerinizi bekliyorum.

Fatih Tolga Ata

Kaynaklar:

  • CodeGear’ın Delphi Parametrize Tipler ile İlgili Taslak Metinleri
  • .NET 2.0 SDK Yardım Dosyası
  • Microsoft Visual C# 2005 Step by Step, John SHARP, 2005 Edition
Bu yazının kategorisi:Delphi ve etiketleri: , , , , , Tekrar ulaşmak içinkalıcı bağlantı. Yorum yazınveya bir geri izleme bırakın:Geri İzleme URL'si.

1 Yorum

(Required)
(Required, will not be published)
Ana Sayfa RSS İrtibat