C# Generics (Şablon Tipler)

Merhaba arkadaşlar,

Yine uzun bir aradan sonra tekrar bloguma kavuştum. Buna vesile olan ve aynı zamanda başımıza dert olan Korona virüsüne bela olsun 🙂 Bela okuduk ama aslında bu virüsün bizlere güzel katkıları oldu 🙂 Bunlardan biri home-office çalışma fırsatı ve bundan doğan zaman. Bu zamanı iyi değerlendirebilmeliyiz. Çok uzatmayacağım ve bu yazıma tekrar kaldığım yerden devam etmek istiyorum.

Bu yazımda Şablon Tiplerden (Generics) yapılarından bahsedeceğiz. Bu yapılar hakkında bilgi edinip örnekler ile pekiştirmeye çalışıcağız.

Motivasyon cümlemiz

Dibe vurmanızın harika bir yanı; yukarıya çıkmaktan başka çarenizin olmamasıdır.

Bu yazıda bahsedilecek konular;

  • Şablon Tiplere Örnek: Liste Koleksiyonu
  • Çalışma Zamanı ve Şablon Türler (Generic Types)
  • Sınıf Aşırı Yükleme (Class Overloading)
  • Şablon Tipler Arasında Türetme
  • Şablon Tipler ve Arayüz (Interface)
  • Şablon Tiplerin Metotlara Etkisi
  • default Operatörü
  • Kısıtlar (Constraints)
  • Şablon Tipli Metotlar
    • Şablon Tipli Çıkarsama (Type Interface)
  • Şablon Tipli Temsilciler
  • Tuple (Sınıf) ve ValueTuple (Struct) Şablon Veri Tipleri
    • C# 7.0 ve Tuple Türleri
    • Deconstruct Özel Metodu ile Tuple İlişkisi
  • Discard ( _ ) İşareti ile Optimizasyon

Profesyonel anlamda yaptığımız projelerde, algoritmalarımızda ve inşa ettiğimiz yapılarda zorlandığımız zamanlar oldu. Bunlardan biri türden bağımsız geliştirme yapmak. C# bu geliştirme doğrudan destek vermiyorsa da dolaylı olarak bu geliştirmeyi yapmamıza imkan sağlıyor.

Bildiğimiz gibi C#’ta “Her şey bir objedir” mantığı var. Şablon türler olmadan önce salında bunu object türü ile yapabiliyorduk. Ancak object türüne atanan referanslar çalışma zamanında performans kaybına yol açıyordu. Demek istediğim object türünden bir özelliğe int türünden bir özelliği atamak isterseniz bunun adı unboxing olur. (boxing ve unbox,ing hatırlamıyorsanız bir tık ötede). İşte bu gibi performans sorunları yaşanırken C# 2.0 ile birlikte dile Generics (Şablon Tipler) eklenmiştir.

Bu noktoda, Generic yapıların yararları, boxing-unboxing işlemlerin etkisi (performans sorunu) ve type safe (tip güvenliği) için boyu dinamik olarak genişleyen bir collection nesnesi ile ele almaya çalışacağız.

SlowCollection adında bir sınıf oluşturuyoruz ve gerekli geliştirmeleri sınıfımın içinde yapıyoruz. Bu sınıf farklı veri türlerini yönetebileceğimiz koleksiyon sınıfımız olacak.

Geliştirdiğimiz bu sınıfın avantajlarına bakacak olursak;

  • object tipi ile diğer türleri atayabilme imkanı,
  • Add() metodu ile eğer kapasite yok ise dinamik olarak kapasiteyi artırma
  • Indeksleyici ile diziden herhangi bir elamana ulaşabilme.
  • Capacity özelliği ile çalışma zamanında dizimizi yeni dizeye kopyalayabiliyoruz.

