Skip to content

Bölüm 7 / 15

Koleksiyonlar

List, Dictionary, HashSet, Queue, Stack.

8 dakikalık okuma

Dizinin sabit boyutu gerçek programlarda hızla dar gelir: kullanıcı kaç kayıt ekleyeceğini önceden söylemez. Bu ders .NET'in beş temel koleksiyonunu kurar — List, Dictionary, HashSet, Queue, Stack — ve her birinden daha önemlisini: hangi işe hangisinin seçileceğini ve neden. Üretim kodunda yanlış koleksiyon seçimi çalışan ama yavaş program üretir; fark ancak veri büyüyünce, yani en kötü anda görünür.

01Ne öğreneceksin

  • List<T>: dinamik dizi; iç mekanizması (Capacity, ikiye katlama) ve gezinirken silme tuzağı
  • Dictionary<TKey,TValue>: anahtarla sabit süreli erişim; TryGetValue deseni
  • HashSet, Queue, Stack: teklik, sıra bekleyen işler, geri alma
  • Seçim rehberi: senaryodan koleksiyona gitmek; Contains'in List'te ve HashSet'te neden farklı hızda olduğu

02Generic sözdizimi: List<int> nasıl okunur

Koleksiyonlar System.Collections.Generic ad alanında yaşar ve adlarında açılı parantez taşır: List<int> "int listesi", Dictionary<string, int> "string anahtarlı, int değerli sözlük" diye okunur. Açılı parantezin içindeki tip, koleksiyonun neyi tutacağını derleme anında sabitler — List<int>'e string eklemek derleme hatasıdır. Tip güvenliği (2. ders) koleksiyonlarda da tam çalışır. Bu mekanizmanın adı generics'tir; kendi generic tipini yazmayı Hat 03'te öğreneceksin (cs-generics), bugün kullanıcı tarafı yeterli.

03List<T>: büyüyebilen dizi

Günlük işin varsayılan koleksiyonu. Dizi gibi sıralı ve indekslidir; farkı boyutun serbest olmasıdır:

var adlar = new List<string>();          // boş başlar
adlar.Add("Aysel");                       // sona ekle
adlar.Add("Deniz");
adlar.Insert(0, "Burak");                 // araya sok (0. konuma)
Console.WriteLine(adlar[1]);              // Aysel — indeksleme dizi gibi
Console.WriteLine(adlar.Count);           // 3 — dizideki Length'in karşılığı
adlar.Remove("Deniz");                    // değere göre sil (ilk eşleşen)
adlar.RemoveAt(0);                        // indekse göre sil
var varMi = adlar.Contains("Aysel");      // true

var puanlar = new List<int> { 85, 92, 78 };   // elemanlarıyla başlatma

İç mekanizmayı bil — mülakatta tam bu cümleyle sorulur: List, arka planda bir dizi tutar. Dizi dolunca List iki kat büyüklükte yeni dizi açar, elemanları taşır ve eskisini bırakır. Bundan üç pratik sonuç çıkar:

  • Sona ekleme çoğu zaman anlıktır; yalnızca büyüme anında taşıma maliyeti öder (amortize sabit süre).
  • Insert(0, ...) ve RemoveAt(0) pahalıdır: baştaki her işlem, kalan tüm elemanları kaydırır. Baştan ekleyip baştan çıkaran bir akış List'e değil Queue'ya işarettir.
  • Eleman sayısını kabaca biliyorsan new List<int>(10000) ile kapasiteyi baştan verirsin; ara taşımalar hiç yaşanmaz. Count eleman sayısı, Capacity arka dizinin boyudur — ikisini karıştırma.

Bir de meşhur tuzak: foreach ile gezerken listeden eleman silemezsin — çalışma zamanında InvalidOperationException alırsın (koleksiyon, üzerinde gezinilirken değiştirildi). Doğru yollar: şarta göre toplu silme için RemoveAll, ya da sondan başa doğru for döngüsüyle silmek (sondan silmek indeksleri kaydırmaz):

