LINQ Any, All, Contains, SequenceEqual ve Dönüşüm Operatörleri
All'ın boş sequence'te neden true döndüğü ve doğrulamada Any ile birlikte kullanmak, Any'nin ilk element'te durması ve Count() > 0 farkı, Contains ve record eşitliği, SequenceEqual ve collection taşıyan record tuzağı, ToList/ToArray/ToHashSet ile snapshot, ToDictionary'de tekrar eden key ve Cast ile OfType 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
Ödeme adımından önce sepeti doğrulayan bir kontrol yazıyoruz: her satırın adedi sıfırdan büyük olmalı. Test ederken sepet boş kaldı. Bu kod ne yazdırır?
List<CartItem> cart = new();
bool allSmall = cart.All(i => i.Quantity <= 5);
bool anyItem = cart.Any();
Console.WriteLine($"All: {allSmall}, Any: {anyItem}");
if (cart.All(i => i.Quantity > 0))
{
Console.WriteLine("Cart is valid, proceeding to payment");
}
record CartItem(string Isbn, int Quantity);
All Boş Sequence'ta Neden True Döner
Çıktı:
All: True, Any: False
Cart is valid, proceeding to payment
Any boş sepette false dedi, beklendiği gibi. All ise iki kez true döndü ve boş bir sepet "geçerli" sayılıp ödemeye gönderildi. Bu bir bug değil, mantığın kendisi. All şu soruyu sorar: "Koşulu bozan bir element var mı?" Boş bir sequence'ta koşulu bozan element yoktur, dolayısıyla cevap true'dur. Dokümantasyon da açık: sequence boşsa All true döndürür. Mantıkta buna "vacuous truth" denir; "odadaki bütün fillerin rengi pembe" cümlesi, odada hiç fil yoksa doğrudur.
Gerçek kodda bu sonuç neredeyse hiç istediğin şey değildir. "Sepet geçerli" demek için iki şart lazım: sepette en az bir ürün olacak ve her ürün kurala uyacak. Yani cart.Any() && cart.All(i => i.Quantity > 0). Bu yazı LINQ'in evet/hayır sorularını (Any, All, Contains, SequenceEqual) ve sorgu sonuçlarını somut collection'lara çeviren dönüşüm operatörlerini anlatıyor. Bundan sonraki örnekler serinin kitapçı verisini kullanıyor; aşağıdaki dosyayı Data.cs adıyla Program.cs'in yanına koy:
record Book(string Isbn, string Title, string Author, decimal Price, int Stock, int Year);
static class Store
{
public static List<Book> Books { get; } = new()
{
new("978-0441172719", "Dune", "Frank Herbert", 12.99m, 4, 1965),
new("978-0553293357", "Foundation", "Isaac Asimov", 9.99m, 0, 1951),
new("978-0451524935", "1984", "George Orwell", 8.99m, 7, 1949),
new("978-0261102217", "The Hobbit", "J.R.R. Tolkien", 10.99m, 2, 1937),
new("978-0547928210", "The Fellowship of the Ring", "J.R.R. Tolkien", 15.99m, 3, 1954),
new("978-0553382563", "I, Robot", "Isaac Asimov", 7.99m, 5, 1950),
new("978-0060850524", "Brave New World", "Aldous Huxley", 11.49m, 1, 1932),
new("978-0451526342", "Animal Farm", "George Orwell", 6.99m, 9, 1945),
new("978-0441569595", "Neuromancer", "William Gibson", 10.49m, 0, 1984),
new("978-0441478125", "The Left Hand of Darkness", "Ursula K. Le Guin", 9.49m, 6, 1969),
};
}
Any ve All: İlk Cevapta Durmak
Bu operatörlerin hepsi immediate çalışır, ama kaynağın tamamını okumak zorunda değildir. Her element'te sayaç artıran bir sorguyla ne kadar okunduğunu görelim:
int reads = 0;
IEnumerable<Book> books = Store.Books.Where(b =>
{
reads++;
return true;
});
reads = 0;
bool any = books.Any();
Console.WriteLine($"Any(): {any}, reads: {reads}");
reads = 0;
bool countPositive = books.Count() > 0;
Console.WriteLine($"Count() > 0: {countPositive}, reads: {reads}");
reads = 0;
bool anyCheap = books.Any(b => b.Price < 9m);
Console.WriteLine($"Any(Price < 9): {anyCheap}, reads: {reads}");
reads = 0;
bool allAboveSeven = books.All(b => b.Price > 7m);
Console.WriteLine($"All(Price > 7): {allAboveSeven}, reads: {reads}");
Any(): True, reads: 1
Count() > 0: True, reads: 10
Any(Price < 9): True, reads: 3
All(Price > 7): False, reads: 8
Any() tek element okudu: bir tane bulması cevap için yeterli. Count() > 0 aynı soruyu on element sayarak cevapladı. Kaynak bir List<T> olsaydı Count() doğrudan property'yi okurdu ve fark kalmazdı; ama Where gibi bir sorgunun ya da bir veritabanı sorgusunun üzerinde bu fark gerçek bir maliyettir. EF Core'da Any() EXISTS'e, Count() ise bütün satırları sayan bir COUNT'a çevrilir. Kural basit: "var mı?" diye soruyorsan Any() yaz.
Predicate'li Any ilk eşleşmede, All ise ilk bozulmada durur. İlk 9'dan ucuz kitap üçüncü sırada (1984), 7'nin altındaki ilk kitap sekizinci sırada (Animal Farm). Where(...).Any() ile Any(...) arasında sonuç ve okuma açısından fark yoktur; kısa olanı tercih et.
Contains ve Eşitlik
string[] tags = { "SciFi", "Classic", "Bestseller" };
Console.WriteLine($"Contains(\"scifi\"): {tags.Contains("scifi")}");
Console.WriteLine($"OrdinalIgnoreCase: {tags.Contains("scifi", StringComparer.OrdinalIgnoreCase)}");
Book dune = new("978-0441172719", "Dune", "Frank Herbert", 12.99m, 4, 1965);
Console.WriteLine($"Same values: {Store.Books.Contains(dune)}");
Console.WriteLine($"Different stock: {Store.Books.Contains(dune with { Stock = 3 })}");
Console.WriteLine($"Same ISBN: {Store.Books.Any(b => b.Isbn == dune.Isbn)}");
Contains("scifi"): False
OrdinalIgnoreCase: True
Same values: True
Different stock: False
Same ISBN: True
Contains, önceki yazılarda gördüğümüz eşitlik kurallarının aynısını kullanır. Varsayılan string karşılaştırması büyük-küçük harfe duyarlı olduğu için "scifi" bulunamadı; comparer alan overload ile bulundu. Book bir record olduğu için, bütün field'ları aynı olan yeni bir instance listede "var" sayıldı. Stoğu farklı bir kopya ise bulunamadı, çünkü record eşitliği bütün property'lere bakar. Çoğu zaman "aynı kitap" derken kastettiğin ISBN'in eşleşmesidir; o durumda Any(b => b.Isbn == dune.Isbn) niyetini daha açık söyler.
Performans tarafında Contains kaynağın tipine göre davranır. Kaynak bir ICollection<T> ise onun kendi Contains'ini çağırır: List<T>'te bu baştan sona bir tarama (O(n)), HashSet<T>'te ortalama O(1) bir hash araması. Bir döngünün içinde aynı listeye tekrar tekrar Contains soruyorsan listeyi bir kez HashSet'e çevirmek en ucuz iyileştirmedir.
SequenceEqual: Sıra Önemli
string[] first = { "Dune", "1984", "The Hobbit" };
List<string> second = new() { "Dune", "1984", "The Hobbit" };
string[] reordered = { "1984", "Dune", "The Hobbit" };
Console.WriteLine($"==: {first == second.ToArray()}, SequenceEqual: {first.SequenceEqual(second)}");
Console.WriteLine($"Reordered: {first.SequenceEqual(reordered)}");
Console.WriteLine($"Sorted first: {first.OrderBy(t => t, StringComparer.Ordinal).SequenceEqual(reordered.OrderBy(t => t, StringComparer.Ordinal))}");
Console.WriteLine($"Ignore case: {first.SequenceEqual(new[] { "DUNE", "1984", "THE HOBBIT" }, StringComparer.OrdinalIgnoreCase)}");
Wishlist a = new("Ada", new List<string> { "Dune" });
Wishlist b = new("Ada", new List<string> { "Dune" });
Console.WriteLine($"record with a List: {a == b}, titles: {a.Titles.SequenceEqual(b.Titles)}");
record Wishlist(string Owner, List<string> Titles); // List reference eşitliğiyle karşılaştırılır
==: False, SequenceEqual: True
Reordered: False
Sorted first: True
Ignore case: True
record with a List: False, titles: True
İki array'i == ile karşılaştırmak reference'larını karşılaştırır; içerikleri aynı olsa da sonuç false. SequenceEqual element'leri sırayla, ikişer ikişer karşılaştırır ve uzunlukları da aynı olmalıdır. Farklı tipte iki collection'ı (array ve List) karşılaştırabilir, ama sırayı önemser: aynı üç kitap farklı sırada olunca sonuç false. Sırayı önemsemiyorsan önce ikisini de aynı kuralla sırala ya da set mantığıyla düşün. Comparer alan overload'u da var.
Son satır, record'larla çalışırken sık düşülen bir tuzak. Wishlist bir record, ama içindeki List<string> kendi başına bir reference type. Derleyicinin ürettiği eşitlik o property'yi EqualityComparer<List<string>>.Default ile, yani reference üzerinden karşılaştırır. İki istek listesinin içeriği aynı olduğu halde a == b false çıktı. Collection içeren record'larda eşitlik istiyorsan Equals'ı kendin yazıp SequenceEqual kullanman gerekir.
ToList, ToArray, ToHashSet: Sorguyu Dondurmak
Dönüşüm operatörleri deferred bir sorguyu çalıştırır ve sonucunu yeni bir collection'a kopyalar. Ondan sonra o collection kaynaktan bağımsızdır:
List<Book> source = new(Store.Books);
IEnumerable<Book> inStock = source.Where(b => b.Stock > 0);
List<Book> list = inStock.ToList();
Book[] array = inStock.ToArray();
HashSet<string> authors = inStock.Select(b => b.Author).ToHashSet();
source.RemoveAll(b => b.Price > 10m); // kaynağı değiştiren List method'u, LINQ değil
Console.WriteLine($"Query: {inStock.Count()}, list: {list.Count}, array: {array.Length}, authors: {authors.Count}");
Console.WriteLine($"Orwell in set: {authors.Contains("George Orwell")}");
Query: 4, list: 8, array: 8, authors: 6
Orwell in set: True
RemoveAll bir LINQ operatörü değil, kaynağı yerinde değiştiren bir List<T> method'u. Kaynaktan beş kitap silindikten sonra sorgu yeniden çalıştı ve 4 kitap buldu. Liste ve array ise sorgunun çağrıldığı andaki 8 kitabı tutuyor; set de o andaki altı yazarı. Bu davranış iki işe yarar: bir sorgunun sonucunu birden fazla kez kullanacaksan onu bir kez çalıştırırsın, ve kaynağın sonradan değişmesinden etkilenmeyen bir snapshot elde edersin.
Hangisini seçeceğin, sonuçla ne yapacağına bağlı. Sonuca element ekleyecek ya da çıkaracaksan ToList(). Boyutu değişmeyecek bir sonuç istiyorsan ToArray(). Sonuç üzerinde sık sık "bu var mı?" diye soracaksan ToHashSet(); aynı zamanda tekrarları da atar. Bir method'dan dışarı veri döndürürken çoğu zaman bu üçünden biri doğru seçimdir: çağıran kodun, senin sorgunun her enumerate edilişte yeniden çalıştığını bilmesi gerekmez.
ToDictionary ve Tekrar Eden Key
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Dictionary<string, Book> byIsbn = Store.Books.ToDictionary(b => b.Isbn);
Dictionary<string, decimal> priceByTitle = Store.Books.ToDictionary(b => b.Title, b => b.Price);
Console.WriteLine($"{byIsbn["978-0451524935"].Title}, Dune: {priceByTitle["Dune"]}");
try
{
Dictionary<string, Book> byAuthor = Store.Books.ToDictionary(b => b.Author);
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message);
}
Dictionary<string, Book> firstByAuthor = Store.Books.DistinctBy(b => b.Author).ToDictionary(b => b.Author);
Dictionary<string, int> countByAuthor = Store.Books.GroupBy(b => b.Author).ToDictionary(g => g.Key, g => g.Count());
Dictionary<string, Book> ignoreCase = Store.Books.ToDictionary(b => b.Title, StringComparer.OrdinalIgnoreCase);
Console.WriteLine($"First Asimov: {firstByAuthor["Isaac Asimov"].Title}, Asimov count: {countByAuthor["Isaac Asimov"]}");
Console.WriteLine($"\"DUNE\" found: {ignoreCase.ContainsKey("DUNE")}");
1984, Dune: 12.99
An item with the same key has already been added. Key: J.R.R. Tolkien
First Asimov: Foundation, Asimov count: 2
"DUNE" found: True
ToDictionary bir key selector, isteğe bağlı bir element selector ve isteğe bağlı bir comparer alır. ISBN gibi gerçekten benzersiz bir field ile sorunsuz çalışır. Yazar gibi tekrar edebilen bir field ile ise ikinci Tolkien kitabında ArgumentException fırlattı; mesaj, çakışan key'i de söylüyor. Bu iyi bir şey: veride beklemediğin bir tekrar varsa sessizce birini kaybetmektense hemen öğrenirsin.
Tekrar normalse üç yolun var. Her key için ilk element yeterliyse önce DistinctBy. Key başına bir özet lazımsa GroupBy ardından ToDictionary(g => g.Key, ...). Her key'in bütün element'lerine ihtiyacın varsa doğrudan ToLookup, gruplama yazısında gördüğümüz gibi. Kullanıcıdan gelen key'lerde (başlık, kullanıcı adı) comparer'ı da unutma: StringComparer.OrdinalIgnoreCase ile kurulan dictionary "DUNE" araması için de kitabı buldu.
Cast ve OfType
LINQ operatörleri IEnumerable<T> üzerinde çalışır. Generics öncesinden kalan ArrayList gibi tipler ise sadece non-generic IEnumerable implemente eder. Onları LINQ dünyasına sokmanın iki yolu var:
using System.Collections;
ArrayList legacy = new() { "Dune", 42, "1984", null, 3.5 };
Console.WriteLine($"OfType<string>: {string.Join(" | ", legacy.OfType<string>())}");
try
{
foreach (string? title in legacy.Cast<string>())
{
Console.WriteLine($"Cast<string>: {title}");
}
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}
int[] numbers = { 1, 2, 3 };
try
{
List<long> longs = numbers.Cast<long>().ToList();
}
catch (InvalidCastException ex)
{
Console.WriteLine(ex.Message);
}
List<long> converted = numbers.Select(n => (long)n).ToList();
Console.WriteLine($"Select: {string.Join(", ", converted)}");
Console.WriteLine($"Cast on List<Book> returns the same object: {ReferenceEquals(Store.Books, Store.Books.Cast<Book>())}");
OfType<string>: Dune | 1984
Cast<string>: Dune
Unable to cast object of type 'System.Int32' to type 'System.String'.
Unable to cast object of type 'System.Int32' to type 'System.Int64'.
Select: 1, 2, 3
Cast on List<Book> returns the same object: True
OfType<string>() bir filtredir: tipi uyan element'leri geçirir, gerisini atlar. 42, 3.5 ve null elendi. Cast<string>() ise bir iddiadır: "hepsi string". İlk element'i verdi, 42'ye gelince InvalidCastException fırlattı. Deferred çalıştığı için hata, sorgu tanımlandığında değil o element'e gelindiğinde çıktı.
İkinci exception klasik bir tuzak. Cast<long>() bir sayı dönüşümü yapmaz. Her element'i önce object'e boxing ile çevirir, sonra long'a unboxing yapmaya çalışır. Kutunun içinde bir int olduğu için unboxing başarısız olur; int'ten long'a implicit dönüşüm olması bir şey değiştirmez. Sayı dönüşümü için Select(n => (long)n) yaz. Son satır da bir iyileştirmeyi gösteriyor: kaynak zaten istenen tipte bir IEnumerable<T> ise Cast yeni bir şey üretmez, aynı object'i döndürür. Makinede .NET 6 runtime'ı olmadığı için örnekler net6.0 hedefleyip DOTNET_ROLL_FORWARD=Major ile .NET 8 runtime'ında çalıştı; exception mesajlarının metni runtime sürümüne göre küçük farklar gösterebilir.
Sık Düşülen Tuzaklar
Boş olabilecek bir collection'da tek başına All ile doğrulama yapmak. Boş sequence her kuralı "geçer". Any() && All(...) yaz.
Varlık kontrolü için Count() > 0 yazmak. Sorgularda ve veritabanında gereksiz yere her şeyi sayar; Any() ilk element'te durur.
Büyük bir List üzerinde döngü içinde Contains çağırmak. Her çağrı O(n) bir tarama; önce ToHashSet().
İki collection'ı == ya da Equals ile karşılaştırmak. Bu reference'lara bakar; içerik için SequenceEqual. Collection taşıyan record'ların eşitliği de aynı sebeple beklediğin gibi çalışmaz.
Tekrar edebilecek bir field ile ToDictionary kurmak. Veri bir gün tekrar getirdiğinde ArgumentException alırsın; ToLookup, GroupBy ya da DistinctBy ile niyetini açıkça seç.
Cast<T>()'ı tip dönüşümü sanmak. Sadece reference ve boxing dönüşümleri yapar; int'ten long'a geçmez. Karışık bir collection'ı filtrelemek için OfType<T>(), sayı dönüşümü için Select.
Küçük Bir Deneme
★☆☆ Kitap listesinde stoğu 0 olan bir kitap olup olmadığını, bütün kitapların 1950'den sonra basılıp basılmadığını ve "Neuromancer"ın listede olup olmadığını üç ayrı operatörle sorgula.
★★☆ İki sipariş listesinin aynı ISBN'leri içerip içermediğini sıradan ve tekrarlardan bağımsız olarak kontrol eden bir fonksiyon yaz. Önce SequenceEqual ile, sonra HashSet<T>.SetEquals ile çöz ve farkı açıkla.
★★★ Wishlist record'unun eşitliğini Titles içeriğine göre çalışacak şekilde yeniden yaz: Equals(Wishlist? other)'ı kendin tanımla ve GetHashCode'un da bununla tutarlı olmasını sağla. Distinct ile test et.
Bu yazıyla LINQ'in hazır operatörlerinin büyük kısmını gördük. Bir sonraki yazıda masanın öbür tarafına geçiyoruz: extension method ve yield return ile kendi LINQ operatörünü yazacağız ve deferred execution'ın içeride nasıl çalıştığını kendi kodumuzda göreceğiz.