CodeGym /Kurslar /C# SELF /Senkron və asenkron generatorlar C#-da (

Senkron və asenkron generatorlar C#-da ( yield)

C# SELF
Səviyyə , Dərs
Mövcuddur

1. Giriş

Təsəvvür edin ki, milyonlarla sətrdən ibarət nəhəng log faylı ilə işləməlisiniz, ya da inanılmaz dərəcədə uzun ədədlər ardıcıllığı yaratmalısınız. Generatorlar olmadan bunu necə edərdiniz?

Ənənəvi yanaşma belə görünərdi:


// Problem: bütün kolleksiyanı birbaşa yaddaşda yaradır
List<int> GenerateAllNumbersSync(int count)
{
    List<int> numbers = new List<int>();
    for (int i = 0; i < count; i++)
    {
        numbers.Add(i);
    }
    return numbers; // Hamısı hazır olanda qaytarırıq
}

// İstifadə:
var myNumbers = GenerateAllNumbersSync(1_000_000); // 1 milyon ədəd dərhal yaddaşda!
foreach (var num in myNumbers) { /* Emal */ }

Burada nə problemlər var?

  1. Yaddaş istehlakı: Əgər count çox böyükdürsə, bütün kolleksiya yaddaşda yaradılır və bu OutOfMemoryException-ə səbəb ola bilər.
  2. Gecikmə: İstifadəçi və ya proqramın növbəti hissəsi bütün məlumatlar tam yaradılıb yaddaşa yüklənənə qədər gözləməlidir.
  3. Sonsuz ardıcıllıqlar: Ardıcıllıq potensial olaraq sonsuzdursa, bu yanaşma işləməyəcək.

Burada köməyə generatorlar gəlir! Onlar lazımi vaxtda hesablanma (Lazy Evaluation) və axınlı emal (Streaming) konsepsiyasını tətbiq edir. Bütün məlumatları birdən yaratmaq əvəzinə, generator elementləri tək-tək, yalnız lazım olduqda istehsal edir.

2. Generatorların əsasları

C#-da generatorlar xüsusi açar söz yield ilə yaradılır.

Generator nədir?

Bu, bir və ya bir neçə yield return ifadəsi olan metod, property-nin get bloku və ya operator ola bilər.

yield return

Generatorların ürəyidir. Kompilyator yield return-lə qarşılaşanda:

  1. yield return-dən sonra göstərilən element çağırana ötürülür.
  2. Generator metodunun icrası dayandırılır, və onun cari vəziyyəti (döngüdə harada olduğu, lokal dəyişənlərin dəyərləri) saxlanılır.
  3. Növbəti element tələb edildikdə (məsələn, foreach-in növbəti iterasiyasında) metod dayandırıldığı yerdən davam edir.

Qaytarılan tip: Generator metodu IEnumerable<T> və ya IEnumerator<T> qaytarmalıdır. Kompilyator bütün lazımi "sehr"i özü yaradır.


// Nümunə 2.1: Sadə ədədlər generatoru
IEnumerable<int> GenerateNumbers(int count)
{
    Console.WriteLine("Başlanğıcda generator...");
    for (int i = 0; i < count; i++)
    {
        Console.WriteLine($"Yaratıram: {i}");
        yield return i; // Dayandırma və elementin qaytarılması
    }
    Console.WriteLine("Generator tamamlandı.");
}

// İstifadə:
// Diqqət edin: "Başlanğıcda generator..." yalnız ilk iterasiyada görünəcək!
// Və "Yaratıram: X" — hər yeni iterasiyada.
foreach (var num in GenerateNumbers(3))
{
    Console.WriteLine($"foreach-də alındı: {num}");
}

yield break

İterasiyanı vaxtından əvvəl bitirmək üçün istifadə olunur. yield break-dən sonra daha element qaytarılmayacaq. Əgər metodun sonuna çatılırsa, ayrıca yield break lazım deyil.


// Nümunə 2.2: Çıxış şərti olan generator
IEnumerable<string> GetFirstNElements(List<string> source, int n)
{
    int count = 0;
    foreach (var item in source)
    {
        if (count >= n)
        {
            yield break; // Generatoru tərk edirik
        }
        yield return item;
        count++;
    }
}

// İstifadə:
// var fruits = new List<string> { "Apple", "Banana", "Orange", "Grape" };
// foreach (var fruit in GetFirstNElements(fruits, 2))
// {
//     Console.WriteLine(fruit); // "Apple", "Banana" çap olunacaq
// }

3. Vəziyyət maşını