puanlar.RemoveAll(p => p < 50);      // 50 altını sil — ok işareti (lambda) Hat 03'te;
                                      // "p için: p < 50 ise" diye oku
for (var i = puanlar.Count - 1; i >= 0; i--)
{
    if (puanlar[i] < 50) { puanlar.RemoveAt(i); }   // elle güvenli sürüm
}

04Dictionary<TKey,TValue>: anahtarla erişim

Listede "Aysel'in puanı kaç?" sorusu tüm listeyi taramak demektir. Veriye bir anahtarla erişeceksen doğru yapı sözlüktür — her kayıt anahtar-değer çiftidir ve anahtarla erişim, eleman sayısından bağımsız, sabit süreye yakındır (iç mekanizması hash tablosudur; Hat 02'nin veri yapıları dersinde açılacak):

var puanlar = new Dictionary<string, int>();
puanlar["aysel"] = 85;                    // ekleme de güncelleme de indexer ile
puanlar["deniz"] = 92;
puanlar.Add("burak", 78);                 // Add: anahtar zaten VARSA ArgumentException
puanlar["aysel"] = 90;                    // indexer: varsa üzerine yazar — fark bu

Console.WriteLine(puanlar["deniz"]);      // 92
Console.WriteLine(puanlar["ece"]);        // ÇALIŞMA ZAMANI HATASI: KeyNotFoundException
Console.WriteLine(puanlar.Count);         // 3
puanlar.Remove("burak");

Olmayan anahtar iki hatanın kaynağıdır; ikisinin de idiomatik çözümü vardır ve profesyonel kodu tanımanın işaretlerindendir:

// Acemi: iki kez arama (önce var mı, sonra oku)
if (puanlar.ContainsKey("ece"))
{
    Console.WriteLine(puanlar["ece"]);
}

// İdiomatik: tek arama — TryGetValue hem kontrol eder hem değeri verir
if (puanlar.TryGetValue("ece", out var puan))
{
    Console.WriteLine(puan);
}
else
{
    Console.WriteLine("Kayıt yok");
}

out var yazımını bir sonraki derste (metotlar, out parametresi) tam olarak anlayacaksın; bugün deseni tanı: Try...(anahtar, out değer) bool döner, başarılıysa değeri doldurur. Aynı desen int.TryParse'ta da var ve 2. dersteki "kullanıcı abc yazarsa Parse patlar" sorununun çözümü. Sözlük üzerinde foreach her kaydı KeyValuePair olarak verir:

foreach (var kayit in puanlar)
{
    Console.WriteLine($"{kayit.Key}: {kayit.Value}");
}

05HashSet<T>: teklik ve hızlı üyelik

HashSet, her elemanı en fazla bir kez tutan kümedir; sırası ve indeksi yoktur. İki işte rakipsizdir: tekrarları ayıklamak ve "bu eleman var mı?" sorusuna çok hızlı cevap vermek (Dictionary ile aynı hash mekanizması — sabit süreye yakın):

var gorulen = new HashSet<string>();
Console.WriteLine(gorulen.Add("aysel"));   // True — eklendi
Console.WriteLine(gorulen.Add("aysel"));   // False — zaten vardı; hata YOK, sessizce reddeder
Console.WriteLine(gorulen.Contains("aysel")); // True
Console.WriteLine(gorulen.Count);          // 1

Add'in bool dönmesi kalıp doğurur: "daha önce işledim mi?" kontrolü ile ekleme tek satırda birleşir — if (!gorulen.Add(id)) continue;. Performans farkını sayıyla bil: List.Contains baştan sona tarama yapar (eleman sayısıyla doğru orantılı, O(n)); HashSet.Contains hash ile doğrudan gider (O(1)'e yakın). Bin elemanlı listede bin kez üyelik kontrolü milyon işlemdir; HashSet'te bin. "Çalışıyor ama yavaş" kodların klasik sebebi, HashSet'lik işte List.Contains kullanmaktır.

06Queue<T> ve Stack<T>: sıra ve yığın

İki yapı da "eleman koy, eleman al" düzenidir; farkları alma sırasıdır:

