Giriş: Tür Belirsizliği ve Kod Tekrarı Sorunu

Generic’ler öncesi dönemde, farklı veri tipleriyle çalışabilen ancak tür güvenliğini koruyan genel amaçlı kodlar yazmak zordu. İki ana yaklaşım vardı ve her ikisinin de ciddi dezavantajları bulunuyordu:

object Kullanımı (Non-Generic Koleksiyonlar ve Metotlar):

En esnek yaklaşım gibi görünür. System.Collections isim alanındaki ArrayList, Hashtable gibi eski koleksiyonlar veya metotlar, her türün temel sınıfı olan object türünü kabul ederdi. Bu sayede, örneğin bir ArrayList’e hem int, hem string, hem de özel bir Musteri nesnesi ekleyebilirdiniz.
Dezavantajları:
Tür Güvenliği Kaybı: Derleyici, koleksiyona hangi türlerin eklendiğini veya metottan hangi türün döndüğünü bilemez. Yanlış türde bir öğeyi koleksiyondan alıp kullanmaya çalıştığınızda, bu ancak çalışma zamanında (runtime) bir InvalidCastException hatasına neden olurdu. Hatalar derleme zamanında değil, çalışma zamanında ortaya çıkardı.
Boxing/Unboxing Performans Maliyeti: Değer tipleri (int, struct vb.) object olarak ele alındığında, boxing (değerin Heap’te bir nesne içine sarmalanması) ve geri alınırken unboxing (nesneden değerin çıkarılması) işlemleri gerekirdi. Bu işlemler, özellikle sık yapıldığında ciddi performans düşüşlerine neden olabilirdi.
Okunabilirlik ve Bakım Zorluğu: Kodu okuyan kişi, koleksiyonun veya metodun hangi türlerle çalışması gerektiğini anlamak için ekstra çaba sarf etmek zorunda kalırdı ve sürekli tür dönüşümü (casting) yapmak gerekirdi.
// Generic öncesi ArrayList örneği (System.Collections) using System.Collections; ArrayList liste = new ArrayList(); liste.Add(10); // int (Boxing yapılır) liste.Add("Merhaba"); // string liste.Add(true); // bool (Boxing yapılır) // Öğeyi alırken tür dönüşümü GEREKLİDİR ve RİSKLİDİR try { int sayi = (int)liste[0]; // Unboxing string metin = (string)liste[1]; // int hataliSayi = (int)liste[1]; // Çalışma zamanı hatası! InvalidCastException Console.WriteLine($"Sayı: {sayi}, Metin: {metin}"); } catch (InvalidCastException ex) { Console.WriteLine("Tür dönüşüm hatası: " + ex.Message); }
Her Tür İçin Ayrı Kod Yazma:

Tür güvenliğini sağlamak için, aynı temel mantığa sahip kodu farklı veri tipleri için tekrar tekrar yazmak gerekirdi. Örneğin, int’leri saklayan bir IntList sınıfı, string’leri saklayan bir StringList sınıfı, Musteri nesnelerini saklayan bir MusteriList sınıfı vb.

Dezavantajları:
Kod Tekrarı: Aynı algoritma veya veri yapısı mantığı birçok kez kopyalanıp yapıştırılırdı.
Bakım Zorluğu: Mantıkta bir değişiklik veya hata düzeltmesi gerektiğinde, bu değişikliğin tüm kopyalara uygulanması gerekirdi. Bu, zaman alıcı ve hataya açıktı.
Generic’lerin Çözümü:

.NET Framework 2.0 ve C# 2.0 ile tanıtılan Generic’ler, bu iki yaklaşımın dezavantajlarını ortadan kaldırdı. Generic’ler, sınıfları, yapıları, arabirimleri, metotları ve delegeleri, üzerinde çalışacakları belirli veri tür(ler)ini bir parametre olarak alacak şekilde tanımlamamızı sağlar.

