GroupBy,CountByveToLookupile grup bazında raporlar hazırlamak- Koleksiyonları
Join,GroupJoin,LeftJoinveSelectManyile birleştirmek - Ertelenmiş yürütmeyi açıklamak, kısıtlamalı generic metotlar ve genişletme metotları yazmak
LINQ'in temellerini zaten biliyorsun: Where, Select, OrderBy. Ama gerçek veriler nadiren tek bir listede durur: öğrenciler bir tabloda, dersler ikincisinde, notlar üçüncüsünde saklanır. Bu derste onları LINQ ile birleştirmeyi, gruplamayı ve düzleştirmeyi, ardından LINQ'in kendisinin üzerine kurulduğu araçları öğreneceksin: generic'ler, onların kısıtlamaları ve genişletme metotları.
Gruplama: GroupBy, CountBy, ToLookup
GroupBy, elemanları bir anahtara göre gruplara ayırır; her grup, Key özelliği olan bir koleksiyondur. Select ile birleşince her grup bir rapor satırına dönüşür: sayı, ortalama, en iyi sonuç. .NET 9 ile gelen CountBy, “her anahtardan kaç tane?” sorusunu tek adımda yanıtlar; ToLookup ise sonucu hemen hesaplar ve anahtara göre arama için saklar (olmayan bir anahtar için boş bir dizi döndürür).
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
List<Student> students =
[
new("Aysel", "10A", 95), new("Murad", "10B", 78), new("Leyla", "10A", 88),
new("Elvin", "10B", 45), new("Nigar", "11A", 91), new("Kamran", "10A", 67)
];
var byClass = students
.GroupBy(s => s.Class)
.Select(g => new
{
Class = g.Key,
Count = g.Count(),
Average = g.Average(s => s.Score),
Best = g.MaxBy(s => s.Score)!.Name
})
.OrderByDescending(x => x.Average);
foreach (var c in byClass)
Console.WriteLine($"{c.Class} ({c.Count}): average {c.Average:F1}, best {c.Best}");
foreach (var (cls, count) in students.CountBy(s => s.Class))
Console.Write($"{cls}={count} ");
Console.WriteLine();
ILookup<string, string> namesByClass = students.ToLookup(s => s.Class, s => s.Name);
Console.WriteLine(string.Join(", ", namesByClass["10B"]));
record Student(string Name, string Class, int Score);11A (1): average 91.0, best Nigar 10A (3): average 83.3, best Aysel 10B (2): average 61.5, best Murad 10A=3 10B=2 11A=1 Murad, Elvin
new { ... }) rapor satırını tutar; MaxBy en yüksek puanlı öğrenciyi grubu sıralamadan bulurJoin, GroupJoin ve SelectMany
Join, iki koleksiyonu ortak bir anahtara göre birleştirir; SQL'deki INNER JOIN gibi: sonuca yalnızca eşi bulunan elemanlar girer. GroupJoin her sol elemana, sağdaki eşleşen elemanların grubunu verir ve grup boş da olabilir; bu yüzden hiç dersi olmayan Leyla da görünür. SelectMany ise iç içe koleksiyonları tek bir düz diziye çevirir: günlük ders listelerinden tüm derslerin tek bir listesi elde edilir.
List<Student> students = [new(1, "Aysel"), new(2, "Murad"), new(3, "Leyla")];
List<Course> courses = [new(10, "Math"), new(20, "Physics")];
List<Enrollment> enrollments = [new(1, 10, 95), new(1, 20, 88), new(2, 10, 78)];
// inner join: only students who have enrollments
var rows = enrollments
.Join(students, e => e.StudentId, s => s.Id, (e, s) => new { s.Name, e.CourseId, e.Score })
.Join(courses, x => x.CourseId, c => c.Id, (x, c) => $"{x.Name} - {c.Title}: {x.Score}");
foreach (var row in rows) Console.WriteLine(row);
// GroupJoin: every student with the group of their enrollments
var counts = students.GroupJoin(enrollments, s => s.Id, e => e.StudentId,
(s, list) => $"{s.Name}: {list.Count()}");
Console.WriteLine(string.Join(", ", counts));
// SelectMany flattens nested collections
string[][] timetable = [["Math", "Physics"], ["Math", "Art"], ["Chemistry"]];
var subjects = timetable.SelectMany(day => day).Distinct().Order();
Console.WriteLine(string.Join(", ", subjects));
record Student(int Id, string Name);
record Course(int Id, string Title);
record Enrollment(int StudentId, int CourseId, int Score);Aysel - Math: 95 Aysel - Physics: 88 Murad - Math: 78 Aysel: 2, Murad: 1, Leyla: 0 Art, Chemistry, Math, Physics
students, courses ve enrollments gibiBirleştirmeler çok olduğunda SQL'e benzeyen sorgu sözdizimi genellikle daha okunaklıdır: from e in enrollments join s in students on e.StudentId equals s.Id select .... Burada karşılaştırma için == değil, equals anahtar sözcüğü yazılır; join ... into g ise bir GroupJoin'e dönüşür. let, bir ara sonuca ad vermeni sağlar: let passed = e.Score >= 50. Derleyici iki yazımı da aynı metot çağrılarına çevirir; bu yüzden hız farkı yoktur.
var result = students
.GroupJoin(enrollments, s => s.Id, e => e.StudentId, (s, list) => new { s, list })
.SelectMany(x => x.list.DefaultIfEmpty(),
(x, e) => $"{x.s.Name}: {e?.Score.ToString() ?? "no score"}");var result = students
.LeftJoin(enrollments, s => s.Id, e => e.StudentId,
(s, e) => $"{s.Name}: {e?.Score.ToString() ?? "no score"}");
// students without enrollments are kept: Leyla: no scoreLeftJoin ve RightJoin metotlarını ekledi; iki sürüm de aynı sonucu verir (denendi)Ertelenmiş yürütme: sorgu ne zaman çalışır?
Temeller dersinde Where ve Select'in hemen hiçbir şey hesaplamadığını gördün. Aşağıdaki yield return yineleyicisi her elemanı yazdırarak bunu “içeriden” gösteriyor: First() ilk eşleşmeyi bulur bulmaz durur, Count() ise bütün zinciri başından sonuna yeniden çalıştırır. Kaynak bir veritabanı ya da ağ olduğunda her yeni dolaşma yeni bir istek demektir.
var query = Numbers().Where(n => n % 2 == 1).Select(n => n * 10);
Console.WriteLine("Query created");
Console.WriteLine($"First: {query.First()}");
Console.WriteLine($"Count: {query.Count()}");
IEnumerable<int> Numbers()
{
for (int i = 1; i <= 5; i++)
{
Console.WriteLine($" producing {i}");
yield return i;
}
}Query created producing 1 First: 10 producing 1 producing 2 producing 3 producing 4 producing 5 Count: 3
First() tek bir eleman istedi, Count() ise hepsini yeniden dolaştıGeneric'ler ve kısıtlamalar
Generic bir metot ya da sınıf bir kez yazılır ve farklı türlerle çalışır: Max<T> hem sayıları hem de dizeleri karşılaştırır. Ama T hakkında hiçbir şey bilinmiyorsa onunla neredeyse hiçbir şey yapılamaz. Bir kısıtlama (where), derleyiciye T'nin neler yapabildiğini söyler: where T : IComparable<T> — CompareTo vardır; where T : INumber<T> — + ve T.Zero vardır (bu, .NET 7'den beri var olan generic matematiktir); where T : class, IEntity — T, Id özelliği olan bir başvuru türüdür.
using System.Globalization;
using System.Numerics;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine(Max([3, 9, 4]));
Console.WriteLine(Max(["pear", "apple", "plum"]));
Console.WriteLine(Sum([1, 2, 3]));
Console.WriteLine(Sum([1.5, 2.25]));
var repo = new Repository<Student>();
repo.Add(new Student(1, "Aysel"));
Console.WriteLine(repo.Find(1)?.Name ?? "none");
Console.WriteLine(repo.Find(7)?.Name ?? "none");
static T Max<T>(IEnumerable<T> items) where T : IComparable<T>
{
T best = items.First();
foreach (T item in items)
if (item.CompareTo(best) > 0) best = item;
return best;
}
static T Sum<T>(IEnumerable<T> items) where T : INumber<T>
{
T total = T.Zero;
foreach (T item in items) total += item;
return total;
}
interface IEntity { int Id { get; } }
record Student(int Id, string Name) : IEntity;
class Repository<T> where T : class, IEntity
{
private readonly Dictionary<int, T> items = [];
public void Add(T item) => items[item.Id] = item;
public T? Find(int id) => items.GetValueOrDefault(id);
}9 plum 6 3.75 Aysel none
Sum<T> hem int hem de double için çalışır; Repository<T> ise Id'si olan her sınıfı saklayabilir| Kısıtlama | Anlamı |
|---|---|
where T : class | başvuru türü (sınıf, record, arayüz) |
where T : struct | değer türü (int, DateTime, bir struct) |
where T : new() | parametresiz bir yapıcısı vardır: new T() yazılabilir |
where T : notnull | null olamayan bir tür |
where T : IComparable<T> | belirtilen arayüzü gerçekleştirir |
where T : INumber<T> | bir sayıdır: +, -, T.Zero, T.One çalışır |
Genişletme metotları
LINQ'in tüm metotları (Where, Select, GroupBy) genişletme metotlarıdır: System.Linq.Enumerable statik sınıfında yazılmışlardır ama IEnumerable<T>'nin kendi metotlarıymış gibi çağrılırlar. Kendi metodunu da aynı şekilde yazabilirsin: static bir sınıfta static bir metot ve ilk parametrenin önünde this. C# 14 ise genişletme blokları getirdi: extension(Student student) { ... } içinde yalnızca metotlar değil, özellikler de tanımlanabilir.
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
int[] scores = [95, 78, 88, 45];
Console.WriteLine(scores.Median());
Console.WriteLine("aysel mammadova".ToTitle());
var leyla = new Student("Leyla", 88);
Console.WriteLine($"{leyla.Name} passed: {leyla.IsPassing}");
static class StatsExtensions
{
public static double Median(this IEnumerable<int> source)
{
int[] sorted = source.Order().ToArray();
int mid = sorted.Length / 2;
return sorted.Length % 2 == 0 ? (sorted[mid - 1] + sorted[mid]) / 2.0 : sorted[mid];
}
public static string ToTitle(this string text) =>
CultureInfo.InvariantCulture.TextInfo.ToTitleCase(text);
}
static class StudentExtensions
{
extension(Student student) // C# 14: an extension block
{
public bool IsPassing => student.Score >= 50;
}
}
record Student(string Name, int Score);83 Aysel Mammadova Leyla passed: True
Median dizilere, ToTitle dizelere, IsPassing ise Student'a “eklendi”; özgün türler değişmedistatic class StudentExtensions
{
// classic extension method: must be called with ()
public static bool IsPassing(this Student student) => student.Score >= 50;
}
// usage: leyla.IsPassing()static class StudentExtensions
{
extension(Student student) // C# 14: extension properties are possible too
{
public bool IsPassing => student.Score >= 50;
}
}
// usage: leyla.IsPassingÖnemli noktalar
GroupBy+Selectgrup bazında rapor oluşturur;CountBysayar,ToLookupgrupları arama için saklar.Joiniç birleştirmedir,GroupJoinsol tarafı koruyarak grup verir,SelectManydüzleştirir; .NET 10'daLeftJoinvardır.- LINQ sorguları tembeldir: her dolaşma onları yeniden çalıştırır;
ToList()sonucu dondurur. - Kısıtlamalar (
where T : ...)T'nin neler yapabildiğini belirtir;INumber<T>generic matematiği açar. - Genişletme metodu,
staticbir sınıftakithisparametrelistaticbir metottur; C# 14'te özellikler de eklenebilir.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.
students.CountBy(s => s.Class) ne döndürür?