Mert Özen Her Satırda Daha İleri
Backend LINQ — #03

LINQ ile Filtreleme ve Sıralama: Where, OrderBy, ThenBy ve Reverse

Mert Özen 7 Kas 2026 10 dk 1 görüntülenme
LINQ ile Filtreleme ve Sıralama: Where, OrderBy, ThenBy ve Reverse

Where ve index overload'u, ikinci OrderBy'ın ilkini neden sildiği, ThenBy ile çok key'li sıralama, OrderBy'ın stable, List.Sort'un unstable oluşu, kültüre bağlı string sıralaması ve Reverse ile OrderByDescending farkı.

Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.

Önce Tahmin Et

Kitapları önce kategoriye, kategori içinde de fiyata göre sıralamak istiyoruz. Aşağıda bunun iki denemesi var. Hangisi istediğimiz sırayı verir? Diğeri ne yapar? Fiyatların her makinede aynı biçimde yazılması için kültürü baştan sabitliyoruz.

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

var books = new List<Book>
{
    new("Clean Code", "Craft", 37.90m),
    new("Domain-Driven Design", "Architecture", 54.99m),
    new("The Pragmatic Programmer", "Craft", 42.50m),
    new("C# in Depth", "Language", 44.99m),
    new("Design Patterns", "Architecture", 49.95m),
    new("Refactoring", "Craft", 42.50m),
    new("CLR via C#", "Language", 59.99m),
};

var first = books.OrderBy(b => b.Category).OrderBy(b => b.Price);
var second = books.OrderBy(b => b.Category).ThenBy(b => b.Price);

Console.WriteLine("OrderBy + OrderBy:");
foreach (Book b in first)
    Console.WriteLine($"  {b.Category,-12} {b.Price,6}  {b.Title}");

Console.WriteLine("OrderBy + ThenBy:");
foreach (Book b in second)
    Console.WriteLine($"  {b.Category,-12} {b.Price,6}  {b.Title}");

record Book(string Title, string Category, decimal Price);

İkinci OrderBy Birincisini Siler

Çıktı şu:

OrderBy + OrderBy:
  Craft         37.90  Clean Code
  Craft         42.50  The Pragmatic Programmer
  Craft         42.50  Refactoring
  Language      44.99  C# in Depth
  Architecture  49.95  Design Patterns
  Architecture  54.99  Domain-Driven Design
  Language      59.99  CLR via C#
OrderBy + ThenBy:
  Architecture  49.95  Design Patterns
  Architecture  54.99  Domain-Driven Design
  Craft         37.90  Clean Code
  Craft         42.50  The Pragmatic Programmer
  Craft         42.50  Refactoring
  Language      44.99  C# in Depth
  Language      59.99  CLR via C#

İlk sorgu kategoriyi tamamen unutmuş, kitaplar yalnızca fiyata göre dizilmiş. OrderBy her çağrıldığında sıfırdan yeni bir birincil sıralama kurar ve önceki sıralamayı yok sayar. Bu davranış belgelenmiştir; derleyici de uyarmaz, çünkü OrderBy'ın sonucu yine bir sequence ve onu tekrar sıralamak geçerli bir işlem.

İkinci key için ThenBy kullanılır. OrderBy düz bir IEnumerable<T> değil, IOrderedEnumerable<T> döndürür. Bu tip "ben zaten sıralıyım, eşit kalanları neye göre ayırayım?" sorusunu taşır ve ThenBy ile ThenByDescending yalnızca bu tip üzerinde tanımlıdır. Telefon rehberini düşün: önce soyadına göre dizilir, aynı soyadını taşıyanlar arasında ada bakılır. Ada bakmak, soyadı sırasını bozmaz; sadece beraberlikleri çözer. İkinci sorguda Architecture, Craft ve Language blokları korunuyor ve her bloğun içi fiyata göre dizili.

Where: Koşulu Sağlayanları Geçir

Where, bir predicate alır ve true dönen element'leri geçirir. Önceki yazılarda en yaygın overload'unu kullandık. Bir de element'in kaynaktaki index'ini veren ikinci bir overload var; bir CSV dosyasının başlık satırını atlamak gibi işlerde işe yarar:

string[] lines =
{
    "Isbn;Title;Stock",
    "978-0132350884;Clean Code;12",
    "",
    "978-0135957059;The Pragmatic Programmer;0",
    "978-0321125217;Domain-Driven Design;3",
};

// İkinci parametre element'in kaynaktaki index'i: başlık satırını atlıyoruz
IEnumerable<string> dataLines = lines.Where((line, index) => index > 0 && line.Length > 0);

IEnumerable<string> inStock = dataLines.Where(line => int.Parse(line.Split(';')[2]) > 0);

Console.WriteLine($"data lines: {dataLines.Count()}");
foreach (string line in inStock)
    Console.WriteLine(line);