Geliştirdiğimiz bu sınıfın dezavantajlarına bakacak olursak;

  • Birincisi Add() metodundaki object tipli parametre. İlk baktığımızda süper diğer türleri kapsar diyebiliriz ama herhangi bir tür güvenliğinden bahsedemeyiz. Bu koleksiyona farklı türden nesneler ekleyebiliriz ve derleme zamanında bir hataya yol açmayabilir ama çalışma zamanında beklenmedik hatalara sebeb olacaktır.
  • İkinci önemli dezavantaj ise performans kaybı. Listeye eklenen her bir parametre eğer değer tipli ise performans kaybı olacaktır. Çünkü değer tipin referans tipine dönüşümü aslında bir boxing özelliğidir ve veriyi okuduğumuzda bir unboxing işlemi olacak ve gereksiz yere veri kopyalama işleri oluşacak. Buda yoğun olarak kullandığımız koleksiyonlar için önemli bir performans kaybı olacaktır. (boxing ve unboxing yazıma buradan ulaşabilirsiniz)

Peki bu bahsettiğimiz dezavantajlardan kaçınmak için SlowCollection sınıfının şablon tür versiyonunu geliştireceğiz. C# 2.0 ile birlikte CLR (Comman Language Runtime)’ye şablon tür (generics) özelliği eklenmiştir.

Önemli Not: Şablon türler sadece derleme aşaması ile ilgili değiş aynı zamanda çalışma zamanıyla da ilgilidir.

Şablon tür versiyonu için FastCollection<T> sınıfımızı oluşturuyoruz. Mantık SlowCollection sınıfı ile aynı olacak tek fark bu koleksiyonun Şablon Tür olduğudur. Bu sınıf şablon tipli olduğu için nesne yaratıldığı anda tek tip (belirtilen tip) olacaktır. Buda daha önce yaptığımız SlowCollection sınıfına göre tip güvenliğini sağlamış olacak ve tek tiple çalışacağımız için performans sorununundan kurtulacağız.

Generic yapıdaki FastCollection<T> yazdıktan sonra gelelim bu iki sınıfı karşılaştırmaya. Performans sorunun net bir şekilde görebilmek için iki sınıfı nesnesini oluşturup elemanlar ekliyoruz ve eklediğimiz elemanlara erişmeye çalışıyoruz. Bu işlemleri yaparken de TimeSpan ile işlem sürelerini ölçeceğiz.

Output’a baktığımızda yaklaşık olarak 3 katı bir performans kaybından bahsedebiliriz. Hem tip güvenliğini sağlamış olduk hem de performansı artırabildik 🙂 Buradan şu sonucu çıkarabiliriz Şablon Türlü (generics) yapacağımız geliştirmeler hem tip güvenliğini (type safe) salamış olacak hem de performans kaybına engel olacak. Kulağa hoş geliyor 🙂

Çalışma Zamanı ve Şablon Türler (Generic Types)

Şablon tür ile oluşturulan sınıflar nesne yaratıldığı anda her bir türün hafıza yapısı ve gerçek nesne modeli çalışma zamanında belirlenir. Yani derleme sırasında kod içinde tamınlanan her farklı şablon tür için ayrı bir sınıf bildirimi yaratılmaz.

  • Hafıza yapısı ve nesne modeli çalışma zamanında belirlenir.
  • Generic Type belirlenerek oluşturulan her nesneye constructed type (inşa edilen tip) denir.
  • CLR (Common Language Runtime) herhangi bir değer tipli şablon oluşturursa bellekte yeni bir alan oluşturur.
  • Referans tiplilerde işin rengi değişiyor. Referans tipli şablonlar sadece bir adet alan kullanır

Aşağıda kodları incelediğinizde kaç adet Şablon Tipli nesne oluşur?

Yukarıdaki örneğe göre herhangi bir değer tipli constructed type oluşturulursa bellekte yeni bir koleksiyon oluşturur. Ancak referans tipli şablon türlerinde durum biraz farklı. Referans tipler hatırlarsanız değerleri direkt stack bölgesinde bulunduğunda bir referans tipi sadece bir adresi temsil etmektedir. Bu yüzden hangi referans tipi kullanılırsa kullanılsın aynı versiyon kullanılır.

Sınıf Aşırı Yükleme (Class Overloading)

Bir sınıf içinde veya şablon tipli belirtilerek (interface, delegate, method, struct) oluşturulan elaman içinde birden fazla şablon tür olabilir. Örnek üzerinde gösterecek olursak CacheService adında bir sınıf oluşturuyoruz

DatabaseService adında iki farklı sınıf oluşturuyorum

Şablon Tipler Arasında Türetme

Şablon tipli bir sınıf bir başka Şablon Tipli sınıftan türeyebilir. Bunun için aşağıdaki kodu inceleyiniz. BaseGeneric adında üst sınıfımız var ve DatabaseService adında daha alt sınıfımız var. DatabaseService’imiz BaseGeneric sınıfından türettik. BaseGeneric sınıfı string tipli olduğundan Console System.String adını yazdırdı. Alt sınıf olan DatabaseService ise BaseGeneric sınıfına T1 tipini gönderiyor ve bu sayede ekrana (int) System.Int32 yazdırıyor.

Türetme polimorfizm gibi konuları ele aldığından Şablon Tiplerin birçok permütasyonunu oluşturabiliriz.

Şablon Tipler ve Arayüz (Interface)

Sınıflarda olduğu gibi şablon tiplerde de arayüz (interface) kullanılır. System.Collection altında birçok Şablon Tipi arayüz var. Aşağıda Şablon Tipli arayüzler vardır. Daha detaylı bilgi için aşağıdaki linkten yararlanabilirsiniz.

ICollection<T>Genel koleksiyonları işlemek için yöntemleri tanımlar.
IComparer<T>İki nesneyi karşılaştırmak için bir türün uyguladığı bir yöntemi tanımlar.
IDictionary<TKey,TValue>Anahtar/değer çiftlerinin genel koleksiyonunu temsil eder.
IEnumerable<T>Belirtilen türde bir koleksiyon üzerinde basit bir yinelemeyi destekleyen Numaralandırıcı gösterir.
IEnumerator<T>Genel bir koleksiyon üzerinde basit bir yinelemeyi destekler.
IEqualityComparer<T>Nesnelerin eşitlik için karşılaştırmasını destekleyecek yöntemleri tanımlar.
IList<T>Dizin tarafından tek tek erişilebilen nesne koleksiyonunu temsil eder.
IReadOnlyCollection<T>Türü kesin belirlenmiş, salt tanımlı bir öğe koleksiyonunu temsil eder.
IReadOnlyDictionary<TKey,TValue>Anahtar/değer çiftlerinin genel bir salt okuma koleksiyonunu temsil eder.
IReadOnlyList<T>Dizin tarafından erişilebilen öğelerin salt okunurdur koleksiyonunu temsil eder.
ISet<T>Küme soyutlaması için temel arabirimi sağlar.
https://docs.microsoft.com/tr-tr/dotnet/api/system.collections.generic?view=netframework-4.8

Şablon Tiplerin Metotlara Etkisi

Şablon Tipler aynı sınıflarda olduğu gibi metotlarda da kullanılır. Şablon Tipli nesneler biliyoruz ki çalışma zamanında belli oldukları için metotlarda oluşacak overloading (aşırı yükleme) dikkatli olmak gerekir.

Özellik, indeksleyici ve olaylarla birlikte kullanılır. Bunu anlamak için şöyle bir örnek yapalım. Şablon tipli bir sınıf oluşturuyoruz ve aynı ada sahip iki farklı method oluşturacağız. Bunlardan biri normal metot diğeri ise Şablon Tipli metot. GetInfo metodu çağrıldığında Console’da “GetInfo with string” değerinin yazıldığını görüyoruz. Buda sınıf içerisindeki GetInfo methodunda çakışma olduğunu gösterir.

default Operatörü

