C# Koleksiyonlar: Dictionary, HashSet, Queue, Stack Rehberi

C# dilinde Dictionary, HashSet, Queue ve Stack gibi güçlü koleksiyon yapılarını öğrenin. Performans ipuçları ve pratik örneklerle oyun geliştirme becerilerinizi artırın.

C# programlama dilinde veri yapıları, uygulamalarımızın temelini oluşturur. Verileri verimli bir şekilde depolamak, erişmek ve işlemek, performanslı ve bakımı kolay kod yazmanın anahtarıdır. Bu makalede, C# Koleksiyonlar dünyasının en sık kullanılan ve güçlü üyelerinden dördünü derinlemesine inceleyeceğiz: Dictionary<TKey, TValue>, HashSet<T>, Queue<T> ve Stack<T>. Her birinin kendine özgü kullanım durumları ve avantajları vardır ve doğru yerde doğru koleksiyonu kullanmak, kodunuzun hem okunabilirliğini hem de verimliliğini önemli ölçüde artıracaktır.

C# Koleksiyonlar: Temel Bilgiler ve Kullanım Alanları

C#’taki koleksiyonlar, birbiriyle ilişkili nesneleri depolamak ve yönetmek için tasarlanmış sınıflardır. .NET Framework, System.Collections ve System.Collections.Generic isim alanları altında birçok farklı koleksiyon türü sunar. Generic koleksiyonlar (örneğin List<T>, Dictionary<TKey, TValue>), tip güvenliği sağlar ve değer tipleri için boxing/unboxing ihtiyacını ortadan kaldırarak performansı artırır.

1. Dictionary<TKey, TValue>: Anahtar-Değer Depolama

Dictionary<TKey, TValue>, anahtar-değer çiftlerini depolayan bir koleksiyondur. Her anahtar benzersiz olmalıdır ve bir değere hızlı bir şekilde erişmek için kullanılır. Anahtarın hash kodu kullanılarak değerlere ortalama O(1) (sabit zaman) karmaşıklığında erişim sağlar. Bu özellik, büyük veri kümelerinde bile çok hızlı arama işlemleri yapılmasına olanak tanır.

Kullanım Alanları:

  • Oyunlarda öğe ID’lerini nesnelere eşleme (örneğin, item_id -> ItemData).
  • Ayarlar veya konfigürasyon verilerini depolama.
  • Hafızada önbelleğe alma sistemleri.

Örnek Kullanım:

using System;
using System.Collections.Generic;

public class DictionaryOrnegi
{
    public static void Main(string[] args)
    {
        Dictionary<string, int> oyuncuSkorlari = new Dictionary<string, int>();

        oyuncuSkorlari.Add("Alice", 1500);
        oyuncuSkorlari.Add("Bob", 1200);
        oyuncuSkorlari["Charlie"] = 1800; // Yeni eleman ekleme veya mevcut olanı güncelleme

        Console.WriteLine($"Alice'in skoru: {oyuncuSkorlari["Alice"]}");

        // Anahtarın varlığını kontrol etme
        if (oyuncuSkorlari.ContainsKey("Bob"))
        {
            Console.WriteLine("Bob listede var.");
        }

        // Güvenli değer alma
        if (oyuncuSkorlari.TryGetValue("David", out int davidSkor))
        {
            Console.WriteLine($"David'in skoru: {davidSkor}");
        }
        else
        {
            Console.WriteLine("David listede yok.");
        }

        // Eleman silme
        oyuncuSkorlari.Remove("Bob");

        Console.WriteLine("\nTüm skorlar:");
        foreach (var entry in oyuncuSkorlari)
        {
            Console.WriteLine($"{entry.Key}: {entry.Value}");
        }
    }
}

2. HashSet<T>: Benzersiz Eleman Kümeleri

HashSet<T>, benzersiz elemanlardan oluşan bir küme depolamak için kullanılır. Bir elemanın koleksiyonda olup olmadığını hızlı bir şekilde kontrol etmek, eleman eklemek veya silmek için idealdir. Tıpkı Dictionary gibi, HashSet de hash tablolarını kullanarak ortalama O(1) karmaşıklığında işlemler sunar. Bir elemanı birden fazla kez eklemeye çalıştığınızda, koleksiyon sadece bir kopyasını saklar.

Kullanım Alanları:

  • Oyunlarda ziyaret edilen bölgeleri veya toplanan öğeleri takip etme (her öğe sadece bir kez sayılmalı).
  • Benzersiz kullanıcı ID’lerini depolama.
  • Küme işlemleri (birleşim, kesişim, fark) yapmak.

Örnek Kullanım:

using System;
using System.Collections.Generic;