Tür Parametreleri: Generic tanımlarken, belirli bir tür yerine bir tür parametresi (genellikle T, TKey, TValue gibi tek büyük harflerle temsil edilir) kullanılır.
Tür Güvenliği: Generic bir tür veya metot kullanıldığında, derleyiciye hangi somut türün (int, string, Musteri vb.) kullanılacağı belirtilir. Derleyici bu bilgiyi kullanarak derleme zamanında tür kontrollerini yapar ve yanlış tür kullanımını engeller.
Performans: Değer tipleriyle çalışırken gereksiz boxing/unboxing işlemlerini ortadan kaldırır, çünkü generic yapı doğrudan belirtilen değer tipiyle çalışacak şekilde optimize edilebilir.
Yeniden Kullanılabilirlik: Aynı generic tanım, farklı veri tipleriyle güvenli ve verimli bir şekilde tekrar tekrar kullanılabilir. Kod tekrarı büyük ölçüde azalır.
Bölüm 1: Generic Sınıflar ve Yapılar (class, struct)

Generic’lerin en yaygın kullanım alanı, farklı türlerde veri saklayabilen koleksiyonlar veya genel amaçlı sınıflar/yapılar oluşturmaktır.

1.1. Generic Sınıf Tanımlama:

Sınıf adından sonra küçüktür/büyüktür işaretleri (<>) içinde bir veya daha fazla tür parametresi (type parameter) belirtilir. Bu parametreler, sınıf içinde belirli bir tür yerine kullanılacak yer tutuculardır.

Sözdizimi:

[erişim_belirleyici] class SinifAdi // T: Tür Parametresi
{
// Alanlar, özellikler, metotlar T türünü kullanabilir
private T _veri;
public T VeriAl()
{
return _veri;
}
public void VeriAta(T yeniVeri)
{
_veri = yeniVeri;
}
// ... diğer üyeler ...
}
// Birden fazla tür parametresi
[erişim_belirleyici] class Cift
{
public TKey Anahtar { get; set; }
public TValue Deger { get; set; }
}
T, TKey, TValue: Tür parametrelerinin isimleridir. Geleneksel olarak tek büyük harf kullanılır (T genellikle Type için, K veya TKey Key için, V veya TValue Value için, U, S ek parametreler için), ancak anlamlı isimler de verilebilir (örn. TElement, TInput).
Sınıfın üyeleri (alanlar, özellikler, metot parametreleri, dönüş türleri) bu tür parametrelerini normal bir tür gibi kullanabilir.
1.2. Generic Sınıf Kullanımı (Örnekleme):

Generic bir sınıftan nesne oluştururken, tür parametrelerine karşılık gelen somut türleri (concrete types) belirtmemiz gerekir. Buna kapalı yapılı tür (closed constructed type) oluşturma denir.

using System;
using System.Collections.Generic; // List burada
// 1. Kendi Generic Sınıfımızdan Örnekleme
public class VeriSaklayici // Generic sınıf tanımı
{
private T _saklananVeri;
public VeriSaklayici(T baslangicVerisi)
{
_saklananVeri = baslangicVerisi;
}
public T VeriGetir() => _saklananVeri;
public void VeriGuncelle(T yeniVeri)
{
_saklananVeri = yeniVeri;
Console.WriteLine($"Veri güncellendi: {_saklananVeri}");
}
}
public class GenericSinifKullanim
{
public static void Main(string[] args)
{
// int türü için VeriSaklayici örneği
VeriSaklayici sayiKasasi = new VeriSaklayici(100);
Console.WriteLine($"Başlangıç Sayı: {sayiKasasi.VeriGetir()}"); // 100
sayiKasasi.VeriGuncelle(150); // Veri güncellendi: 150
// sayiKasasi.VeriGuncelle("Merhaba"); // HATA! Derleme hatası - string atanamaz (Tür Güvenliği)
// string türü için VeriSaklayici örneği
VeriSaklayici metinKasasi = new VeriSaklayici("İlk Metin");
Console.WriteLine($"Başlangıç Metin: {metinKasasi.VeriGetir()}"); // İlk Metin
metinKasasi.VeriGuncelle("Yeni Metin"); // Veri güncellendi: Yeni Metin
// metinKasasi.VeriGuncelle(true); // HATA! Derleme hatası - bool atanamaz
// 2. Yerleşik Generic Koleksiyon: List
List isimler = new List(); // Sadece string kabul eder
isimler.Add("Ali");
isimler.Add("Ayşe");
// isimler.Add(123); // HATA! Derleme hatası
foreach (string isim in isimler)
{
Console.WriteLine(isim.ToUpper()); // Doğrudan string metotlarını kullanabiliriz
}
List notlar = new List { 90.5, 88.0 }; // Sadece double kabul eder
notlar.Add(75.5);
}
}
Generic’ler sayesinde VeriSaklayici sınıfını hem int hem de string (veya başka herhangi bir tür) için ayrı ayrı yazmamıza gerek kalmadı. Derleyici, her kullanımda tür kontrollerini yaparak güvenliği sağladı.

