Skip to content

Bölüm 6 / 13

LINQ

Where, Select, OrderBy, GroupBy, Any/All, FirstOrDefault, ertelenmiş çalışma.

9 dakikalık okuma

Hat 02'nin 14. dersinde SQL'in matematiksel atası ilişkisel cebiri görmüştün — σ satır seçer, π sütun seçer. LINQ, aynı cebiri C# koleksiyonlarına getirir: Where bir σ'dır, Select bir π. Bu ders operatör ailesini, elemanı seçme metotlarının ince farklarını (First mü Single mı?) ve LINQ'nun en çok yanlış anlaşılan davranışını — ertelenmiş çalışmayı — işliyor. Döngü yazmayı azaltan bu araç, yanlış kullanınca Hat 02'nin 10. dersindeki gizli O(n²)'leri geri getirir; farkı bilen yazar.

01Ne öğreneceksin

  • Sorgu zinciri modeli: Where/Select/OrderBy ve ilişkisel cebir akrabalığı
  • Eleman seçiciler: First / FirstOrDefault / Single / SingleOrDefault karar tablosu
  • Gruplama ve dönüştürme: GroupBy, ToDictionary, SelectMany
  • Ertelenmiş çalışma: sorgu tarif midir sonuç mudur, çoklu numaralandırma tuzağı
  • Performans refleksleri: Any vs Count, döngüde LINQ, IQueryable'a bakış

02Model: koleksiyonu sorgulamak, döngüyü tarif etmek

LINQ (Language Integrated Query), IEnumerable<T> üzerinde çalışan bir sorgu metotları ailesidir. Fikri tek cümledir: "nasıl dolaşacağını" değil "ne istediğini" yaz. Şu klasik döngüyü:

var sonuc = new List<string>();
foreach (var m in musteriler)
    if (m.Sehir == "Ankara")
        sonuc.Add(m.Ad);
sonuc.Sort();

LINQ üç halkalı bir zincire çevirir:

var sonuc = musteriler
    .Where(m => m.Sehir == "Ankara")   // σ — satır süz
    .OrderBy(m => m.Ad)                // sırala
    .Select(m => m.Ad)                 // π — sütuna indirge
    .ToList();

Parantez içindeki m => m.Sehir == "Ankara" bir lambda: "m alır, şu ifadeyi döndürür" diyen isimsiz küçük fonksiyon. Mekanizmasını (delegate, Func) 7. ders sökecek; bu derste iş görecek kadarı yeter: sol taraf parametre, ok, sağ taraf dönen ifade. Zincirin her halkası yine bir IEnumerable döndürür — Hat 02'nin 14. dersindeki "sonuç yine tablodur, o yüzden zincirlenir" ilkesinin koleksiyon hâli. Çekirdek operatörler:

OperatörİşiCebir/SQL akrabası
Where(kosul)koşula uyan elemanları süzerσ — WHERE
Select(donusum)her elemanı dönüştürürπ — SELECT listesi
OrderBy / ThenBy / OrderByDescendingsıralar; ikincil anahtar ThenByORDER BY
Count / Sum / Min / Max / Averagetoplar, tek değere indirgertoplama fonksiyonları
Any(kosul) / All(kosul)en az biri / hepsi sağlıyor mu (bool)EXISTS
Distinct / Union / Intersect / Exceptküme işlemleri∪ ∩ − akrabaları
Take(n) / Skip(n)ilk n / n atla — sayfalama ikilisiLIMIT/OFFSET

Bir de sorgu sözdizimi vardır (from m in musteriler where ... select ...) — SQL'e benzeyen ikinci yazım. İkisi aynı koda derlenir; sahada metot sözdizimi baskındır, bu hat da onu kullanır; sorgu sözdizimini okuyabilecek kadar tanı.

03Eleman seçiciler: dört metot, dört farklı sözleşme

"Listeden bir eleman al" işleminin dört varyantı, 2. dersin ve 4. dersin sözleşme diliyle ayrışır — hangisini seçtiğin, "bulunamama"nın senin senaryonda olağan mı istisnai mi olduğunu bildirir:

