Herkese merhaba. Bu yazıda pek çok gelişmiş programlama dilinde bulunan bir konuyu, generic tipler konusunu ele alacağız. C# içerisinde kullanabileceğimiz tiplerin üzerine kendi oluşturduğumuz tipleri ekleyebileceğimizi nesne yönelimli programlamada görmüştük. Kendi oluşturduğuğumuz farklı tipleri ortak bir metot için kullanmak istiyorsak, metot parametresine object tipi verebiliriz. Bu yöntem kısa vadede sorunlarımızı çözüyor gibi görünse de uzun vadede bazı sıkıntılara yol açıyor. Yazıda bulunan örneklere github hesabımdan ulaşabilirsiniz.

Generic Tipler

Generic tipleri incelemeden önce yukarıda da bahsettiğim parametreyi object olarak göndermeyi inceleyelim. Metodun parametresini object olarak işaretlediğimizde hem değer hem de referans tipli değişkenleri kapsıyor. Burada da işin içerisine boxing ve unboxing dediğimiz tip dönüşümü giriyor.

Boxing: Object veri tipinden bir değişken içerisine değer tipli bir veri atadığımızda arka planda boxing (kutulama) işlemi yapılarak heapte veri saklanır.

Unboxing: Heap’te tutulan object tipindeki değişkenin tuttuğu değer tipli veriyi, değer tipli bir değişken üzerine cast ettiğimizde yapılan arka plan işlemidir.

Bu iki yöntemde arkaplanda ekstra efor harcanmasına neden olur. Ayrıca yapılan bu işlem tip güvenliğini de zedelemiş olur. Bu işlemlerin sağladığı performans maliyetini azaltmak ve tip güvenliğini sağlamak için mümkün mertebe generic yapıları kullanmamız gerekir. Nongeneric yapıyı daha iyi anlamak için bir örnek ile devam edelim. Örneğin nongeneric bir liste yapısı oluşturalım.

internal class MyNongenericList : IEnumerable
{
    private object[] items;
    private static int capacity = 2;
    private int itemCount;

    public MyNongenericList()
    {
        items = new object[capacity];
        itemCount = 0;
    }

    public void Add(object obj)
    {
        if (itemCount == capacity)
        {
            capacity = capacity * 2;
            Resize();
        }
        items[itemCount] = obj;
        itemCount++;
    }

    private void Resize()
    {
        Array.Resize(ref items, capacity);
    }

    public IEnumerator GetEnumerator()
    {
        yield return items.GetEnumerator();
    }

    public object this[int index]
    {
        get
        {
            if (index < 0 || index >= itemCount)
            {
                throw new IndexOutOfRangeException();
            }

            return items[index];
        }
        set
        {
            if (index < 0 || index >= itemCount)
            {
                throw new IndexOutOfRangeException();
            }

            items[index] = value;
        }
    }
}

İçerisine her türlü değer alabilen bir liste oluşturduk. Listemizin ekleme metodunun paramertesi object olduğu için, içerisine integer değer eklediğimizde boxing işlemi gerçekleşti. Bunun yanı sıra yazdırma işlemi yaparken de unboxing gerçekleşti. Bu örnek küçük ölçekli olduğu için buradaki boxing ve unboxing maliyeti çok az gelebilir. Ancak bu listenin bir e-ticaret uygulamasında sepet için eklendiğini düşünelim. Eklenen her ürün için boxing ve unboxing yapmak oldukça performans kaybettirecektir.

Boxing ve Unboxing

Perfomans olarak karşılaştırma yapabilmek için bu listenin generic versiyonunu oluşturalım. Generic bir tip yaratmak için oluşturduğumuz sınıfın yanına ibaresini bırakmamız yeterli olacaktır. Buradaki T, oluşturulacak listenin tipini belirler. Örnek olarak integer bir liste için “<int>” şeklinde bir yazım kullanabiliriz. Bir sınıfa aynı anda birden fazla tip tanımlayabiliriz. (<T,U,W> gibi)

internal class MyGenericList<T> : IEnumerable<T>
{
    private T[] items;
    private static int capacity = 2;
    private int itemCount;

    public MyGenericList()
    {
        items = new T[capacity];
        itemCount = 0;
    }

    public void Add(T item)
    {
        if (itemCount == capacity)
        {
            capacity = capacity * 2;
            Resize();
        }
        items[itemCount] = item;
        itemCount++;
    }

    private void Resize()
    {
        Array.Resize(ref items, capacity);
    }

    public IEnumerator GetEnumerator()
    {
        foreach (T item in items)
        {
            yield return item;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return items.GetEnumerator();
    }

    public T this[int index]
    {
        get
        {
            if (index < 0 || index >= itemCount)
            {
                throw new IndexOutOfRangeException();
            }

            return items[index];
        }
        set
        {
            if (index < 0 || index >= itemCount)
            {
                throw new IndexOutOfRangeException();
            }

            items[index] = value;
        }
    }
}

Örnekte olduğu gibi, listenin içine string değer eklemek istediğimizde hata alırız. Performans açısından ikisini karşılaştırmak için ufak bir performans testi hazırladım. 1 milyon kez dönen bir döngüde kaç saniyede boxing ve unboxing işlemi gerçekleşecek görelim.

Nongeneric listede boxing ve unboxing işlemi 44 milisaniye sürerken, generic listede tam olarak böyle bir işlem yapılmadığından 26 milisaniyede döngü tamamlandı.

Generic Metotlar

Generic bir metot oluşturmak için illa generic bir sınıf oluşturmamız gerekmez. Bu metotları normal classların içerisine de ekleyebiliriz. Genellikle, içeride farklı değişken tipleri için yapılan işlem aynıysa fakat geri dönüş tipi o değişkene bağlıysa bu metotlar tercih edilir. Generic metot içerisinde birden fazla generic parametre varsa, bu iki tip arasında aritmetik ve karşılaştırma operatörleri kullanılamaz. Fakat karşılaştırma yapmak için string metodu olan CompareTo metodunu kullanabiliriz. Bunun yanı sıra bu metodu kullanarak farklı karşılaştırma işlemleri de yapabiliriz.

public static void FindMax<T>(T param1, T param2)
{
    //if (param1 > param2)
    //{

    //}

    if (param1.ToString().CompareTo(param2.ToString()) == 1)
    {
        Console.WriteLine(param1.ToString());
    }
    else
    {
        Console.WriteLine(param2.ToString());
    }
}

Farz edelim ki bir sitede kullanıcıya faturaları, developerlara hataları ve yeni üye bildirimini mail olarak gönderen bir sistem oluşturmamız gerekiyor. Helper içerisine SendMail isminde generic bir metot oluşturalım. Bunun yanı sıra generic tipli bir mail nesnesi oluşturmamız da gerekiyor ki herkese gönderilecek mail datası farklı olsun.

public static bool SendMail<T>(SystemMembers member, T data)
{
    bool result = false;
    switch (member)
    {
        case SystemMembers.Admin:
            result = new Mail() { From = "site@site.com", To = "admin@site.com", Data = "Yeni üye var" }.Send();
            break;
        case SystemMembers.Developer:
            result = new Mail() { From = "site@site.com", To = "developer@site.com", Data = new Exception() }.Send();
            break;
        case SystemMembers.Customer:
            result = new Mail { From = "site@site.com", To = "cust@gmail.com", Data = new Invoice() }.Send();
            break;
        default:
            break;
    }

    return result;
}
class Mail<T>
{
    public string From { get; set; }
    public string To { get; set; }

    public T Data { get; set; }