Bunun "kapotun altında" necə işlədiyini bilmək istəyirsən? Burada sehr yox — yalnız kompilyatorun ağıllı işi var.

Siz yield ilə metod yazdıqda, kompilyator C# onu IEnumerator<T>IEnumerable<T> implement edən bir sinfə çevirir. Bu yaradılan sinif — vəziyyət maşını-dır.

  • Vəziyyətin saxlanması: maşın dayandığı yeri (vəziyyət nömrəsi) və dayandırılma anındakı bütün lokal dəyişənlərin dəyərlərini saxlayır.
  • Iterasiya: foreach vasitəsilə iterasiya edəndə MoveNext() metodları çağırılır və Current oxunur. MoveNext() növbəti yield return/yield break-ə qədər icranı davam etdirir, Current isə cari elementi qaytarır.

Əslində kompilyator sizin üçün Iterator Pattern-ini həyata keçirir.

4. Generatorların tətbiqi

Nümunə: Böyük həcmli məlumatların işlənməsi

Faylı bütününü yaddaşa yükləmədən sətir-sətir oxumaq.


// Böyük faylın oxunmasının simulyasiyası
IEnumerable<string> ReadBigFileLines(string filePath)
{
    Console.WriteLine($"Faylı açıram: {filePath}");
    // Real tətbiqdə burada StreamReader olacaq
    yield return "Məlumat sətiri 1";
    yield return "Məlumat sətiri 2";
    yield return "Məlumat sətiri 3";
    Console.WriteLine("Fayl oxumasını simulyasiya etməyi bitirdim.");
}

// İstifadə:
Console.WriteLine("Emala başlanıldı.");
foreach (var line in ReadBigFileLines("my_huge_log.txt"))
{
    Console.WriteLine($"Emal olunan sətir: {line}");
    if (line.Contains("2")) break; // İstədiyimiz vaxt dayana bilərik
}
Console.WriteLine("Emal tamamlandı.");

Qeyd edin ki, son mesaj yalnız iterasiya bitdikdən sonra görünəcək.

Nümunə: Sonsuz ardıcıllıqlar


IEnumerable<long> FibonacciSequence()
{
    long a = 0;
    long b = 1;
    while (true) // Potensial olaraq sonsuz ardıcıllıq
    {
        yield return a;
        long temp = a;
        a = b;
        b = temp + b;
    }
}

// İstifadə:
int count = 0;
foreach (var num in FibonacciSequence())
{
    Console.WriteLine(num);
    count++;
    if (count >= 10) break; // Çıxış qoymaq lazımdır ki, ilişib qalmarıq
}

Nümunə: Data pipeline-lar

Hər mərhələ məlumatları "uçuşda" emal edən metod zəncirləri yaratmaq.


IEnumerable<int> GetNumbers()
{
    yield return 1; yield return 2; yield return 3; yield return 4; yield return 5;
}

IEnumerable<int> FilterEven(IEnumerable<int> source)
{
    foreach (var num in source)
    {
        if (num % 2 == 0) yield return num;
    }
}

IEnumerable<int> Square(IEnumerable<int> source)
{
    foreach (var num in source)
    {
        yield return num * num;
    }
}

// İstifadə:
foreach (var result in Square(FilterEven(GetNumbers())))
{
    Console.WriteLine(result); // 4, 16
}

Bu, LINQ operatorlarının (məsələn, Where, Select, Take, Skip) necə işlədiyinə çox bənzəyir.

5. Asenkron generatorlar

Senkron generatorlar əla olsa da, hər bir element asenkron əməliyyat tələb edirsə (məsələn, şəbəkə sorğusu), onda C# 8.0-dək bunu həyata keçirmək çətin idi.

Məsələ: Asenkron data axınları

Senkron generator metodunun içində await istifadə etmək olmaz.


// Bu SƏHVƏR SƏRFEDİLƏCƏK!
IEnumerable<string> GetStringsAsyncProblem()
{
    await Task.Delay(100); // Səhv: await yalnız async metodda ola bilər
    yield return "Hello";
}

Həll: IAsyncEnumerable<T>await foreach

  • IAsyncEnumerable<T>IEnumerable<T>-in asenkron analoqudur.
  • await foreach — asenkron ardıcıllığı dolaşmaq üçün rahat sintaksisdir (içəridə MoveNextAsync()-i çağırır və asenkron vəziyyəti idarə edir).

async yield return

İndi async metodun içində, IAsyncEnumerable<T> qaytaran halda yield return istifadə etmək olar. Kompilyator asenkron vəziyyət maşını quracaq.