1.3. Generic Yapılar (struct)

Sınıflar gibi, yapılar da generic olabilir. Aynı söz dizimi ve kurallar geçerlidir. Generic yapılar da değer tipi semantiğine sahiptir.

public struct Koordinat where T : struct // Tür kısıtlaması (sadece değer tipleri)
{
public T X { get; set; }
public T Y { get; set; }
public Koordinat(T x, T y) { X = x; Y = y; }
public override string ToString() => $"({X}, {Y})";
}
// Kullanım
Koordinat tamSayiNokta = new Koordinat(10, 20);
Koordinat ondalikliNokta = new Koordinat(3.5, 7.2);
// Koordinat metinNokta; // HATA! Tür kısıtlaması nedeniyle string kullanılamaz.
Console.WriteLine(tamSayiNokta); // (10, 20)
Console.WriteLine(ondalikliNokta); // (3.5, 7.2)
Bölüm 2: Generic Metotlar (MetotAdi(…))

Sadece belirli bir metotun farklı türlerle çalışmasını istiyorsak, tüm sınıfı generic yapmak yerine sadece o metodu generic yapabiliriz.

2.1. Generic Metot Tanımlama:

Metot adından sonra (ve parametre listesinden önce) küçüktür/büyüktür işaretleri (<>) içinde bir veya daha fazla tür parametresi belirtilir. Bu tür parametreleri, metodun parametre türlerinde, dönüş türünde veya metot gövdesi içindeki yerel değişkenlerde kullanılabilir.

Sözdizimi:

[erişim_belirleyici] [static] dönüş_türü MetotAdi([parametre_listesi_T_kullanan])
{
// Metot gövdesi T türünü kullanabilir
}
Örnek:

public class YardimciMetotlar
{
// İki değeri yer değiştiren generic metot
// (ref kullanarak orijinal değişkenleri değiştirir)
public static void Degistir(ref T a, ref T b)
{
Console.WriteLine($"Değiştiriliyor: a={a}, b={b}");
T gecici = a;
a = b;
b = gecici;
Console.WriteLine($"Değiştirildi: a={a}, b={b}");
}
// Bir dizinin elemanlarını yazdıran generic metot
public static void DiziyiYazdir(TElement[] dizi)
{
if (dizi == null || dizi.Length == 0)
{
Console.WriteLine("Dizi boş.");
return;
}
Console.Write("[ ");
foreach (TElement eleman in dizi)
{
Console.Write(eleman + " ");
}
Console.WriteLine("]");
}
}
// Kullanım
public class GenericMetotKullanim
{
public static void Main(string[] args)
{
int x = 5, y = 10;
Console.WriteLine($"Önce: x={x}, y={y}");
YardimciMetotlar.Degistir(ref x, ref y); // Türü açıkça belirttik (int)
Console.WriteLine($"Sonra: x={x}, y={y}");
string s1 = "İlk", s2 = "Son";
Console.WriteLine($"\nÖnce: s1={s1}, s2={s2}");
YardimciMetotlar.Degistir(ref s1, ref s2); // TÜR ÇIKARIMI: Derleyici argümanlardan T'nin string olduğunu anlar
Console.WriteLine($"Sonra: s1={s1}, s2={s2}");
double[] doubleDizi = { 1.1, 2.2, 3.3 };
YardimciMetotlar.DiziyiYazdir(doubleDizi); // TElement = double
char[] charDizi = { 'a', 'b', 'c' };
YardimciMetotlar.DiziyiYazdir(charDizi); // TElement = char
}
}
2.2. Tür Çıkarımı (Type Inference):

Generic metotları çağırırken, derleyici genellikle metota geçirilen argümanların türlerine bakarak tür parametresinin (T gibi) ne olması gerektiğini otomatik olarak çıkarabilir. Bu durumda, metot çağrısında veya gibi türü açıkça belirtmemize gerek kalmaz (YardimciMetotlar.Degistir(ref s1, ref s2); örneğindeki gibi). Bu, kodu daha kısa hale getirir. Ancak derleyici türü çıkaramazsa veya belirsizlik varsa, türü açıkça belirtmeniz gerekir.

Bölüm 3: Tür Kısıtlamaları (Constraints)

Generic tür parametrelerinin (T gibi) varsayılan olarak herhangi bir tür olabilmesi esneklik sağlar, ancak bazen tür parametresinin belirli özelliklere sahip olmasını (örn. belirli bir sınıftan türemiş olması, belirli bir arayüzü uygulaması, bir yapıcıya sahip olması) zorunlu kılmak isteyebiliriz. Bu, generic kodun içinde o türe özgü metotları veya özellikleri güvenle kullanabilmemizi sağlar. Bu zorunluluklara tür kısıtlamaları (constraints) denir ve where anahtar kelimesiyle tanımlanır.

Sözdizimi:

class BenimGenericSinifim where T : Kısıtlama1, Kısıtlama2, ...
{ ... }
void BenimGenericMetodum(T param) where T : Kısıtlama1, Kısıtlama2, ...
{ ... }
where T :: T tür parametresi için kısıtlamaların başladığını belirtir.
Yaygın Kısıtlamalar:
where T : class: T bir referans tipi (sınıf, arayüz, delege veya dizi) olmalıdır.
where T : struct: T Nullable olmayan bir değer tipi (int, bool, struct vb.) olmalıdır.
where T : new(): T’nin public, parametresiz bir yapıcısı (constructor) olmalıdır. Bu, generic kod içinde new T() ile o türden yeni örnekler oluşturmanızı sağlar. Bu kısıtlama en sonda belirtilmelidir.
where T : : T, belirtilen temel sınıftan türemiş olmalı veya o sınıfın kendisi olmalıdır. Sadece bir tane temel sınıf kısıtlaması olabilir.
where T : : T, belirtilen arayüzü uygulamalıdır. Birden fazla arayüz kısıtlaması olabilir.
where T : U: T tür parametresi, başka bir tür parametresi olan U ile aynı türde olmalı veya ondan türemiş olmalıdır.
where T : unmanaged (C# 7.3+): T, yönetilmeyen (unmanaged) bir tür olmalıdır (pointer olmayan, generic olmayan ve bool, char, enum, sayısal tipler gibi belirli struct’ları içeren). Düşük seviyeli kodlar için kullanılır.
where T : notnull (C# 8+): T, null olamayan (non-nullable) bir tür olmalıdır (referans veya değer tipi olabilir).
Örnekler:

// T'nin IComparable arayüzünü uygulamasını zorunlu kılma (Karşılaştırma için)
public static T EnBuyuk(T a, T b) where T : IComparable
{
return a.CompareTo(b) > 0 ? a : b; // CompareTo metodu artık güvenle çağrılabilir
}
// T'nin bir sınıf (referans tipi) ve public parametresiz yapıcıya sahip olmasını zorunlu kılma
public class Depo where T : class, new()
{
public T YeniOlustur()
{
return new T(); // new() kısıtlaması sayesinde bu mümkün
}
}
// T'nin hem ILogger hem de IDisposable olmasını zorunlu kılma
public void LoglaVeTemizle(T logger) where T : ILogger, IDisposable
{
try {
logger.LogMesaj("İşlem yapılıyor...");
}
finally {
logger.Dispose(); // IDisposable sayesinde Dispose çağrılabilir
}
}
// Kullanım
Console.WriteLine(EnBuyuk(5, 10)); // 10
// Console.WriteLine(EnBuyuk(new object(), new object())); // HATA! object, IComparable uygulamaz.
Depo> listDepo = new Depo>(); // List class ve new()'a uyar
List yeniListe = listDepo.YeniOlustur();
// Depo sayiDepo = new Depo(); // HATA! int, class kısıtlamasına uymaz (struct'tır).
Kısıtlamalar, generic kodun yeteneklerini genişletirken tür güvenliğini korumanın anahtarıdır.

Bölüm 4: Generic Arabirimler ve Delegeler

Generic Arabirimler (interface IAdi): Arabirimler de generic olabilir. Bu, belirli bir türle çalışacak bir sözleşme tanımlamayı sağlar. En bilinen örnekleri IEnumerable, ICollection, IList, IDictionary gibi koleksiyon arayüzleridir.
public interface IKarsilastirici { int Karsilastir(T x, T y); } public class SayiKarsilastirici : IKarsilastirici { public int Karsilastir(int x, int y) => x.CompareTo(y); }
Generic Delegeler (delegate TResult Func(T arg)): Delegeler (metotlara referans tutan tipler) de generic olabilir. .NET’teki Action<…> (değer döndürmeyen) ve Func<…> (değer döndüren) generic delegeleri, lambda ifadeleri ve metot grupları ile birlikte çok yaygın olarak kullanılır.
// Func : T türünde bir argüman alır, TResult türünde değer döner Func ciftMi = sayi => sayi % 2 == 0; Console.WriteLine(ciftMi(4)); // True // Action : T1 ve T2 türünde iki argüman alır, değer döndürmez Action yazdir = (mesaj, tekrar) => { for(int i=0; i Bölüm 5: Generic’ler ve Kalıtım

Generic sınıflar da kalıtım hiyerarşilerine katılabilir.
Bir alt sınıf, üst generic sınıfın tür parametresini belirleyebilir veya kendisi de generic kalabilir.
// 1. Tür parametresini belirleyen alt sınıf
public class StringListesi : List // List'den T'yi string olarak belirleyerek miras alır
{
// List'in tüm metotlarını miras alır
// İsterse ek metotlar tanımlayabilir
public void IlkHarfiBuyukYazdir()
{
foreach(string s in this)
{
if (!string.IsNullOrEmpty(s))
Console.WriteLine(char.ToUpper(s[0]) + s.Substring(1));
}
}
}
// 2. Kendisi de generic kalan alt sınıf
public class SiraliListe : List where T : IComparable // Kısıtlama miras alınmaz, gerekirse tekrar belirtilir
{
public void EkleVeSirala(T eleman)
{
base.Add(eleman);
base.Sort(); // List'nin Sort metodunu kullanır (T, IComparable olmalı)
}
}
Bölüm 6: Generic’lerin Avantajları (Özet)

Tür Güvenliği (Type Safety): Hataların derleme zamanında yakalanmasını sağlar, çalışma zamanı InvalidCastException riskini azaltır.
Performans: Değer tipleri için boxing/unboxing işlemlerini ortadan kaldırarak performansı artırır.
Kodun Yeniden Kullanılabilirliği: Aynı kod mantığını farklı veri tipleri için tekrar yazmak yerine tek bir generic yapı tanımlanır.
Okunabilirlik ve Bakım Kolaylığı: Kod daha temiz, daha kısa ve anlaşılır hale gelir. Bakımı kolaylaşır.
Daha Güçlü API’ler: Kütüphane ve framework geliştiricilerinin esnek ve tür güvenli API’ler oluşturmasını sağlar (List, LINQ vb.).
Bölüm 7: Sonuç — Modern C#’ın Vazgeçilmezi

Generic’ler, C# 2.0'dan beri dilin ayrılmaz bir parçası olmuş ve modern .NET geliştirmenin temelini oluşturmuştur. Tür güvenliği, performans ve kodun yeniden kullanılabilirliği gibi kritik sorunlara zarif ve etkili bir çözüm sunarlar. Generic sınıflar, yapılar, metotlar, arabirimler ve delegeler, belirli bir türe bağlı kalmadan esnek ve güçlü algoritmalar ve veri yapıları tasarlamamıza olanak tanır.

Tür kısıtlamaları (where), generic kodun yeteneklerini genişletirken güvenliği korurken, tür çıkarımı gibi özellikler generic metotların kullanımını kolaylaştırır. List ve Dictionary gibi temel generic koleksiyonlar, eski ArrayList ve Hashtable’ın yerini alarak .NET’te veri yönetiminin standart yolu haline gelmiştir.

Generic’leri anlamak ve etkin bir şekilde kullanmak, sadece daha iyi kod yazmakla kalmaz, aynı zamanda LINQ, Entity Framework Core, ASP.NET Core gibi modern .NET teknolojilerini ve kütüphanelerini tam olarak kavramak için de zorunludur. Generic’ler, C#’ı güçlü, güvenli ve verimli bir dil yapan temel özelliklerden biridir ve her C# geliştiricisinin araç kutusunda mutlaka bulunmalıdır.

Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Özgeçmiş
Github
Github
Linkedin