data lines: 3
978-0132350884;Clean Code;12
978-0321125217;Domain-Driven Design;3

Index, filtrelemeden önceki konumdur: boş satır elendiği hâlde sonraki satırların index'i kaymaz. Bu overload'un query syntax karşılığı yok, sadece method syntax ile yazılır.

İkinci Where'i ayrı yazdık, ama koşulları && ile tek bir Where'de birleştirmek de aynı sonucu verir. Hangisini seçeceğin okunabilirlik meselesi: birbirinden bağımsız iş kuralları ayrı Where'lerde daha rahat okunur ve tek tek açılıp kapatılabilir. .NET ardışık Where çağrılarını içeride tek bir iterator'de birleştirdiği için aradaki fark ölçmeye değmez.

Sıralamayla birlikte kullanırken sıra önemli. Önce filtrele, sonra sırala: OrderBy önceki yazıda gördüğümüz gibi bütün kaynağı belleğe alıp sıralar ve ona ne kadar az element gelirse o kadar az iş yapar. books.OrderBy(...).Where(...) doğru sonucu verir ama elenecek kitapları da boşuna sıralar.

Azalan Sıra ve Çok Key'li Sıralama

Her key'in kendi yönü olabilir. Kategoriye göre artan, kategori içinde fiyata göre azalan, fiyatı aynı olanlar arasında da başlığa göre artan bir sıra kuralım:

var methodSyntax = books
    .OrderBy(b => b.Category)
    .ThenByDescending(b => b.Price)
    .ThenBy(b => b.Title);

var querySyntax =
    from b in books
    orderby b.Category, b.Price descending, b.Title
    select b;

foreach (Book b in methodSyntax)
    Console.WriteLine($"{b.Category,-12} {b.Price,6}  {b.Title}");

Console.WriteLine(methodSyntax.SequenceEqual(querySyntax));
Architecture  54.99  Domain-Driven Design
Architecture  49.95  Design Patterns
Craft         42.50  Refactoring
Craft         42.50  The Pragmatic Programmer
Craft         37.90  Clean Code
Language      59.99  CLR via C#
Language      44.99  C# in Depth
True

Query syntax'ta key'ler orderby'dan sonra virgülle sıralanır ve her biri kendi ascending ya da descending işaretini taşır; işaret yoksa artan kabul edilir. Derleyici ilk key'i OrderBy'a, sonrakileri ThenBy/ThenByDescending'e çevirir, SequenceEqual'ın True demesi bundan. Craft kategorisinde fiyatı 42.50 olan iki kitap üçüncü key sayesinde alfabetik diziliyor: Refactoring, The Pragmatic Programmer'dan önce geliyor.

Element'in kendisine göre sıralamak istersen key selector olarak x => x verirsin. .NET 7 ile bunun için selector almayan Order() ve OrderDescending() geldi; LINQ serisinin sürüm yazılarında göreceğiz.

Stability ve Maliyet

Üçüncü key'i yazmasaydık Craft'taki iki kitap hangi sırada çıkardı? Cevap belgede açıkça yazılı: OrderBy stable (kararlı) sıralama yapar, yani key'leri eşit olan element'lerin kaynaktaki sırası korunur. List<T>.Sort için belge tam tersini söyler: unstable (kararsız) sıralama yapar ve eşit element'lerin sırasının korunacağının garantisi yoktur. Farkı görmek için 40 siparişi durumlarına göre sıralayalım:

var orders = Enumerable.Range(1, 40)
    .Select(id => new Order(id, id % 3 == 0 ? "Shipped" : "Pending"))
    .ToList();

// OrderBy: eşit key'lerde kaynak sırası korunur
List<Order> linqSorted = orders.OrderBy(o => o.Status).ToList();

// List<T>.Sort: yerinde ve unstable sıralama
var listSorted = new List<Order>(orders);
listSorted.Sort((x, y) => string.CompareOrdinal(x.Status, y.Status));

Console.WriteLine("OrderBy:   " + string.Join(" ", linqSorted.Take(8).Select(o => o.Id)));
Console.WriteLine("List.Sort: " + string.Join(" ", listSorted.Take(8).Select(o => o.Id)));

record Order(int Id, string Status);
OrderBy:   1 2 4 5 7 8 10 11
List.Sort: 1 38 37 35 34 32 31 29

İki yöntem de "Pending" siparişleri öne aldı. Ama OrderBy onları kaynaktaki sırayla, 1, 2, 4, 5 diye verdi; List<T>.Sort ise karıştırdı. Küçük listelerde Sort çoğu zaman sırayı koruyormuş gibi görünür ve bu yüzden hata testte yakalanmaz. Ekranda "en yeni sipariş en üstte" gibi bir varsayımın varsa ve onu ikinci bir key ile açıkça yazmadıysan, sıralama yöntemini değiştirdiğin gün sıra da değişebilir. En güvenlisi, beraberliğin nasıl çözüleceğini her zaman bir ThenBy ile söylemek.