var kuyruk = new Queue<string>();     // FIFO: ilk giren ilk çıkar
kuyruk.Enqueue("is-1");
kuyruk.Enqueue("is-2");
Console.WriteLine(kuyruk.Dequeue());   // is-1 — en öndeki çıkar
Console.WriteLine(kuyruk.Peek());      // is-2 — bak ama çıkarma

var yigin = new Stack<string>();       // LIFO: son giren ilk çıkar
yigin.Push("adim-1");
yigin.Push("adim-2");
Console.WriteLine(yigin.Pop());        // adim-2 — en üsttekini çıkar

Gerçek karşılıkları ezber değil sezgi olsun: kuyruk, sırayla işlenecek işlerdir — yazıcı kuyruğu, mesaj kuyruğu (Hat 19'da RabbitMQ tam bu kavramın sunucu halidir). Yığın, "en son yaptığımı ilk geri alırım" düzenidir — editördeki Ctrl+Z geçmişi, tarayıcının geri tuşu, ve çalışan programındaki çağrı yığını (call stack — metotlar dersinde göreceksin; hata mesajlarındaki "stack trace" adı buradan gelir). Boş kuyruktan Dequeue / boş yığından Pop çalışma zamanı hatasıdır; önce Count ya da TryDequeue/TryPop ile kontrol edilir — yine Try deseni.

07Seçim rehberi

Mülakatta ve kod incelemesinde asıl ölçülen bilgi bu tablodur — koleksiyonu senaryodan seçebilmek:

İhtiyaçDoğru yapıGerekçe
Sıralı kayıtlar, sondan ekleme, indeksle erişimList<T>Dinamik dizi; varsayılan seçim
Boyut baştan belli ve sabitdiziEn yalın ve en az ek yük
Anahtarla bulma (kimlikten kayda)Dictionary<K,V>Hash ile sabit süreli erişim
Teklik / hızlı "var mı?" kontrolüHashSet<T>O(1)'e yakın üyelik; tekrarı sessizce reddeder
Geliş sırasıyla işlemeQueue<T>FIFO; baştan almak List'te pahalı, burada doğal
Son yapılanı ilk geri almaStack<T>LIFO; geri al / çağrı yığını düzeni

İki ek not: sözlük ve kümenin sıra garantisi yoktur — foreach'in verdiği sıraya asla dayanma, sıralı çıktı gerekiyorsa sıralamayı açıkça yap. Ve bu beş yapının hepsi referans tipidir: geçen dersin kuralı aynen geçerli — atama kopyalamaz, iki isim aynı koleksiyonu gösterir; bağımsız kopya için new List<int>(digeri) gibi kurucular kullanılır.

08Mini alıştırma — kelime sayacı

Yaklaşık 20 dakika · dotnet new console -o koleksiyonlar · mülakat klasiği

  1. Şu metinle başla: var metin = "elma armut elma kiraz armut elma muz"; Split(' ') ile kelimelere ayır. Bir Dictionary<string, int> ile her kelimenin kaç kez geçtiğini say (ipucu: TryGetValue ile mevcut sayıyı al, yoksa 0 kabul et; ya da "acemi" ContainsKey sürümünü yazıp idiomatik sürümle karşılaştır). Sonucu "kelime: adet" satırlarıyla yazdır — elma 3, armut 2, kiraz 1, muz 1.
  2. Aynı metinden benzersiz kelimeleri HashSet ile çıkar; Add'in dönüş değerini kullanarak "tekrar" olanları ayrıca listele.
  3. Stack pratiği: kullanıcıdan aldığın bir kelimeyi foreach (var harf in kelime) ile karakter karakter yığına Push'la, sonra boşalana kadar Pop'layıp yazdır — kelimenin tersini üretmiş oldun. Neden Queue ile aynı sonucu alamayacağını bir cümleyle açıkla.
  4. Tuzak deneyi: bir List<int> aç, foreach içinde bir elemanı Remove etmeyi dene; InvalidOperationException'ı gör. Aynı işi önce sondan başa for ile, sonra RemoveAll ile yap.
  5. Ölçek sezgisi: 1'den 100.000'e kadar sayıları bir List'e ve bir HashSet'e koy; 50000 kez Contains(99999) çağıran iki döngü yaz ve kabaca süre farkını hisset (kesin ölçüm şart değil; farkın gözle görülür olması yeter).

09Kendini sına

List arka planda nasıl çalışır? Insert(0, x) neden pahalıdır?

List arka planda bir dizi tutar; dolunca iki kat büyüklükte yeni dizi açıp elemanları taşır (bu yüzden sona ekleme amortize sabit süredir ve beklenen boyut biliniyorsa kapasiteyi baştan vermek taşımaları önler). Insert(0, x) pahalıdır çünkü baştaki her ekleme, mevcut tüm elemanları bir sağa kaydırır; baştan ekleyip baştan alan akış için doğru yapı Queue'dur.

Dictionary'de olmayan anahtarı indexer ile okumak ne yapar? İdiomatik çözüm nedir?

Çalışma zamanında KeyNotFoundException fırlatır. İdiomatik çözüm TryGetValue(anahtar, out var deger)'dur: tek aramada hem varlığı kontrol eder hem değeri doldurur, bool döner. ContainsKey + indexer ikilisi aynı işi iki aramayla yapar; çalışır ama gereksiz maliyettir. Yazarken de Add ile indexer farklıdır: Add mevcut anahtarda hata fırlatır, indexer üzerine yazar.

List.Contains ile HashSet.Contains arasındaki performans farkı nedir ve nereden gelir?

List.Contains listeyi baştan sona tarar — süre eleman sayısıyla doğru orantılıdır (O(n)). HashSet.Contains, elemanın hash değeriyle doğrudan ilgili göze gider — eleman sayısından bağımsız, sabit süreye yakındır (O(1)). Sık üyelik kontrolü yapılan yerde List kullanmak, çalışan ama veri büyüyünce yavaşlayan kodun klasik sebebidir.

Queue ile Stack'in çıkarma sırası nedir? Her birine gerçek bir kullanım örneği ver.

Queue FIFO'dur — ilk giren ilk çıkar: sırayla işlenecek işler, yazıcı/mesaj kuyruğu. Stack LIFO'dur — son giren ilk çıkar: editörün geri al (Ctrl+Z) geçmişi, tarayıcı geri tuşu, metot çağrı yığını. Boş yapıdan Dequeue/Pop çalışma zamanı hatasıdır; TryDequeue/TryPop ya da Count kontrolü ile korunulur.

10Sınav köşesi

Yazılıda nasıl sorulur

İki kalıp öne çıkar: (1) Eşleştirme — "İlk giren ilk çıkar yapısı hangisidir?" (Queue), "Tekrarsız eleman tutan koleksiyon?" (HashSet), "Anahtar-değer çifti?" (Dictionary). FIFO/LIFO açılımlarını İngilizceleriyle bil. (2) Davranış — "Var olan anahtarla Add çağrılırsa ne olur?" (exception; indexer olsaydı üzerine yazardı), "foreach sırasında Remove?" (InvalidOperationException). Temel bilişim tarafında aynı kavramlar veri yapısı adıyla gelir: kuyruk, yığın, hash tablosu — bu ders o soruların C# yüzüdür, Hat 02 teorisini tamamlar.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

"Hangi koleksiyonu ne zaman kullanırsınız?" mülakatın en verimli sorusudur; cevabın senaryo → yapı → gerekçe üçlüsüyle aksın: (1) Varsayılanım List — dinamik dizi; arka planda dizi tuttuğunu, dolunca ikiye katlandığını söyle. (2) Anahtarla erişim varsa Dictionary, teklik ya da yoğun üyelik kontrolü varsa HashSet — ikisinin de hash ile O(1)'e yakın çalıştığını belirt. (3) Geliş sırasıyla işleme Queue, geri alma düzeni Stack. (4) Kapanış cümlesi: "List.Contains'in O(n) olduğunu bilirim; sık aranan veriyi HashSet'e ya da Dictionary'ye taşırım" — performans bilinci tam bu cümlede ölçülür.