MetotSonuç yoksaBirden çoksaNe zaman
Firstexceptionilkini verir"kesin var" — yoksa bu bir hatadır
FirstOrDefaultdefault (null/0)ilkini verirbulunamama olağan; dönüşte null kontrolü şart
Singleexceptionexception"tam olarak bir tane olmalı" — teklik de sözleşme
SingleOrDefaultdefaultexceptionsıfır ya da bir; ikincisi veri hatası
var m1 = musteriler.First(m => m.Id == id);             // yoksa InvalidOperationException
var m2 = musteriler.FirstOrDefault(m => m.Id == id);    // yoksa null → 2. dersin ?? / ?. dünyası
var m3 = musteriler.Single(m => m.TcNo == tc);          // TC benzersiz OLMALI — iki tane
                                                        // çıkarsa sessizce ilkini almaz, PATLAR

Kıdım farkı Single'ın değerindedir: benzersizlik bir iş kuralıysa First yazmak hatayı gizler, Single veri bozukluğunu ilk gören olur. Kural: sözleşmeni en sıkı ifade eden metodu seç — "hangisi patlamaz" değil, "hangisi doğru anda patlar".

04Gruplama ve düzleştirme: GroupBy, ToDictionary, SelectMany

// GroupBy: anahtara göre kümele — sonuç IGrouping listesi (Key + elemanlar)
var sehirGruplari = musteriler.GroupBy(m => m.Sehir);
foreach (var grup in sehirGruplari)
    Console.WriteLine($"{grup.Key}: {grup.Count()} müşteri");

// gruptan özet tabloya: şehir → toplam ciro
Dictionary<string, decimal> ciroTablosu = siparisler
    .GroupBy(s => s.Sehir)
    .ToDictionary(g => g.Key, g => g.Sum(s => s.Tutar));

// SelectMany: iç listeleri DÜZLEŞTİR — "her müşterinin tüm siparişleri" tek akış olsun
var tumSatirlar = musteriler.SelectMany(m => m.Siparisler);
// Select olsaydı: IEnumerable<List<Siparis>> — liste listesi; SelectMany: IEnumerable<Siparis>
  • GroupBy, SQL'deki GROUP BY'ın (Hat 07'de db-groupby) bellek içi hâlidir; her grup Key taşır ve kendisi numaralandırılabilir.
  • ToDictionary / ToList / ToArray / ToHashSet ailesi sorguyu maddeleştirir — bir sonraki bölümün anahtar kavramı.
  • Select ile SelectMany ayrımı mülakat sorusudur: eleman başına bir değer üretiyorsan Select, eleman başına bir koleksiyon üretip hepsini tek akışta istiyorsan SelectMany.

05Ertelenmiş çalışma: sorgu bir tariftir, yemek değil

LINQ'nun en kritik davranışı: Where/Select/OrderBy gibi operatörler çağrıldıkları anda hiçbir eleman işlemez. Döndürdükleri IEnumerable, bir tariftir; mutfak ancak biri sonucu numaralandırınca çalışır — foreach, ToList, Count(), First()... Buna ertelenmiş çalışma (deferred execution) denir ve iki yüzü vardır:

var buyukler = sayilar.Where(x => x > 10);   // şu an SIFIR iş yapıldı
sayilar.Add(99);                             // tarif hazırlanırken kaynak değişti
foreach (var x in buyukler) ...              // ŞİMDİ çalıştı — 99 sonuçta VAR:
                                             // tarif, çalıştığı andaki kaynağı görür

İyi yüzü verimliliktir: zincir, ara koleksiyonlar üretmeden eleman eleman akar; First bulduğu anda durur (kısa devre). Tehlikeli yüzü çoklu numaralandırmadır:

var pahali = siparisler.Where(s => YavasKontrol(s));   // tarif
if (pahali.Any())                        // 1. çalıştırma
    Console.WriteLine(pahali.Count());   // 2. çalıştırma — YavasKontrol HEPSİNE bir daha!

var liste = siparisler.Where(s => YavasKontrol(s)).ToList();   // BİR kez çalıştır, sonucu tut
if (liste.Count > 0) ...                 // artık bedava

Kural netleşsin: sonucu birden çok kez kullanacaksan ya da kaynağın değişmemesini istiyorsan ToList() ile maddeleştir; tek geçişte tüketeceksen tarif olarak bırak — erteleme senin lehine çalışır. Metot imzasında IEnumerable<T> dönen her değerin "henüz çalışmamış olabilir" taşıdığını bil; bu farkındalık, "neden iki kez sorgulandı?" ve "kaynağı değiştirince sonuç değişti" sürprizlerinin tamamını keser.

06Performans refleksleri: LINQ zarafeti maliyeti gizler

Hat 02'nin 10. dersi "LINQ'da da geçerli olacak" diye söz vermişti — işte senetler:

  • Any() varken Count() > 0 yazma: Count tüm akışı sayar (O(n)), Any ilk elemanda durur (O(1)). Aynı aile: First kısa devre yapar, OrderBy(...).First() ise tüm listeyi sıralar — "en küçüğü bul" için Min dururken O(n log n) ödeme.
  • Döngü içinde LINQ, gizli iç döngüdür: foreach gövdesindeki listeB.Contains(...) ya da Where(...).First(), 10. dersin O(n²) tuzağının LINQ kılığıdır. Çare aynı: aramayı döngü dışında bir HashSet'e ya da ToDictionary/ToLookup tablosuna çevir — O(n²) → O(n).
  • Zincir sırası maliyettir: önce süz, sonra sırala/dönüştür — Where(...).OrderBy(...), OrderBy(...).Where(...)'den ucuzdur; sıralayıcıya az eleman gitsin.
  • Ölçmeden suçlama, ölçmeden de güvenme: LINQ çoğu iş için yeterince hızlıdır; sıcak yolda (milyonluk veri, istek başına çağrı) Stopwatch'la döngüyle karşılaştır — bazen sade foreach kazanır, bu da meşru bir sonuçtur.

Bir kapı aralığı: bu derste gördüğün her şey LINQ to Objects — bellekteki koleksiyonlar, IEnumerable. EF Core (Hat 07) aynı sözdizimini IQueryable üzerinde kullanır: lambda'ların ifade ağacına çevrilip SQL olarak veritabanında çalışır. Aynı satırın nerede koştuğu — bellekte mi motorda mı — orada performansın ana sorusu olacak; şimdilik ayrımın var olduğunu bil.

07Sahada LINQ: okunabilirlik sözleşmesi

LINQ'nun asıl kazancı satır sayısı değil, niyetin görünmesidir: üç halkalı zincir, "Ankara'dakileri ada göre sırala, adları al" cümlesini kod incelemesinde cümle gibi okutur. Bunu korumak için saha görgü kuralları: zinciri halka başına bir satır kır; lambda üç-dört satırı geçiyorsa isimli metoda çıkar (zincir cümle kalsın); beş-altı halkayı aşan sorguyu ara değişkenlerle isimlendirerek böl ("aktifMusteriler", "sehirOzeti") — hem okunur hem ayıklanabilir. Ve LINQ'yu sorgulama için kullan: yan etkili işlem (loglama, kaydetme) zincire değil foreach gövdesine yakışır — tarif ne zaman çalışacağı belirsiz bir şeydir, yan etkiyi belirsizliğe bağlama.

08Mini alıştırma — Sipariş analizi + erteleme kanıtı

Yaklaşık 20 dakika · dotnet run · 15-20 satırlık örnek veri listesi kur

  1. Zincir turu: Siparis(Id, Musteri, Sehir, Tutar, Tarih) listesi kur. Sırayla yaz: 500 TL üstü siparişlerin müşteri adları (tutara göre azalan), şehir başına sipariş sayısı ve toplam ciro (GroupBy + ToDictionary), en yüksek tutarlı üç sipariş (OrderByDescending + Take).
  2. Seçici sözleşmesi: aynı Id ile First, FirstOrDefault, Single çağır; olmayan Id ve bilerek çiftlenmiş Id ile her birinin davranışını (değer mi, null mı, hangi exception) tablo hâlinde not et.
  3. Erteleme kanıtı: Where lambda'sının içine Console.Write(".") koy. Sorguyu kur (nokta yok — kanıt 1), listeye eleman ekle, foreach'le tüket (noktalar şimdi aktı ve yeni eleman sonuçta — kanıt 2), aynı sorguyu ikinci kez tüket (noktalar İKİNCİ kez — kanıt 3). Sonra ToList() ekleyip üç kanıtın da nasıl değiştiğini gör.
  4. O(n²) avı: 100.000 elemanlı iki Id listesinin kesişimini önce a.Where(x => b.Contains(x)) ile, sonra b'yi ToHashSet() yapıp ölç. Stopwatch farkını Hat 02'nin 10. dersindeki tabloyla ilişkilendir.

09Kendini sına

Where ve Select'in ilişkisel cebirdeki karşılıkları nedir? Zincirlenebilmelerini sağlayan ortak özellik hangisidir?

Where seçmedir (σ) — koşula uyan elemanları süzer, SQL'deki WHERE; Select izdüşümdür (π) — her elemanı istenen biçime indirger, SELECT listesi. Zincirlenebilmenin sırrı Hat 02'nin 14. dersindeki ilkeyle aynıdır: her operatörün sonucu yine IEnumerable'dır — "sonuç yine tablodur"un koleksiyon hâli; bu yüzden Where'in çıkışı OrderBy'ın girişi olabilir.

First, FirstOrDefault, Single, SingleOrDefault farklarını söyle; TC kimlik ile arama örneğinde hangisi doğrudur, neden?

First: yoksa exception, çoksa ilki. FirstOrDefault: yoksa default (referansta null), çoksa ilki. Single: yoksa da çoksa da exception — tam bir tane sözleşmesi. SingleOrDefault: yoksa default, çoksa exception. TC kimlik benzersiz bir iş kuralı olduğundan doğru seçim Single'dır: kayıt çiftlenmişse First sessizce ilkini alıp hatayı gizler, Single veri bozukluğunu ilk gören olur — metot seçimi, sözleşmenin koddaki ifadesidir.

Ertelenmiş çalışma nedir? Çoklu numaralandırma tuzağını ve ToList kuralını anlat.

Where/Select gibi operatörler çağrıldıkları anda iş yapmaz; tarif döndürürler ve tarif ancak numaralandırılınca (foreach, ToList, Count, First) çalışır — çalıştığı andaki kaynağı görür. Tuzak: aynı tarifi iki kez tüketmek (Any sonra Count gibi) tüm sorguyu iki kez çalıştırır; pahalı lambda'larda fatura ikiye katlanır. Kural: sonuç birden çok kez kullanılacaksa ya da kaynağın o anki görüntüsü sabitlenecekse ToList ile maddeleştir; tek geçişlik tüketimde tarif bırak — erteleme, kısa devre ve ara koleksiyonsuz akışla lehine çalışır.

Any() ile Count() > 0 arasında ve döngü içindeki Contains'te hangi performans farkları vardır?

Count tüm akışı numaralandırıp sayar — O(n); Any ilk elemanı görünce true döner — O(1), bu yüzden "var mı?" sorusunun doğru aracı Any'dir. Döngü içindeki liste.Contains ise gizli iç döngüdür: n elemanlı dış döngüde m elemanlı listede doğrusal arama O(n·m) eder — Hat 02'nin 10. dersindeki tuzağın LINQ kılığı. Çare aramayı sabit zamanlı yapıya taşımaktır: ToHashSet ya da ToDictionary/ToLookup ile O(n)'e iner.

10Sınav köşesi

Yazılıda nasıl sorulur

(1) Operatör eşleme: "koşula uyan elemanları süzen LINQ metodu?" (Where), "her elemanı dönüştüren?" (Select), "en az bir eleman koşulu sağlıyor mu?" (Any). (2) Çıktı tahmini — erteleme klasiği: sorgu kurulduktan sonra kaynağa eleman eklenir, foreach sonucu sorulur (yeni eleman dahildir; ToList'li varyantta değildir). (3) Seçici farkı: "listede koşulu sağlayan iki eleman varken Single ne yapar?" (InvalidOperationException). (4) SQL köprüsü: Where → WHERE, GroupBy → GROUP BY, Take/Skip → sayfalama eşlemesi; ilişkisel cebir sorusuyla (σ/π) birlikte gelebilir — Hat 02'nin 14. dersiyle çapraz çalış.

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

"LINQ nedir, nelere dikkat edersin?" için düzen: (1) Tanım + değer — "koleksiyon üzerinde bildirimsel sorgu: nasıl dolaşacağımı değil ne istediğimi yazarım; Where bir seçme, Select bir izdüşümdür — SQL ile aynı cebir" (teori bağı kurmak ayrıştırır). (2) Davranış derinliği — "operatörler tariftir, numaralandırılınca çalışır; bu yüzden çoklu numaralandırmadan kaçınır, çok kullanılacak sonucu ToList ile sabitlerim." (3) Performans refleksi — "Any varken Count kullanmam, döngü içindeki Contains'i HashSet'e çeviririm, önce süzer sonra sıralarım; sıcak yolda ölçerim." (4) Kapanış köprüsü: "Aynı sözdizimi EF Core'da IQueryable ile SQL'e çevrilir — bellekte mi veritabanında mı çalıştığını bilmek, oradaki ilk performans sorusudur."