Maliyet tarafında bilmen gereken bir ayrıntı daha var. Sıralama algoritması element'leri defalarca karşılaştırır; key selector her karşılaştırmada çağrılsaydı pahalı bir key ciddi bir yük olurdu:

int keyCalls = 0;

decimal PriceWithTax(Book b)
{
    keyCalls++;
    return b.Price * 1.2m;
}

List<Book> sorted = books.OrderBy(PriceWithTax).ToList();
Console.WriteLine($"{books.Count} books, {keyCalls} key calls");
7 books, 7 key calls

Yedi kitap için key yedi kez hesaplandı. OrderBy önce her element'in key'ini bir kez hesaplayıp saklar, sonra karşılaştırmaları bu saklanmış key'ler üzerinden yapar. Yine de key hesabı bir veritabanı ya da servis çağrısıysa, onu sorgunun dışına taşımak daha doğru olur.

String Sıralaması ve Kültür

String key'lerle sıralarken OrderBy, Comparer<string>.Default'u kullanır ve bu comparer o anki kültüre bakar. Türkçe isimlerde bunun sonucu çarpıcı:

using System.Globalization;

string[] authors = { "Zorlu", "Çelik", "Cengiz", "Ilgaz", "İnce", "Sarı", "Şahin" };

CultureInfo.CurrentCulture = new CultureInfo("tr-TR");
Console.WriteLine("tr-TR:     " + string.Join(", ", authors.OrderBy(a => a)));

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine("Invariant: " + string.Join(", ", authors.OrderBy(a => a)));

Console.WriteLine("Ordinal:   " + string.Join(", ", authors.OrderBy(a => a, StringComparer.Ordinal)));

// Kültürü sorgunun kendisine yazmak: ortamdan bağımsız
StringComparer turkish = StringComparer.Create(new CultureInfo("tr-TR"), ignoreCase: false);
Console.WriteLine("Explicit:  " + string.Join(", ", authors.OrderBy(a => a, turkish)));
tr-TR:     Cengiz, Çelik, Ilgaz, İnce, Sarı, Şahin, Zorlu
Invariant: Çelik, Cengiz, Ilgaz, İnce, Şahin, Sarı, Zorlu
Ordinal:   Cengiz, Ilgaz, Sarı, Zorlu, Çelik, İnce, Şahin
Explicit:  Cengiz, Çelik, Ilgaz, İnce, Sarı, Şahin, Zorlu

Aynı array, aynı OrderBy(a => a), üç farklı sonuç. Türk alfabesinde Ç, C'den sonra gelen ayrı bir harf, o yüzden tr-TR'de Cengiz önce geliyor. Invariant kültür Ç'yi C'nin aksanlı bir hâli sayıyor ve önce sonraki harflere bakıyor: "Çel" ile "Cen" karşılaştırmasında l, n'den önce geldiği için Çelik öne geçiyor. Ordinal karşılaştırma ise karakter kodlarına bakıyor ve Türkçe harfleri Z'den sonraya atıyor. Bu çıktılar macOS'ta alındı. .NET 5'ten beri Windows'ta da (Windows 10 1903 ve sonrası) kültüre bağlı karşılaştırmalar ICU ile yapıldığı için sonuç platformlar arasında genelde tutarlıdır.

Asıl tehlike ilk iki satırda. Kodda hiçbir şey değişmeden, sadece sunucunun kültürü değiştiğinde sıralama değişiyor. Geliştirici makinen tr-TR, production'daki container invariant olabilir. Kullanıcıya gösterilen bir listeyi sıralıyorsan kültürü StringComparer.Create ile sorgunun kendisine yaz. Kod, ISBN ya da dosya adı gibi makineye yönelik değerleri sıralıyorsan StringComparer.Ordinal kullan; hem hızlıdır hem de her ortamda aynı sonucu verir. Koleksiyon serisinde comparer'ları ayrıntılı görmüştük, burada da aynı comparer'lar işini görüyor.

Reverse ve OrderByDescending Aynı Şey Değil

Ters sırayı almak için akla iki yol gelir: OrderByDescending ya da OrderBy ardından Reverse(). Beraberlik olmadığı sürece ikisi aynı görünür:

var byPriceDesc = books.OrderByDescending(b => b.Price);
var byPriceThenReversed = books.OrderBy(b => b.Price).Reverse();

Console.WriteLine(string.Join(" | ", byPriceDesc.Skip(4).Select(b => $"{b.Title} {b.Price}")));
Console.WriteLine(string.Join(" | ", byPriceThenReversed.Skip(4).Select(b => $"{b.Title} {b.Price}")));
The Pragmatic Programmer 42.50 | Refactoring 42.50 | Clean Code 37.90
Refactoring 42.50 | The Pragmatic Programmer 42.50 | Clean Code 37.90