default operatörü basit ama kullanışlı bir operatördür. Kodunuza görünüş bakımından katkı sağlar. Vay bee dedirtir. Ancak kendisinin basit bir görevi vardır. C# 2.0 ile birlikte referans tipler için null, değer tipler için ilgili tipin varsayılan değerini döner.

default operatörünün kullanımı için DefaultGeneric<T> Şablon tipli nesne oluşturuyoruz. Bu sınıfın içine GetDefault adında bir metot ekliyoruz. Metodun görevi çok basit. 2 parametre alacak ve ikinci parametreye göre geri dönüş yapacak. Aşağıdaki kod örneğinde Console’a GetDefault metodunu çağırarak 0 döndüğünü görüyorsunuz.

Kısıtlar (Constraints)

Şablon Tipli geliştirmeye başladıktan sonra her ne kadar türden bağımsız algoritmalar geliştirsekte Şablon Tür ile geliştirdiğimiz bir yapının o şablon türünün özelliğini bilmediğimiz için istediğimiz gibi işlem yapamayız. Yani kafamıza göre takılamayız. Hatalara sebep olur. Bu hataları daha detaylı incelemek için Virus sınıfını ve Generic type ile sarmalanmış (wrap) VirusGeneric<T> sınıfını oluşturuyoruz. Virus sınıfının bir özelliğini VirusGeneric sınıfında kullanmak istediğimizde ve aşağıdaki hatayı alırız.

Yukarıdaki durumu çözmek için yani Şablon Tipli yapımızdaki şablon türünü bilmemiz gerekiyor. Bu yüzden C# 2.0 ile birlikte 5 adet kısıtlayıcı geldi.

  • struct -> şablon tipi struct olmalı
  • class -> şablon tipi sadece sınıf olabileceği bildirilir.
  • new() -> şablon tipi mutlaka varsayılan yapıcı metot içermeli
  • türetme -> şablon tipi belirtilen türden türemeli
  • interface -> şablon tipi sadece belirtilen interfaceden türemeli
  • notnull -> şablon tipi boş olamaz *C# 8.0 ile birlikte geldi
  • unmanaged -> şablon tipi boş olmayan ve yönetilmeyen tür olmalıdır.

Kısıtlarla ilgili detayli bilgiye buradan göz atabilirsiniz. Aşağıda her bir kısıtlayıcı için örnek verilmiştir.

Yukarıdaki örneklerden çıkarımlar

  • Kısıtlamalar where anahtar sözcüğü ile kullanılırlar
  • Birden fazla kısıtlama kullanılabilir. Bu durumda kısıtlar arası virgül kullanılır ve new() en sonda kullanılmalıdır.
  • Kısıtlamar çalışma zamanında değil derleme zamanını ile ilgilidir.

Şablon Tipli Metotlar

Metot seviyesinde şablon tip tanımlama biçimi, sınıf seviyesinde tanımlama biçimi ile hemen hemen aynıdır. Aynı zamanda Temsilci (delegate) seviyesine de tanımlanabilir

Şablon Tipli Çıkarsama (Type Interface)

C# Şablon tipli parametrelere bakarak otomatik bir çıkarsama yapabilmektedir.

Şablon Tipli Temsilciler

Şablon tipli temsilciler kullanılarak parametre sayıları aynı olan faklı methodları tek temsilci ile ifade edebiliriz. Bunu bir örnekte görebilmek için GenericDelegate<T>(T t1, T t2) adında bir temsilci oluşturuyoruz. Temsilci için metot gerekiyor o yüzden basit Topla metodu yaratıp Temsilci ile ifade edeceğiz.

Tuple (Sınıf) ve ValueTuple (Struct) Şablon Veri Tipleri

Tuple, 2010 yılında, Microsoft .Net 4.0 sürümü ile birlikte beraber geldi. Tuple matematikte sonlu serilere verilen isimdir.