    public bool Send()
    {
        return true;
    }
}

class Invoice { }
internal enum SystemMembers
{
    Admin,
    Developer,
    Customer
}

Generic Interface ve Kalıtım

Yukarıda bahsettiğim bütün özelliklerin yanı sıra generic interface ve generic class oluşturabiliriz. Interfaceleri de classları oluşturduğumuz gibi oluşturabiliriz. Tıpkı classlarda olduğu gibi interface’lerde de implement edilirken tipini de tanımlamamız gerekiyor. Örnek olarak ekleme işlemi yapılan bir interface oluşturalım.

internal interface IAdd<T>
{
    T Add(T param1, T param2);
}
internal class IntegerAdd : IAdd
{
    public int Add(int param1, int param2)
    {
        return param2 + param1;
    }
}
internal class StringAdd : IAdd
{
    public string Add(string param1, string param2)
    {
        return param1 + " " + param2;
    }
}

Bunun yanı sıra kalıtım yaptığımızda “<>” içerisindeki değeri kalıtım verdiğimiz sınıfta belirleyebiliriz. Ayrıca farklı bir generic tipteki classa o classın tipini verebiliriz. Yani generic bir classtan generic bir class türetebiliriz. Bu şekilde bir tanımlama yaptığımızda türetilen sınıftaki tipi base sınıfa aktarmış oluruz.

internal class FirstClass<T>
{
    public void GetType(T param)
    {
        Console.WriteLine("Type : " + param.GetType().Name);
        Console.WriteLine("BaseType : "  + param.GetType().BaseType.Name);
        Console.WriteLine();
    }
}

internal class SecondClass<int> : FirstClass<int>
{

}

internal class ThirdClass<U> : FirstClass<U>
{

}

Generic Kalıtım

Generic Constraints

Eğer bir kütüphane oluşturuyorsanız ya da yazdığınız generic classın daha başka kullanıcıları olcaksa, o metoda ya da sınıfa ait generic tipe verilecek tipin kısıtlanmasını sağlayabilirsiniz. Bu sayede belirlediğiniz tip kısıtı dışında kullanılmasını engelleyebilirsiniz. Ancak verilen kısıta uymayan bir değer atanmaya çalışıldığında compiler hata verecektir. Generic tipe “where” keywordünü kullanarak bir veya birden fazla kısıt tanımlayabilirsiniz. Kısıt türlerini aşağıdaki tabloda bulabilirsiniz.

KısıtAçıklama
classTip argümanı sadece referans tipli bir değer almalıdır. (class, interface, delegate, or array type)
class?Tip argümanı sadece null yapılabilir veya null yapılamaz referans tipli bir değer almalıdır.
structTip argümanı sadece null yapılamaz değer tipli data türlerini değer almalıdır. (bool, char, int vb.)
new()Tip argümanı sadece referans tipli ve public parametresiz constructor’a sahip değer almalıdır. struct ve unmanaged kısıtlarıyla beraber kullanılamaz.
notnullTip argümanı sadece null yapılamaz referans ya da değer tipli data türlerini değer almalıdır. (C# 8.0 ve üzerinde geçerlidir.)
unmanagedTip argümanı sadece null yapılamaz unmanaged tipinde değer almalıdır.
(base class name)Tip argümanı sadece belirtilmiş base class’tan veya ondan türetilmiş classlardan değer almalıdır.
(base class name)?Tip argümanı sadece null yapılabilir veya null yapılamaz belirtilmiş base class’tan veya ondan türetilmiş classlardan değer almalıdır.
(interface name)Tip argümanı sadece implenment edilen interface tipinde değer almalıdır.
(interface name)?Tip argümanı sadece değer tipli, null yapılabilir veya null yapılamaz implenment edilen interface tipinde değer almalıdır.
where T: UTip argümanı sadece U için belirtilen tiplerden değer almalıdır.
internal class MyGenericClass<T>
    //where T : struct // Value Type Constraint
    //where T : class // Reference Type Constraint
    //where T : new() // Default Constructor Constraint
    //where T : MyClass, new()
    where T : IEnumerable<T>

{
}

internal class MyGenericClass<T, K>
where T : IEnumerable<T>
where K : class
{
}

İlk örnekteki gibi, T tip argümanına farklı türlerde kısıt verilebilir. Eğer bir tipe birden fazla kısıt vermek istiyorsak virgülle ayırmamız yeterli olacaktır.

Generic tanımlaması yaptığınızda bu değerler RAM’e derlenme zamanında değil çalışma zamanında çıkar. Fakat generic tipin değerinin, referans ve değer tipli olmasına göre farklı çalışır. Referans tipli bir atama yapıldığında belleğin heap bölgesine sadece 1 tane çıkar. Bunun yanı sıra değer tipliler için birden fazla değer çıkar.

Generic Kısıt Örnekleri

Generic class kısıtları için yeterince örnek hazırlayamadım. Ancak burada bulunan örnekleri inceleyebilirsiniz. Ayrıca her kısıt için ayrı ayrı örnek bulunmakta.

Benim generic tipler için notlarım bu kadar. Özetleyecek olursak gelişime ve değişime açık kod blokları oluşturmak için generic tiplerin kullanılması gerekiyor. Özellikle kendini tekrarlayan kod blokları yerine generic tipleri kullanarak kod kalitenizi arttırabilirsiniz. Bir sonraki yazıya kadar kendinize çok iyi bakın! 😊