public class HashSetOrnegi
{
    public static void Main(string[] args)
    {
        HashSet<string> ziyaretEdilenBolgeler = new HashSet<string>();

        ziyaretEdilenBolgeler.Add("Orman");
        ziyaretEdilenBolgeler.Add("Dağ");
        ziyaretEdilenBolgeler.Add("Orman"); // Tekrar eklenmeyecektir

        Console.WriteLine($"Toplam ziyaret edilen bölge sayısı: {ziyaretEdilenBolgeler.Count}"); // Çıktı: 2

        if (ziyaretEdilenBolgeler.Contains("Dağ"))
        {
            Console.WriteLine("Dağ ziyaret edildi.");
        }

        ziyaretEdilenBolgeler.Remove("Orman");

        Console.WriteLine("\nKalan bölgeler:");
        foreach (var bolge in ziyaretEdilenBolgeler)
        {
            Console.WriteLine(bolge);
        }
    }
}

3. Queue<T>: İlk Giren İlk Çıkar (FIFO)

Queue<T>, ‘İlk Giren İlk Çıkar’ (FIFO – First-In, First-Out) prensibiyle çalışan bir koleksiyondur. Yani, koleksiyona ilk eklenen eleman, ilk çıkarılan eleman olacaktır. Gerçek hayattaki bir kuyruğa benzer. Eleman ekleme (Enqueue) ve çıkarma (Dequeue) işlemleri ortalama O(1) karmaşıklığına sahiptir.

Kullanım Alanları:

  • Görev sıralayıcıları (task schedulers) veya mesaj kuyrukları.
  • Oyunlarda animasyon komutlarının sıraya alınması.
  • Ağ isteklerini veya işlenecek olayları düzenleme.

Örnek Kullanım:

using System;
using System.Collections.Generic;

public class QueueOrnegi
{
    public static void Main(string[] args)
    {
        Queue<string> gorevKuyrugu = new Queue<string>();

        gorevKuyrugu.Enqueue("Animasyon Oynat");
        gorevKuyrugu.Enqueue("Ses Efekti Çal");
        gorevKuyrugu.Enqueue("Oyuncu Hareket Ettir");

        Console.WriteLine($"Kuyruktaki görev sayısı: {gorevKuyrugu.Count}");

        string siradakiGorev = gorevKuyrugu.Dequeue();
        Console.WriteLine($"İşlenen görev: {siradakiGorev}"); // Çıktı: Animasyon Oynat

        Console.WriteLine($"Yeni sıradaki görev (Peek): {gorevKuyrugu.Peek()}"); // Çıktı: Ses Efekti Çal

        while (gorevKuyrugu.Count > 0)
        {
            Console.WriteLine($"İşlenen görev: {gorevKuyrugu.Dequeue()}");
        }

        Console.WriteLine($"Kuyruk boş mu? {gorevKuyrugu.Count == 0}");
    }
}

4. Stack<T>: Son Giren İlk Çıkar (LIFO)

Stack<T>, ‘Son Giren İlk Çıkar’ (LIFO – Last-In, First-Out) prensibiyle çalışan bir koleksiyondur. Bir kitap yığınına benzer: en son eklenen kitap, yığından ilk alınan kitap olacaktır. Eleman ekleme (Push) ve çıkarma (Pop) işlemleri ortalama O(1) karmaşıklığına sahiptir.

Kullanım Alanları:

  • Geri alma/yineleme (undo/redo) sistemleri.
  • Yol bulma algoritmalarında (örneğin, derinlik öncelikli arama – DFS).
  • Fonksiyon çağrı yığınlarının simülasyonu.

Örnek Kullanım:

using System;
using System.Collections.Generic;

public class StackOrnegi
{
    public static void Main(string[] args)
    {
        Stack<string> gecmisHareketler = new Stack<string>();

        gecmisHareketler.Push("Oyuncu Yürüdü");
        gecmisHareketler.Push("Eşya Topladı");
        gecmisHareketler.Push("Kapı Açtı");

        Console.WriteLine($"Yığındaki hareket sayısı: {gecmisHareketler.Count}");

        string sonHareket = gecmisHareketler.Pop();
        Console.WriteLine($"Geri alınan hareket: {sonHareket}"); // Çıktı: Kapı Açtı

        Console.WriteLine($"Yeni en üstteki hareket (Peek): {gecmisHareketler.Peek()}"); // Çıktı: Eşya Topladı

        while (gecmisHareketler.Count > 0)
        {
            Console.WriteLine($"Geri alınan hareket: {gecmisHareketler.Pop()}");
        }

        Console.WriteLine($"Yığın boş mu? {gecmisHareketler.Count == 0}");
    }
}

Pratik İpuçları ve En İyi Uygulamalar

1. Dictionary İçin TryGetValue Kullanımı

Bir Dictionary‘den eleman alırken, anahtarın var olup olmadığını kontrol etmek için ContainsKey ve ardından indeksleyici ([]) kullanmak yerine TryGetValue metodunu tercih edin. Bu, hem anahtarın varlığını kontrol eder hem de değeri tek bir işlemde alır, böylece potansiyel bir KeyNotFoundException hatasından kaçınmış olursunuz ve performansı artırırsınız.

// Kötü:
if (myDict.ContainsKey("anahtar"))
{
    var deger = myDict["anahtar"];
}

// İyi:
if (myDict.TryGetValue("anahtar", out var deger))
{
    // Değeri kullan
}

2. Doğru Koleksiyonu Seçmek

İhtiyaçlarınıza en uygun C# Koleksiyonlar türünü seçmek kritik öneme sahiptir. Örneğin:

  • Sadece benzersiz elemanlar depolamak ve hızlı varlık kontrolü yapmak istiyorsanız: HashSet<T>.
  • Anahtar-değer çiftleriyle çalışıyor ve hızlı arama/erişim gerekiyorsa: Dictionary<TKey, TValue>.
  • Görevleri sırayla işlemek istiyorsanız (FIFO): Queue<T>.
  • Geri alma sistemleri veya son ekleneni ilk işleme ihtiyacınız varsa (LIFO): Stack<T>.

3. Başlangıç Kapasitesini Belirleme

Büyük koleksiyonlarla çalışırken, başlangıç kapasitesini (constructor’da) belirtmek performansı artırabilir. Bu, koleksiyonun eleman ekledikçe yeniden boyutlandırma (reallocation) ihtiyacını azaltır ve gereksiz bellek kopyalamalarının önüne geçer.

// Yaklaşık 1000 eleman depolayacak bir Dictionary oluşturma
Dictionary<string, GameObject> gameObjects = new Dictionary<string, GameObject>(1000);

Yaygın Hatalar ve Çözümleri

1. Dictionary’ye Mevcut Anahtarı Eklemeye Çalışmak

Dictionary‘ye zaten var olan bir anahtarı Add metodu ile eklemeye çalışmak ArgumentException hatasına neden olur. Çözüm, eklemeden önce ContainsKey ile kontrol etmek veya anahtara doğrudan indeksleyici ([]) ile atama yapmaktır (bu, anahtar yoksa ekler, varsa günceller).

Dictionary<string, int> skorlar = new Dictionary<string, int>();
skorlar.Add("Oyuncu1", 100);

// Hata verecek: skorlar.Add("Oyuncu1", 200);

// Çözüm 1: Kontrol et
if (!skorlar.ContainsKey("Oyuncu1"))
{
    skorlar.Add("Oyuncu1", 200);
}

// Çözüm 2: İndeksleyici kullan (günceller veya ekler)
skorlar["Oyuncu1"] = 200;

2. Boş Queue veya Stack’ten Çıkarmaya Çalışmak

Boş bir Queue‘dan Dequeue veya boş bir Stack‘ten Pop yapmaya çalışmak InvalidOperationException hatasına yol açar. Her zaman işlem yapmadan önce Count özelliğini kontrol edin veya TryDequeue/TryPop (C# 7.2 ve sonrası) metotlarını kullanın.

Queue<string> gorevler = new Queue<string>();
// Hata verecek: gorevler.Dequeue();

// Çözüm 1: Count kontrolü
if (gorevler.Count > 0)
{
    string gorev = gorevler.Dequeue();
}

// Çözüm 2 (C# 7.2+): TryDequeue/TryPop
if (gorevler.TryDequeue(out string gorev))
{
    // Görevi kullan
}

Performans ve Optimizasyon Notları

  • Hashing Kalitesi: Dictionary ve HashSet, elemanları depolamak ve aramak için nesnelerin GetHashCode() metodunu kullanır. Kendi özel tiplerinizi anahtar olarak kullanıyorsanız, iyi bir GetHashCode() ve Equals() implementasyonu, koleksiyonların performansını doğrudan etkiler. Kötü bir hash fonksiyonu, hash çarpışmalarına yol açarak performansı O(n)‘e kadar düşürebilir.
  • Bellek Ayırmaları: Koleksiyonlar büyüdükçe, bellek ayırmaları (reallocations) meydana gelir. Bu, özellikle oyunlarda kare hızını etkileyebilecek kısa süreli takılmalara (hiccup) neden olabilir. Mümkün olduğunca başlangıç kapasitesini tahmin ederek veya TrimExcess() gibi metotlarla kullanılmayan belleği serbest bırakarak bellek ayırmalarını optimize edin.
  • Generic Koleksiyonlar: Daima generic koleksiyonları (List<T>, Dictionary<TKey, TValue> vb.) non-generic (ArrayList, Hashtable) olanlara tercih edin. Generic koleksiyonlar tip güvenliği sağlar ve değer tipleri için boxing/unboxing ihtiyacını ortadan kaldırarak performans artışı sağlar.

Sonuç

C# Koleksiyonlar, modern uygulamalar ve özellikle Unity ile oyun geliştirme için vazgeçilmez araçlardır. Dictionary, HashSet, Queue ve Stack, farklı ihtiyaçlara yönelik güçlü ve verimli çözümler sunar. Bu koleksiyonların ne zaman ve nasıl kullanılacağını anlamak, daha sağlam, hızlı ve bakımı kolay kod yazmanıza yardımcı olacaktır. Doğru koleksiyonu seçmek, pratik ipuçlarını uygulamak ve yaygın hatalardan kaçınmak, geliştirme sürecinizi çok daha verimli hale getirecektir.

Leave a Reply

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir