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;TryGetValuedeseniHashSet,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, ...)veRemoveAt(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.Counteleman sayısı,Capacityarka 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şim | List<T> | Dinamik dizi; varsayılan seçim |
| Boyut baştan belli ve sabit | dizi | En 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şleme | Queue<T> | FIFO; baştan almak List'te pahalı, burada doğal |
| Son yapılanı ilk geri alma | Stack<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
- Şu metinle başla:
var metin = "elma armut elma kiraz armut elma muz";Split(' ')ile kelimelere ayır. BirDictionary<string, int>ile her kelimenin kaç kez geçtiğini say (ipucu:TryGetValueile 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. - Aynı metinden benzersiz kelimeleri HashSet ile çıkar;
Add'in dönüş değerini kullanarak "tekrar" olanları ayrıca listele. - 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. - Tuzak deneyi: bir
List<int>aç, foreach içinde bir elemanıRemoveetmeyi dene;InvalidOperationException'ı gör. Aynı işi önce sondan başa for ile, sonraRemoveAllile yap. - Ö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.
