Məzmuna keç
Educora
İrəli23 dəq13 / 16

Qabaqcıl LINQ və generiklər

Verilənləri `GroupBy`, `Join`, `SelectMany` ilə qruplaşdır və birləşdir, təxirə salınmış icranı dərindən anla, generik məhdudiyyətlər və genişləndirmə metodları yaz.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • GroupBy, CountBy, ToLookup ilə qruplar üzrə hesabat qurmaq
  • Kolleksiyaları Join, GroupJoin, LeftJoin və SelectMany ilə birləşdirmək
  • Təxirə salınmış icranı izah etmək, məhdudiyyətli generik metodlar və genişləndirmə metodları yazmaq

LINQ-in əsaslarını artıq bilirsən: Where, Select, OrderBy. Real verilənlər isə nadir hallarda bir siyahıda olur: şagirdlər bir cədvəldə, fənlər ikincidə, qiymətlər üçüncüdə saxlanılır. Bu dərsdə onları LINQ ilə birləşdirməyi, qruplaşdırmağı və düzləşdirməyi, sonra isə LINQ-in özünün qurulduğu alətləri — generikləri, onların məhdudiyyətlərini və genişləndirmə metodlarını öyrənəcəksən.

Qruplaşdırma: GroupBy, CountBy, ToLookup

GroupBy elementləri açara görə qruplara bölür, hər qrup isə Key xassəsi olan kolleksiyadır. Onu Select ilə birləşdirəndə hər qrupdan hesabat sətri — say, orta, ən yaxşı nəticə — alınır. .NET 9-da gələn CountBy «hər açardan neçə dənə?» sualına bir addımda cavab verir, ToLookup isə nəticəni dərhal hesablayıb açara görə axtarış üçün saxlayır (açar yoxdursa, boş ardıcıllıq qaytarır).

C#
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);
Gözlənilən nəticə
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
Anonim tip (new { ... }) hesabat sətrini saxlayır; MaxBy ən yüksək ballı şagirdi qrupu sıralamadan tapır

Join, GroupJoin və SelectMany

Join iki kolleksiyanı ortaq açara görə birləşdirir — SQL-dəki INNER JOIN kimi: nəticəyə yalnız cütü tapılan elementlər düşür. GroupJoin hər sol elementə sağdakı uyğun elementlərin qrupunu verir, qrup boş da ola bilər — ona görə heç bir kursu olmayan Leyla da görünür. SelectMany isə iç-içə kolleksiyaları bir düz ardıcıllığa çevirir: günlərin dərs siyahılarından bütün fənlərin vahid siyahısı alınır.

C#
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);
Gözlənilən nəticə
Aysel - Math: 95
Aysel - Physics: 88
Murad - Math: 78
Aysel: 2, Murad: 1, Leyla: 0
Art, Chemistry, Math, Physics
Üç «cədvəl» — SQL kursundakı students, courses, enrollments kimi

Birləşmələr çox olanda SQL-ə bənzəyən sorğu sintaksisi adətən daha oxunaqlıdır: from e in enrollments join s in students on e.StudentId equals s.Id select .... Burada müqayisə üçün == deyil, equals açar sözü yazılır, join ... into g isə GroupJoin-ə çevrilir. let ara nəticəni adlandırmağa imkan verir: let passed = e.Score >= 50. Kompilyator hər iki yazılışı eyni metod çağırışlarına çevirir, ona görə sürət fərqi yoxdur.

Köhnə sol birləşmə: `GroupJoin` + `SelectMany` + `DefaultIfEmpty`
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"}");
.NET 10: `LeftJoin`
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 score
.NET 10 LINQ-ə LeftJoin və RightJoin metodlarını əlavə edib; hər iki variant eyni nəticəni verir (yoxlanılıb)

Təxirə salınmış icra: sorğu nə vaxt işləyir?