// Nümunə 5.1: Asenkron ədədlər generatoru
async IAsyncEnumerable<int> GenerateNumbersAsync()
{
    Console.WriteLine("Asenkron generatora başlanğıc...");
    for (int i = 0; i < 5; i++)
    {
        await Task.Delay(100); // Asenkron işin simulyasiyası (məsələn, şəbəkə sorğusu)
        Console.WriteLine($"Asenkron yaradılır: {i}");
        yield return i; // Elementi qaytarırıq
    }
    Console.WriteLine("Asenkron generator tamamlandı.");
}

// İstifadə:
async Task ConsumeAsyncNumbers()
{
    Console.WriteLine("Asenkron emala başlanıldı...");
    await foreach (var number in GenerateNumbersAsync())
    {
        Console.WriteLine($"Asenkron alındı: {number}");
    }
    Console.WriteLine("Asenkron emal tamamlandı.");
}

// İşə salma:
await ConsumeAsyncNumbers(); // Bunu async Main və ya oxşar konteksdə çağırın

IAsyncDisposableawait using (generator kontekstində)

Əgər generator asenkron şəkildə azad edilən resurs açırsa (DisposeAsync()), await using istifadə edin. await foreach bitəndə daxili iterator IAsyncDisposable implement edirsə avtomatik olaraq DisposeAsync()-ı çağıracaq.


// Nümunə 5.2: await using ilə asenkron fayl oxumaq
// Real StreamReader IAsyncDisposable implement edir
async IAsyncEnumerable<string> ReadFileLinesAsync(string filePath)
{
    Console.WriteLine($"[Generator] Asenkron faylı açıram: {filePath}");
    // await using DisposeAsync() çağırılmasını təmin edir
    await using var reader = new StreamReader(filePath); 
    
    string? line;
    while ((line = await reader.ReadLineAsync()) != null) // Sətirin asenkron oxunması
    {
        yield return line;
    }
    Console.WriteLine($"[Generator] Fayl oxunması bitdi: {filePath}");
}

// İstifadə:
async Task ProcessFileAsync()
{
    Console.WriteLine("[Processor] Fayl emalına başlanıldı.");
    await foreach (var line in ReadFileLinesAsync("path_to_some_file.txt")) // real yol ilə əvəz edin
    {
        Console.WriteLine($"[Processor] Sətir alındı: {line}");
        // Burada hər sətirin asenkron emalını yerinə yetirə bilərsiniz
        await Task.Delay(50); 
    }
    Console.WriteLine("[Processor] Fayl emalı tamamlandı.");
}

// İşə salma:
await ProcessFileAsync(); // Bunu async Main-də çağırın

Asenkron generatorların ləğvi: CancellationToken

Asenkron generatorlara CancellationToken əlavə edin ki, çağıran kod generatoru ləğv edə bilsin.


// Nümunə 5.3: Ləğv edilə bilən asenkron generator
async IAsyncEnumerable<int> GenerateCancelableSequence(
    int start, int count, 
    [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken token = default)
{
    for (int i = 0; i < count; i++)
    {
        token.ThrowIfCancellationRequested(); // Ləğv tokenini yoxlayırıq
        await Task.Delay(100, token); // Task.Delay də ləğvi dəstəkləyir
        yield return start + i;
    }
}

İstifadə


var cts = new CancellationTokenSource();
Task.Run(async () =>
{
    await Task.Delay(300); // Generatorun biraz işləməsinə imkan veririk
    cts.Cancel(); // Ləğv edirik!
});

try
{
    await foreach (var num in GenerateCancelableSequence(0, 100, cts.Token))
    {
        Console.WriteLine($"Alındı: {num}");
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("Generator ləğv edildi.");
}

6. Məhdudiyyətlər və diqqətli istifadə

yield məhdudiyyətləri:

  • yield return və ya yield break olan try-bloklarında catch və ya finally içərisində də yield istifadə etmək olmaz.
  • yield-li metodlar unsafe ola bilməz.
  • yield async void metodlarda istifadə edilə bilməz (yerinə async Task və ya IAsyncEnumerable<T> istifadə edin).

Performans: Çox kiçik və ya sabit ölçülü kolleksiyalar üçün vəziyyət maşınının əlavə overhead-i birbaşa List<T> qaytarmaqdan bir az çox ola bilər. Amma böyük məlumatlar üçün lazy və streaming-dən əldə olunan fayda adətən daha önəmlidir.

Səhvlərin emalı: Generator daxilində atılan istisnalar çağıran koda doğru düzgün çatdırılacaq və orada adi metodlar kimi tutulub emal oluna bilər.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION