C Sharp Programlama Dili/Şablon tipler

Vikikitap, özgür kütüphane
Ders 32. Şablon tipler


Şimdiye kadar bir sınıftaki tür bilgileri sınıf bildirimi esnasında belliydi. Ancak artık şablon tipler sayesinde sınıftaki istediğimiz türlerin nesne yaratımı sırasında belli olmasını sağlayacağız. Üstelik her nesne için söz konusu türler değişebilecek. Şablon tipler sayesinde türden bağımsız işlemler yapabileceğiz. Şimdi şablon tipleri bir örnek üzerinde görelim:

 using System;
 class Sinif<SablonTip>
 {
    public SablonTip Ozellik;
 }
 class AnaProgram
 {
    static void Main()
    {
       Sinif<int> s1=new Sinif<int>();
       s1.Ozellik=4;
       Sinif<string> s2=new Sinif<string>();
       s2.Ozellik="deneme"; 
       Console.WriteLine(s1.Ozellik+" "+s2.Ozellik);
    }
 }

Bu programda s1 nesnesinin Ozellik özelliğinin geri dönüş tipi int, ancak s2 nesnesinin Ozellik özelliğinin geri dönüş tipi stringdir. Benzer şeyi metotların geri dönüş tipleri, metotların parametre tipleri vb. için de yapabilirdik. Bu programdaki Sinif sınıfı türden bağımsız bir sınıftır. Yani bu sınıfla istediğimiz türdeki verilerle çalışabiliriz. Bu programcılar için büyük esneklik sağlar. Sınıflar, arayüzler, yapılar, temsilciler ve metotlar şablon tip olarak bildirilebilir.

Sınıf aşırı yükleme[değiştir]

Bir sınıf (veya yapı, arayüz vs.) birden fazla şablon tip alabilir. Örnek:

 class Sinif<SablonTip1, SablonTip2>

Bu durumda bu sınıf türünden nesne şunun gibi yaratılır:

 Sinif<int,string> s=new Sinif<int,string>();

Aynı isim alanında isimleri aynı olsa bile farklı sayıda şablon tipi olan sınıflar bildirilebilir. Buna sınıf aşırı yükleme denir.

Şablon tipler arasında türeme[değiştir]

Şablon tipler arasında türeme ile ilgili çok fazla kombinasyon düşünebiliriz. Ancak biz burada karşımıza en çok çıkacak iki kombinasyondan bahsedeceğiz:

 class AnaSinif<T>
 {
    //...
 }
 class YavruSinif<T,Y>:AnaSinif<T>
 {
    //Gördüğünüz gibi yavru sınıf en az ana sınıfın şablon tipini içerdi.
 }
 class YavruSinif2<T>:AnaSinif<int>
 {
    /*Gördüğünüz gibi yavru sınıf ana sınıfın şablon tipine belirli bir tür koydu. Böylelikle yavru sınıftaki ana sınıftan 
    gelen T harfleri int olarak değiştirilecektir.*/ 
 }

Şablon tipler ve arayüzler[değiştir]

Arayüzlerin de şablon tipli versiyonları yazılabilir. Örneğin System.Collections.Generic isim alanında birçok şablon tipli arayüz bulunmaktadır. Bunlardan birisi de IEnumerable arayüzünün şablon tipli versiyonudur. IEnumerable arayüzünün şablon tipli versiyonu aşağıdaki gibidir.

 interface IEnumerable<T>:IEnumerable
 {
    IEnumerator<T> GetEnumerator();
 }

Buradan anlıyoruz ki bu arayüzü uygulayan sınıf geri dönüş tipi IEnumerator<T> olan GetEnumerator() metodunu ve IEnumerable arayüzünün içerdiği tüm üyeleri içermeli. Bu arayüzü bir sınıfta şöyle uygulayabiliriz:

 class Sinif:IEnumerable<int>
 {
    IEnumerator IEnumerable.GetEnumerator()
    {
       //...
    }
    IEnumerator<int> IEnumerable<int>.GetEnumerator()
    {
       //...
    }
 }

Burada hem IEnumerable hem de IEnumerable<T> arayüzlerinin metotları uygulandı. Çünkü IEnumerable<T> arayüzü, IEnumerable arayüzünden türemişti.

Şablon tiplerin metotlara etkisi[değiştir]

Bildiğiniz gibi sınıf düzeyinde bir şablon tip belirlediğimizde bu şablon tip metotların geri dönüş tipine, parametre tiplerine koyulabilir. Ancak bunun sonucunda bazen bir çakışma oluşabilir. Örneğin:

 class Sinif<T>
 {
    public int Metot(T a)
    {
       return 0;
    }
    public int Metot(int a)
    {
       return 1;
    }
 }

Bu sınıf türünden nesneyi şöyle tanımlayıp kullanırsak;

 Sinif<int> a=new Sinif<int>();
 Console.WriteLine(a.Metot(10));

Sizce ne olur? Bu gibi bir durumda Metot(int a) metodu çalışır. Yani normal tipli versiyon şablon tipli versiyonu gizler.

default operatörü[değiştir]

Bildiğimiz gibi şablon tipler herhangi bir tipi temsil ederler. Bu yüzden C#, yalnızca bazı türlere özgü olan operatörler (+, -, *, ...) kullanılmasına izin vermediği gibi şablon tip türünden bir nesne yaratılıp bu nesneye bir değer verilmesine engel olur. Örnekler:

 class Sinif<T>
 {
    public int Metot(T a)
    {
       return a+2;
    }
 }

Bu sınıf derlenmez. Çünkü T tipinin hangi tip olduğunu bilmiyoruz. + operatörü bu tip için aşırı yüklenmiş olmayabilir. Bu yüzden C# bu gibi tehlikeli durumları önlemek için bu tür bir kullanımı engeller. Başka bir kullanım:

 class Sinif<T>
 {
    public int Metot(T a)
    {
       T nesne=0;
    }
 }

Yine burada da bir hata söz konusudur. Çünkü her tipten nesneye 0 atanmayabilir. Benzer şekilde yapı nesnelerine de null değer atanamaz. Gelelim default operatörüne. default operatörü bir şablon tipin varsayılan değerini elde etmek için kullanılır. Örnek:

 class Sinif<T>
 {
    public void Metot()
    {
       T nesne=default(T);
    }
 }

Varsayılan değer bazı türler için 0 (int, short, float vs.) bazı türler için null (tüm sınıflar) bool türü için de false'tur.

Kısıtlar[değiştir]

Daha önce şablon tip türünden nesnelerle herhangi bir operatör kullanılamayacağını, bu şablon türden nesnelere herhangi bir sabit atanamayacağını vs. söylemiştik. Bunlara paralel olarak şablon tür nesneleriyle herhangi bir üye de (metot, özellik vs.) çalıştıramayız. Çünkü şablon tipin ne olduğunu bilmediğimiz için olmayan bir metodu çalıştırmaya çalışıyor olabiliriz. Tüm bunların sebebi aslında şablon sınıf türünden nesne yaratırken şablonu herhangi bir tür yapabilmemizdi. Eğer şablona koyulabilecek türleri kısıtlarsak bunları yapabiliriz. C#'ta şu kısıtlar bulunmaktadır:

struct şablon tip yalnızca yapılar olabilir.

class şablon tip yalnızca sınıflar olabilir.

new() şablon tipin parametre almayan bir yapıcı metotla nesnesinin oluşturulabilmesi gerekir.

türetme şablon tip mutlaka belirtilen bir türden türemiş olmalıdır.

interface şablon tip mutlaka belirtilen bir arayüzü uygulamalıdır.

Kısıtlar where anahtar sözcüğüyle yapılmaktadır. Şimdi isterseniz her bir kısıtı sırayla inceleyelim.

Türetme kısıtı[değiştir]

 class A{}
 class B:A{}
 class C:A{}
 class D<T> where T:A{}
 class AnaProgram
 {
    static void Main()
    {
       D<B> nesne1=new D<B>();
       D<C> nesne2=new D<C>();
       //Aşağıdaki olmaz.
       //D<int> nesne3=new D<int>();
    }
 }

Türeme kısıtı sayesinde şablon tip nesnesiyle ana sınıftaki bütün üye elemanları çağırabiliriz. Ana sınıfın kendisi de bu türetme kısıtına uyar. Yani örnekteki D sınıfının T tip parametresine A da yazılabilir.

Türetme kısıtıyla tip parametresinin belirli bir türden türemesini isteyebilmekle birlikte bir tip parametresinin başka bir tip parametresinden türemesini de isteyebiliriz. Örnek:

class D<T,U> where T:U{}

Bu durumda sınıf içinde U tipten değişkenlere T tipten değişkenleri atayabiliriz.

new() kısıtı[değiştir]

new() kısıtı ilgili tipin parametre almayan bir yapıcı metotla nesnesinin oluşturulabildiğini belirtir. Örnek:

 class Sinif<T> where T:new()
 {}

Artık bu sınıfın içinde T türünden nesneleri new operatörüyle oluşturabiliriz. Tabii ki C# şablon tipe parametre almayan bir yapıcı metotla nesnesi oluşturulamayan bir tür koyulmasını engelleyecektir.

NOT: new kısıtı ilgili tipin parametre almayan bir yapıcı metotla nesnesinin oluşturulabilmesini zorunlu kılar. Yapıcı metotla parametre vererek nesnesi oluşturulabilen, ancak parametre vermeden nesnesi oluşturulamayan tipler bu kısıtı sağlamaz.

Arayüz kısıtı[değiştir]

Başka bir önemli kısıt ise arayüz kısıtıdır. Arayüz kısıtı sayesinde şablon tipe koyulan tipin mutlaka belirli bir arayüzü kullanmış olmasını sağlarız. Örnek program:

 using System;
 class Karsilastirici<T> where T:IComparable<T>
 {
    public static int Karsilastir(T a,T b)
    {
       return a.CompareTo(b);
    }
 }
 class Program
 {
    static void Main()
    {
       int s1=Karsilastirici<int>.Karsilastir(4,5);
       int s2=Karsilastirici<float>.Karsilastir(2.3f,2.3f);
       int s3=Karsilastirici<string>.Karsilastir("Ali","Veli");
       int s4=Karsilastirici<DateTime>.Karsilastir(DateTime.Now,DateTime.Now.AddDays(1));
       Console.WriteLine("{0} {1} {2} {3}",s1,s2,s3,s4);
    }
 }

Bu program ekrana -1 0 -1 -1 çıktısını verecektir. .Net Framework kütüphanesindeki IComparable<T> arayüzünde CompareTo(<T> b) metodu bulunmaktadır. Bu programdaki Karsilastici<T> sınıfındaki T tip parametresi sadece kendisiyle karşılaştırılabilen sınıflar olabilir. Bir sınıf IComparable<T> arayüzünü iki şekilde kullanabilir. Birinci kullanım:

class Sinif:IComparable<Sinif>
{
    public int CompareTo(Sinif b)
    {
        //...
    }
}

Bu sınıf kendisiyle karşılaştırılabilen bir sınıftır, az önceki programdaki tip parametresi olma şartını sağlamaktadır. Başka bir örnek:

class Sinif<T>:IComparable<T>
{
    public int CompareTo(T b)
    {
        //...
    }
}

Bu sınıf ise IComparable<T> arayüzünün kurulmamış halini uygulamaktadır. Bu durumda bu arayüzü uygulayan sınıfın en az bir tane T isimli tip parametresi olması gerekmektedir. Bu sınıf yukarıdaki kendiyle karşılaştırılabilme şartını sağlamamaktadır. Bu sınıf nesnesi kendisiyle değil, parametresindeki tiple karşılaştırılabilmektedir. Peki sadece, kendisiyle değil parametresindeki tiple karşılaştırılabilen sınıfları (yani örneğin yukarıdaki sınıfı) tip parametresi olarak kabul eden bir sınıf nasıl yazılır? Şöyle mi acaba:

class Deneme<T<U>> where T<U>:IComparable<U> {}

Böyle bir şey olamaz. Çünkü tip parametreleri iç içe geçemez.

NOT: Arayüzün kendisi de arayüz kısıtına uyar.

class ve struct kısıtları[değiştir]

Diğer önemli iki kısıt ise şablon tipe yalnızca bir referans tipi koyulabilmesini sağlayan class ve aynı şeyi değer tipleri için yapan struct kısıtlarıdır. Yapılar ve enumlar değer tipi; sınıflar, arayüzler ve temsilciler referans tipidir. Örnekler:

 class Sinif1<T> where T:class{}
 class Sinif2<T> where T:struct{}

Kısıtlarla ilgili çeşitli notlar[değiştir]

Tabii ki bir şablon tipe birden fazla kısıt eklenebilir. Bu durumda kısıtlar virgülle ayrılır. Örnek:

 class Sinif<T> where T:class,IComparable,new(){}

Kısıtlarda en önce sınıftan türeme kısıtı (veya class-struct kısıtı), sonra bir veya daha fazla arayüz kısıtı, sonra new() kısıtı olmalıdır.

new() kısıtı ile struct kısıtı bir arada olamaz.

Sınıftan türeme kısıtıyla "struct" veya "class" kısıtları bir arada olamaz.

class kısıtı ile struct kısıtı bir arada olamaz.

Sınıflar yanında diğer bütün şablon tip ve üye elemanlar da (temsilciler, arayüzler, yapılar, metotlar) kısıt alabilir.

Bir sınıfa eklenen birden fazla şablon tip varsa her biri için ayrı ayrı kısıtlar koyulabilir. Örnek:

 class Sinif<T,S> where T:IComparable,IEnumerable where S:AnaSinif

Kısıta en az "class" koymak şartıyla == ve != ve operatörlerini tip parametresindeki nesneler için kullanabiliriz. Ancak bu durumda sınıf == ve != operatörlerini aşırı yüklemiş olsa bile sadece referansın eşitliği kontrol edilir. Eğer aşırı yüklenmiş == ve != operatörlerinin kullanılmasını istiyorsak tip parametresine "class" kısıtını koymak yerine o sınıftan türeme kısıtını koyabiliriz. Örnek:

using System;
class Sinif
{
    public int Sayi;
    public Sinif(int i) { Sayi = i; }
    public static bool operator ==(Sinif a, Sinif b)
    {
        if (a.Sayi == b.Sayi)
            return true;
        else
            return false;
    }
    public static bool operator !=(Sinif a, Sinif b)
    {
        return !(a == b);
    }
}
class Program
{
    static void Main()
    {
        Sinif s1 = new Sinif(1);
        Sinif s2 = new Sinif(1);
        Console.WriteLine(s1 == s2);
        Metot(s1, s2);   
    }
    static void Metot<T>(T t1,T t2) where T:class 
    {
        Console.WriteLine(t1 == t2);
    }
}

Bu program alt alta True ve False çıktısını verecektir. Metot<T>() metodunun içinde t1 ve t2 nesnelerinin aşırı yüklenmiş == ve != operatörleri değil, C# derleyicisinin her referans tipindeki nesneye bahşettiği ve referansları kıyaslamaya yarayan == ve != operatörleri kullanılmıştır.

Kısıt almayan şablon tip nesneleriyle == ve != operatörleri kullanılamaz. Şablon tip nesneleriyle == ve != operatörlerinin kullanılabilmesi için şablon tipin class veya bir referans tipinden türeme kısıtını almış olması gerekir.

Kısıt almayan şablon tip nesneleri object tipine bilinçsiz (veya bilinçli) olarak dönüştürülebilir.

Kısıt almayan şablon tip nesneleri herhangi bir arayüz tipine bilinçli olarak dönüştürülebilir, ancak kurulmuş olan tip ilgili arayüzü uygulamamışsa çalışma zamanı hatası meydana gelir.

Kısıt almayan şablon tip nesneleri null değer ile karşılaştırılabilir. Bu durumda değer tipi nesneleri her zaman false döndürecektir, referans tipi nesneleri null olup olmama durumuna göre true ve false döndürebilir.

Kısıt almayan şablon tip nesneleri as ve is operatörleriyle kullanılabilir. Örnek:

public static void Metot<T>(T t)
{
    string s=t as string;
    bool b=t is string;
}

Bir şablon tipten yapılandırmadan türeyen yavru tip en az ana sınıfın kısıtlarını uygulamalıdır. Örnek:

class Ana<T> where T : System.IComparable<T>, new() { }
class Yavru<T> : Ana<T> where T : System.IComparable<T>, System.ICloneable, new() { }

NOT: Main() metodu mutlaka şablon tipli olmayan bir sınıfın içinde olmalıdır, kendisi de şablon tipli olamaz. Ayrıca Main() metodu ya bir sınıfın içinde ya da bir yapının içinde olmalıdır. Diğer bir deyişle çalışabilir her program en az bir sınıf ya da yapı içermelidir.

Şablon tipli metotlar[değiştir]

Şimdiye kadar şablon tipler sınıf seviyesinde tanımlanmıştı. Halbuki şablon tipler metot ve temsilci düzeyinde de tanımlanabilir. Örnek:

 using System;
 class karsilastirma
 {
    static void Main()
    {
       Console.WriteLine(EnBuyuk<int>(4,5));
       Console.WriteLine(EnBuyuk<string>("Ali","Veli"));
    }
    static T EnBuyuk<T>(T p1,T p2) where T:IComparable
    {
       T geridonus=p2;
       if(p2.CompareTo(p1)<0)
          geridonus=p1;
       return geridonus;
   }
 }

Bu program alt alta 5 ve Veli yazacaktır.

Şablon tipi çıkarsama[değiştir]

Az önceki örneğin sadece Main() metodunu alalım:

    static void Main()
    {
       Console.WriteLine(EnBuyuk<int>(4,5));
       Console.WriteLine(EnBuyuk<string>("Ali","Veli"));
    }

Burada parametrelerin türleri belli olduğu hâlde ayrıca int ve string türlerini de belirttik. İstersek bunu şöyle de yazabilirdik:

    static void Main()
    {
       Console.WriteLine(EnBuyuk(4,5));
       Console.WriteLine(EnBuyuk("Ali","Veli"));
    }

Bu programda metot şöyle düşünecektir: "Benim parametrelerim T türünden, o hâlde yalnızca parametreme bakarak T'nin hangi tür olduğunu bulabilirim." Gördüğünüz gibi metotların aşırı yükleyerek saatlerce uğraşarak yazabileceğimiz programları şablon tipli metotlar kullanarak birkaç dakikada yazabiliyoruz.

NOT: Bir metot kısıtlayıcı tip parametresini bağlı olduğu sınıfın tip parametresinden alabilir. Örnek:

class Sinif<T>
{
    static void Metot<U>(U u) where U:T
    {
        //...
    }
}

Burada Metot<U>() metodunun aldığı parametrenin tipi bağlı olduğu sınıfın aldığı parametrenin tipinden türemelidir.

Şablon tipli temsilciler[değiştir]

Temsilciler de şablon tip alabilirler. Bu sayede temsilcinin temsil edebileceği metot miktarını artırabiliriz. Örnek:

 using System;
 delegate T Temsilci<T>(T s1,T s2);
 class deneme
 {
    static int Metot1(int a,int b){return 0;}
    static string Metot2(string a,string b){return null;}
    static void Main()
    {
       Temsilci<int> nesne1=new Temsilci<int>(Metot1);
       Temsilci<string> nesne2=new Temsilci<string>(Metot2);
       Console.WriteLine(nesne1(1,2));
       Console.WriteLine(nesne2("w","q"));
    }
 }

Temsilci şablon tipleri de kısıt alabilirler. Örnek:

 delegate T Temsilci<T>(T s1,T s2) where T:struct

Burada T yalnızca bir yapı olabilir. Yani bu temsilcinin temsil edeceği metodun parametreleri ve geri dönüş tipi yalnızca bir yapı olabilir.

İç içe tiplerde şablonlar[değiştir]

Tiplerin iç içe geçmesi durumunda içteki tip dıştaki tipin tip parametresini gönlünce kullanabilir. İçteki tipin içinde kullanılan tip parametresinin gerçek tipinin ne olduğu dış tipe ulaşılırken belirlenir. Örnek:

using System;
class Dis<T>
{
    public class Ic
    {
        public static T Metot(T t)
        {
            return default(T);
        }
    }
}
class Ana
{
    static void Main()
    {
        Console.WriteLine(Dis<int>.Ic.Metot(5));
        Console.WriteLine(Dis<string>.Ic.Metot("deneme"));
    }
}

İç içe tipler söz konusu olduğunda içteki tip dıştaki şablon tipin tip parametresinden türeyemez.

null değer alabilen yapı nesneleri[değiştir]

Bildiğiniz gibi yapı nesneleri null değer alamaz. Örneğin şu kod hatalıdır:

 int a=null;

Ancak System isim alanındaki Nullable<T> yapısı sayesinde yapı nesnelerinin de null değer alabilmesini sağlayabiliriz. System.Nullable<T> yapısı şu gibidir:

 public struct Nullable<T> where T:struct
 {
    private T value;
    private bool hasValue;
    public T Value{get{...}}
    public bool HasValue{get{...}}
    public T GetValueOrDefault(){...}
 }

Bu yapıya göre null değer alabilen yapı nesneleri şöyle oluşturulur:

 Nullable<int> a=new Nullable<int>();
 a=5;
 a=null;
 Nullable<double> b=new Nullable<double>(2.3);
 Console.WriteLine("{0}{1}",a,b);

Gördüğünüz gibi değerler yapıcı metot yoluyla ya da normal yolla verilebiliyor. Nullable<T> yapısının GetValueOrDefault() metodu ilgili nesnenin değeri null ise ilgili nesnenin tipinin varsayılan değerini döndürür (int için 0), ilgili nesnenin değeri null değilse ilgili nesnenin değerini olduğu gibi döndürür. Yine aynı yapıya ait Value özelliği ilgili nesnenin değerini döndürür. Ancak az önceki metottan farklı olarak nesnenin değeri null ise null döndürür. Eğer bu özelliğin döndürdüğü değer null ise çalışma zamanı hatası oluşacaktır. Çünkü bir yapı nesnesi null alamaz. Nullable tipler daha çok veri tabanı işlemlerinde kullanılır. Çünkü veri tabanındaki int, double gibi veri tipleri null değer alabilir. Nullable tipler veri tabanındaki verileri programımıza olduğu gibi aktarmak için kullanılabilir.

? işareti[değiştir]

C#'ı tasarlayan mühendisler bizim nullable tiplere çokça ihtiyaç duyabileceğimizi düşünmüş olacaklar ki ? işaretini geliştirmişler. ? takısı kısa yoldan nullable tipte nesne oluşturmak için kullanılır. Örneğin:

 Nullable<double> d=10;

ile

 double? d=5;

satırları birbirine denktir. Yani aşağıdaki gibi bir satır mümkündür:

 double? d=null;

Nullable nesneleri normal nesnelere tür dönüştürme operatörünü kullanarak dönüştürebiliriz. Ancak nullable nesnenin değeri null ise çalışma zamanı hatası alırız. Örnek:

 int? a=5;
 int b=(int)a;

Benzer şekilde tür dönüşüm kurallarına uymak şartıyla farklı dönüşüm kombinasyonları da mümkündür:

 int? a=5;
 double b=(double)a;

Normal ve nullable nesneler arasında ters dönüşüm de mümkündür. Normal nesneler nullable nesnelere bilinçsiz olarak dönüşebilir. Örnek:

 int a=5;
 int? b=a;

Nullable nesneler operatörler ile kullanılabilir. Örnek:

 int? a=5;
 int? b=10;
 int c=(int)(a+b);

Burada a+b ifadesinin ürettiği değer yine int? türünden olduğu için tür dönüşüm operatörü kullanıldı.

?? operatörü[değiştir]

?? operatörü Nullable<T> yapısındaki GetValueOrDefault() metoduna benzer şekilde çalışır. Örnek:

 int? a=null;
 int b=a??0;

Burada eğer a null ise ?? operatörü 0 değerini döndürür. ?? operatörünün döndürdüğü değer normal (nullable olmayan) tiptedir. Eğer ilgili nullable nesne null değilse olduğu değeri döndürür. Başka bir örnek:

 int? a=null;
 int b=a??50;

Burada ise eğer a null ise 50 döndürülür. Yani ?? operatöründe GetValueOrDefault() metodundan farklı olarak ilgili nesne null olduğunda döndürülecek değeri belirleyebiliyoruz.

Bu kitabın diğer sayfaları
  • Sınıflar
  • Operatör aşırı yükleme
  • İndeksleyiciler
  • Yapılar
  • Enum sabitleri
  • İsim alanları
  • System isim alanı
  • Temel I/O işlemleri
  • Temel string işlemleri
  • Kalıtım
  • Arayüzler
  • Partial (kısmi) tipler
  • İstisnai durum yakalama mekanizması
  • Temsilciler
  • Olaylar
  • Önişlemci komutları
  • Göstericiler
  • Assembly kavramı
  • Yansıma
  • Nitelikler
  • Örnekler
  • Şablon tipler
  • Koleksiyonlar
  • yield
  • Veri tabanı işlemleri
  • XML işlemleri
  • Form tabanlı uygulamalar
  • Visual Studio.NET
  • Çok kanallı uygulamalar
  • ASP.NET