Genellikle aşağıdaki amaçlar için kullanılır;

  • Bir veri tipini kolay bir şekilde modellemek için Tuple’dan yararlanabiliriz.
  • out anahtarını kullanmamıza gerek yok. Yani out keyword kullanmadan bir metottan birden fazla değer dönmek için kullanırız.
  • Tek bir veri kümesini temsil etmek için kullanırız. Örneğin, bir tuple veri tabanı kaydını temsil edebilir ve elemanları kaydın alanlarını temsil edebilir.
  • Aşırı kullanımı kod kalitesini bozabilir.

Bu bilgiler neticesinde Tuple nesnesini kullanarak isim soy isim döndüren bir örnek yapalım.

ValueTuple ise Tuple ile aynı özelliklere sahiptir. Dikkat etmemiz gereken şey veri yapısı struct olmasıdır. ValueTuple kullanabilmek için minimum .Net Framework 4.7 kurulu olması gerekir.

C# 7.0 ile birlikte Tuple’da yeni geliştirmeler yapıldı. Bu geliştirmelerle birlikte hayatımıza Tuple Type ve Tuple Literal özellikleri geldi.

Yukarıda yaptığımız örneği refactör ederek yeni özelilkler ile geliştirelim.

Yine C# 7.0 ile birlikte gelen TupleType ve geri dönüş elemanlarına anlamlı isimler verebiliriz.

GetInfo metoduna bir refactor yaparak yeni haline kazandıralım.

yukarıda geri dönüş elemanlarını sırayla vermek yerine eşleme yoluyla da verebiliriz. O halde tekrar kodumuzu refactör edersek.

Deconstruct Özel Metodu ile Tuple İlişkisi

C# 7.0 ile birlikte sınıf içerisindeki özelliklerin dış dünyaya sunabilme seçeneği geldi. Bu işlemi yapabilmek için sınıf içerisinde Deconstruct metodu tanımlanır.

Deconstruct özelliği projemizde önemli özelliklere erişimi standart hale getirebilir ve kodun kalitesini artırabilir. Deconstruct özelliğini TupleType ile kullabiliriz. Kullanım için .Net Framework 4.7 olmalı.

Deconstruct özelliği katarak geliştirme yapmaya başlayalım. Person sınıfımız olsun isim ve soy-ismi dış dünyaya deconstruct ile sunalım.

Deconstruct hakkında bilmemiz gerekenler

  • Sınıf içerisinde Deconstruct ile tanımlanırlar
  • Decosntruct metodundaki parametreler out ile bildirmelidirler.
  • Sınıf içerisinde önemli özelliklere erişimi standart hale getirebilir
  • Kodun kalitesi artırılabilir.
  • Deconstruct özelliği ValueTuple ile kullanılabilir.
  • Kullanım için .Net Framework 4.7 olmalıdır

Discard ( _ ) İşareti ile Optimizasyon

C# 7.0 ile birlikte bellek optimizasyonu için geldi. İlgilenmediğimiz değişkenlerin gereksiz yere bellekte yer kaplamaması için geliştirilmiş bir dil özelliğidir.

Discard özelliğinin kullanımı için İletişim bilgilerinden GSM no sadece ihtiyacımız var ve diğer alanları önemsemiyoruz.

Discard karakterinin genel olarak kullanımı sınırlıdır. Bu durumlar;

Tuple ve Deconstruct kullanımı ( daha önceden yaptığımız örnekler)

Is operatöründe desen eleştirmede değişkeni yok saymak için

out parametreli metot çağrımında

Metot çağrımından dönen parametrelerin önemsenmediği durumlarda

Not: Yukarıdaki durumlar dışında kullanılan "_" karakteri standart bir değişken adı olarak algılanacaktır.

Bu yazımda Şablon Türler hakkında edindiğim bilgileri paylaşmaya çalıştım. Umarım faydalı olmuştur.

Bir sonraki yazımda görüşmek üzere 🙂

Kaynaklar:

Her Yönüyle C# 7.0, Sefer Algan, Pusula Yayincilik

https://docs.microsoft.com/tr-tr/dotnet/csharp/programming-guide/generics/

Leave a Comment