Əsaslar dərsində gördün ki, Where və Select dərhal heç nə hesablamır. Aşağıdakı yield return iteratoru hər elementi çap edərək bunu «içəridən» göstərir: First() birinci uyğun elementi tapan kimi dayanır, Count() isə bütün zənciri yenidən, başdan sona icra edir. Mənbə verilənlər bazası və ya şəbəkə olanda hər təkrar gəzinti yeni sorğu deməkdir.

C#
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;
    }
}
Gözlənilən nəticə
Query created
  producing 1
First: 10
  producing 1
  producing 2
  producing 3
  producing 4
  producing 5
Count: 3
Sorğu yaradılanda heç nə çap olunmadı; First() bir element istədi, Count() isə hamısını yenidən keçdi

Generiklər və məhdudiyyətlər

Generik metod və ya sinif bir dəfə yazılır və müxtəlif tiplərlə işləyir: Max<T> həm ədədləri, həm də sətirləri müqayisə edir. Amma T haqqında heç nə bilinmirsə, onunla demək olar ki, heç nə etmək olmur. Məhdudiyyət (where) kompilyatora T-nin nə bacardığını deyir: where T : IComparable<T> — CompareTo var; where T : INumber<T> — + və T.Zero var (bu, .NET 7-dən bəri mövcud olan generik riyaziyyatdır); where T : class, IEntity — T istinad tipidir və Id xassəsinə malikdir.

C#
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);
}
Gözlənilən nəticə
9
plum
6
3.75
Aysel
none
Bir Sum<T> həm int, həm də double üçün işləyir; Repository<T> isə Id-si olan istənilən sinfi saxlaya bilir
MəhdudiyyətMənası
where T : classistinad tipi (sinif, record, interfeys)
where T : structqiymət tipi (int, DateTime, struct)
where T : new()parametrsiz konstruktoru var: new T() yazmaq olar
where T : notnullnull ola bilməyən tip
where T : IComparable<T>göstərilən interfeysi reallaşdırır
where T : INumber<T>ədəddir: +, -, T.Zero, T.One işləyir

Genişləndirmə metodları

LINQ-in bütün metodları — Where, Select, GroupBy — genişləndirmə metodlarıdır: onlar System.Linq.Enumerable statik sinfində yazılıb, amma IEnumerable<T>-nin öz metodları kimi çağırılır. Öz metodunu da belə yaza bilərsən: static sinifdə static metod, birinci parametrin qarşısında isə this. C# 14 genişləndirmə blokları gətirib: extension(Student student) { ... } içində yalnız metodlar deyil, xassələr də elan etmək olur.

C#
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);
Gözlənilən nəticə
83
Aysel Mammadova
Leyla passed: True
Median massivə, ToTitle sətrə, IsPassing isə Student-ə «əlavə olunub» — orijinal tiplər dəyişməyib
Klassik genişləndirmə metodu
static class StudentExtensions
{
    // classic extension method: must be called with ()
    public static bool IsPassing(this Student student) => student.Score >= 50;
}
// usage: leyla.IsPassing()
C# 14: genişləndirmə xassəsi
static class StudentExtensions
{
    extension(Student student)      // C# 14: extension properties are possible too
    {
        public bool IsPassing => student.Score >= 50;
    }
}
// usage: leyla.IsPassing

Əsas fikirlər

  • GroupBy + Select qruplar üzrə hesabat qurur; CountBy sayır, ToLookup qrupları axtarış üçün saxlayır.
  • Join — daxili birləşmə, GroupJoin — sol tərəfi saxlayan qruplar, SelectMany — düzləşdirmə; .NET 10-da LeftJoin var.
  • LINQ sorğuları tənbəldir: hər gəzinti onları yenidən icra edir; ToList() nəticəni dondurur.
  • Məhdudiyyətlər (where T : ...) T-nin bacarıqlarını bildirir; INumber<T> generik riyaziyyatı açır.
  • Genişləndirmə metodu: static sinifdə this parametrli static metod; C# 14-də xassələr də əlavə etmək olur.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
Dərsdəki verilənlər üçün students.CountBy(s => s.Class) nə qaytarır?