Son üç kitaba baktık. OrderByDescending de stable'dır: fiyatı eşit olan The Pragmatic Programmer ile Refactoring, kaynaktaki sırasıyla geliyor. Reverse() ise sıralanmış sonucu olduğu gibi ters çevirdiği için beraberlikleri de ters çeviriyor. Ayrıca Reverse non-streaming bir operatör; son element'i verebilmek için sequence'ın tamamını okur. Azalan sıra istiyorsan OrderByDescending yaz.

Reverse'in bir tuzağı daha var. List<T>'nin kendi Reverse() method'u vardır ve derleyici instance method'u extension method'a tercih eder:

var titles = new List<string> { "Clean Code", "Refactoring", "Domain-Driven Design" };

var reversed = titles.Reverse();
Console.WriteLine(string.Join(", ", reversed));
Program.cs(3,5): error CS0815: Cannot assign void to an implicitly-typed variable

List<T>.Reverse() listeyi yerinde ters çevirir ve void döner; derleyici void'i bir değişkene atayamadığını söylüyor. LINQ'in Reverse'ini istiyorsan listeyi önce IEnumerable<T> olarak görmelisin:

var titles = new List<string> { "Clean Code", "Refactoring", "Domain-Driven Design" };

IEnumerable<string> reversed = titles.AsEnumerable().Reverse();
Console.WriteLine(string.Join(", ", reversed));
Console.WriteLine(string.Join(", ", titles));

titles.Reverse();
Console.WriteLine(string.Join(", ", titles));
Domain-Driven Design, Refactoring, Clean Code
Clean Code, Refactoring, Domain-Driven Design
Domain-Driven Design, Refactoring, Clean Code

AsEnumerable() sonrası çağrı LINQ'e gitti ve liste değişmedi. Son satırdaki titles.Reverse() ise listenin kendisini ters çevirdi. Değişkeni açık tiple, IEnumerable<string> olarak tanımlasaydın da derleyici bu kez CS0029 ile, void'i IEnumerable<string>'e çeviremediğini söyleyerek durdururdu. Asıl sessiz tehlike dönüş değerini hiç kullanmadığın satır: titles.Reverse(); yazıp LINQ'i çağırdığını sanırsan, farkında olmadan listenin kendisini değiştirirsin.

Sık Düşülen Tuzaklar

İkinci key için tekrar OrderBy yazmak. Önceki sıralamayı siler; beraberlikleri çözmek için ThenBy ya da ThenByDescending kullan.

Beraberliklerin sırasına güvenmek. OrderBy stable'dır ama bu, sonucun kaynak sırasına bağlı olduğu anlamına gelir. Kaynak bir veritabanı sorgusuysa ve orada sıra belirtilmemişse, "kaynak sırası" da garanti değildir. Önemli olan her sırayı bir key ile yaz.

Sıralamayı kültüre bırakmak. String key'lerle OrderBy çalıştığı makinenin kültürüne göre farklı sonuç verebilir. Kullanıcıya dönük listelerde kültürü açıkça ver, teknik değerlerde StringComparer.Ordinal kullan.

Sıralamadan sonra filtrelemek. OrderBy(...).Where(...) doğru çalışır ama elenecek element'leri de sıralar. Where'i öne al.

List<T>.Reverse() ile LINQ'in Reverse()'ini karıştırmak. Biri listeyi yerinde değiştirir ve void döner, diğeri yeni bir sequence döndürür.

Küçük Bir Deneme

★☆☆ Tahmin sorusundaki kitapları kategoriye göre azalan, kategori içinde başlığa göre artan sırala. Önce method syntax, sonra query syntax ile yaz.

★★☆ 40 siparişlik örnekte List<T>.Sort'u stable hâle getir: karşılaştırma fonksiyonunda durumlar eşitse Id'ye bak. İlk sekiz Id artık OrderBy ile aynı mı?

★★★ Yazar isimleri array'ine "Ilgaz", "ılık" ve "İnce" ekle ve tr-TR, invariant ve ordinal sıralamaları yeniden karşılaştır. Sonra StringComparer.Create(new CultureInfo("tr-TR"), ignoreCase: true) ile büyük-küçük harf duyarsız sırala ve "I/ı/İ" harflerinin nereye düştüğünü açıkla.

Filtreledik ve sıraladık; ama hep elimizdeki Book object'lerini olduğu gibi döndürdük. Bir sonraki yazıda sonucu şekillendirmeye geçiyoruz: Select ile projection, iç içe collection'ları SelectMany ile düzleştirmek ve anonymous type'ların ne zaman işe yaradığı.