CodeGym /Kurslar /C# SELF /Abunəçilər və hadisələrin təhlükəsiz çağırışı

Abunəçilər və hadisələrin təhlükəsiz çağırışı

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

1. Giriş

Biz yazanda

worker.WorkCompleted += listener.OnWorkCompleted;

əslində hadisənin "çağırış siyahısına" (multicast delegate) metoda göstərici əlavə edirik. Bu "siyahı" hadisənin daxilində — sadəcə hadisə yaradıldıqda çağırılacaq metodların ardıcıllığıdır. C#-də hadisə çoxlu abunəçiləri dəstəkləyən delegate üzərində qurulur.

Bir abunəlik göndərişini təsəvvür edin: sizin abunəçilər siyahınız var (email ünvanları). Siz göndəriş etdikdə (hadisəni çağırdıqda) bütün abunəçilər məktub alır. Kimsə abunəlikdən çıxarsa, onu siyahıdan çıxarırlar və o artıq məktub almır.

Abunəçi necə əlavə/çıxarılır

Abunəlik (+=) və çıxış (-=) hadisənin daxilindəki delegate ilə işləyir. Budur lambda nümunəsi, onu həm abunə edə, həm də çıxara bilərsiniz:

EventHandler<WorkCompletedEventArgs> handler = (sender, e) =>
{
    Console.WriteLine($"[Lyambda] İş tamamlandı: {e.Message}");
};

worker.WorkCompleted += handler; // Abunə oluruq
worker.WorkCompleted -= handler; // Abunəlikdən çıxırıq

Adi metodlar üçün çıxış eyni görünür:

worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted -= listener.OnWorkCompleted;

Diqqət yetirin: əgər eyni metodu bir neçə dəfə abunə etmisinizsə, o qədər dəfələrlə çağırılacaq və onu çıxarmaq üçün də -= bir o qədər dəfə çağırılmalıdır.

2. Niyə əl ilə idarə etmək lazımdır?

Abunəçiləri idarə etmək niyə vacibdir?

Real tətbiqlərdə, xüsusən uzunömürlü (məsələn, desktop və ya server) proqramlarda, abunəliklərin səhv idarə olunması yaddaş sızmasına səbəb ola bilər. Əgər abunəçi obyekt artıq lazım deyilsə, amma hadisənin abunəçilər siyahısında asılı qalıbsa, garbage collector onu silməyəcək — çünki hadisənin delegate-dən ona göstərici hələ də mövcuddur.

İllüstrasiya

Əməliyyat Abunəçi üçün nəticə
+= (abunə olundu) Siyahıya əlavə olundu
-= (çıxarıldı) Siyahıdan silindi
Abunəçi obyekti silindi ƏGƏR ÇIXARILMAYIBSA! — SILINMƏYƏCƏK, çünki hadisədə hələ göstərici var
Abunəçi obyekti silindi ƏGƏR ÇIXARILIBSA — normal şəkildə silinəcək

Hadisəyə kim abunədir necə öyrənmək olar?

Hadisələr abunəçilər siyahısını enkapsulyasiya edir, ona görə də yayımçı sinifin xaricindən bu siyahını birbaşa əldə etmək mümkün deyil — yalnız əlavə (+=) və ya çıxarma (-=) edə bilərsiniz.

Amma hadisənin elan edildiyi sinif daxilində, delegate əsaslı hadisə üçün (məsələn, EventHandler) cari abunəçilər siyahısını GetInvocationList() metodu ilə əldə etmək olar:

// Yayımçı sinifin daxilində
if (WorkCompleted != null)
{
    foreach (Delegate subscriber in WorkCompleted.GetInvocationList())
    {
        Console.WriteLine($"Handler: {subscriber.Method.Name}, Ob'yekt: {subscriber.Target}");
    }
}

Bu yanaşma gündəlik inkişafda nadir hallarda lazım olur, amma debug etmək və ya bütün abunəçiləri topluca çıxarmaq üçün faydalı ola bilər.

3. Hadisələrin təhlükəsiz çağırışı: "minalar" və onlardan necə qaçmaq

Hadisəni çağırarkən nə pis ola bilər?

Hər şey sadə görünür: siz çağırırsınız

WorkCompleted?.Invoke(this, args);

və hər şey əla işləyir... Əksər hallarda! Amma incə məqamlar var. Onlar:

1. Çoknövbəlilik təhlükəsi

Çoxnövbəli tətbiqdə belə vəziyyət mümkündür: null yoxlaması ilə handlerləri çağırmaq arasında başqa thread abunəlikləri dəyişdirir. Məsələn:

1) Thread A yoxlayır: WorkCompleted != null.
2) Eyni zamanda thread B hadisədən çıxır (-=), və handler siyahısı boşalır.
3) Thread A WorkCompleted.Invoke(...) çağırmağa çalışır — NullReferenceException yaranır, çünki artıq handler yoxdu.

Bu hadisələrlə işləməkdə klassik data race-dir.

2. Handlerlərdə gözlənilməz istisnalar

Əgər abunəçilərdən biri hadisəni emal edərkən istisna atarsa, digər handlerlərin çağırışı dayandırılır. Yəni hadisə ilk istisnada "qırılır" və qalan abunəçilər xəbərdar olmur. Bunu aradan qaldırmaq üçün hər handler-in çağırışını try-catch ilə qorumaq məsləhətdir, əgər hamısının çağırılması vacibdirsə.

3. İstifadəsiz kontekst sızması

Hadisə handleri tez-tez instans metod olur və abunəçi obyektə (this) istinad tutur. Əgər abunəçi yayıcıdan çıxmağı unutsa, onun istinadı yayıcının delegate siyahısında qalır. Nəticədə garbage collector o obyekti azad edə bilmir — yaddaş sızması yaranır.

Hadisəni necə təhlükəsiz çağırmaq olar?

1) Delegate-i lokal dəyişəndə kopyalamaq

Lokal dəyişəndən çağırış zamanı abunəçilər siyahısının dəyişməyinə zəmanət verilir:

// Köhnə, etibarlı üsul
var handler = WorkCompleted;
if (handler != null)
{
    handler(this, args);
}

Və ya daha modern şəkildə null-conditional operator ilə:

WorkCompleted?.Invoke(this, args);

Çox vaxt bu kifayətdir, çünki C# kompilyatoru bu konstruksiyanı "başa düşür" və daxildə referensiyanı kopyalayır (bax: rəsmi sənədlər).

2) Handler istisnalarından qorunma

Əgər bütün handlerlərin çağırılması kritikdirsə (hətta biri düşsə də), əllə iterasiya istifadə edin:

var handler = WorkCompleted;
if (handler != null)
{
    foreach (EventHandler<WorkCompletedEventArgs> subscriber in handler.GetInvocationList())
    {
        try
        {
            subscriber(this, args);
        }
        catch (Exception ex)
        {
            // Log edirik, amma hadisənin tam çökməsinə imkan vermirik
            Console.WriteLine($"Xətə handler-də: {ex.Message}");
        }
    }
}

Bu variant adi UI-ssenarilər üçün nadir hallarda tələb olunur, amma kitabxanalar, logger-lər və kompleks sistemlərdə faydalıdır.

3) Yaddaş sızmasının qarşısını almaq

Əgər abunəçi yayıcıdan qısaömürlüdürsə (məsələn, pəncərə tətbiqin hadisəsinə abunə olub), o mütləq abunəlikdən çıxmalıdır:

worker.WorkCompleted -= listener.OnWorkCompleted;

Əks halda garbage collector listener-ı azad edə bilməyəcək, hətta onun üzərinə daha heç bir "açıq""referensiya yoxdursa da.

4. Praktik nümunə: toplu abunəlik/çıxış meneceri

Gəlin tədris tətbiqimizi genişləndirək. Tutaq ki, bir neçə listener-imiz var və onları dinamik şəkildə abunə etmək və çıxarmaq istəyirik, proqram işinə görə.

public class WorkListener
{
    private readonly string _name;

    public WorkListener(string name)
    {
        _name = name;
    }

    public void OnWorkCompleted(object sender, WorkCompletedEventArgs e)
    {
        Console.WriteLine($"Listener {_name}: {e.Message}");
    }
}

Əsas proqramda:

var worker = new Worker();

var listeners = new List<WorkListener>
{
    new WorkListener("İvan"),
    new WorkListener("Mariya"),
    new WorkListener("Denis")
};

// Bütün listener-ləri abunə edirik
foreach (var listener in listeners)
    worker.WorkCompleted += listener.OnWorkCompleted;

// Hadisəni çağırırıq
worker.DoWork();

// Toplu çıxış
foreach (var listener in listeners)
    worker.WorkCompleted -= listener.OnWorkCompleted;

// Yoxlayırıq ki, artıq heç kim reaksiya vermir
worker.DoWork();

Konsolda ilk işdən sonra 3 mesaj görünəcək, ikinci işdən sonra isə heç bir mesaj olmayacaq.

5. Hadisələrlə təhlükəsiz işləmək üçün məsləhətlər

  • Əgər abunəçinin life-cycle-i yayıcının life-cycle-indən qısadırsa, vaxtında çıxış edin.
  • Əgər "uzunömürlü yayıcı — qısaömürlü abunəçi" pattern-i tətbiq edirsinizsə, həmişə abunəlikdən çıxın, məsələn, Dispose()-da, pəncərə bağlananda və ya obyektin həyatının sona çatdığı digər yerlərdə.
  • Bir dəfəyə cavab verən hadisələr üçün anonim lambda handler istifadə edib içində özünüzü dərhal çıxara bilərsiniz:
EventHandler<WorkCompletedEventArgs> handler = null;
handler = (s, e) => 
{
    Console.WriteLine("Hadisə bir dəfə emal olundu!");
    worker.WorkCompleted -= handler;
};
worker.WorkCompleted += handler;
  • "Kim abunədir" yoxlaması üçün abunəçilərin referensiyalarını saxlamayın — bu adi biznes-loqikada tələb olunmur. Bu cür yoxlamanı yalnız debug məqsədləri üçün edin.

6. Tez-tez rast gəlinən səhvlər və onlardan necə qaçmaq

Səhv №1: hadisədən çıxmağı unutmusunuz — yaddaş sızması.
Əgər abunəçi çıxmayıbsa, xüsusən çoxlu hadisə və abunəçilərin olduğu böyük tətbiqlərdə obyektlər lazım olandan daha uzun yaddaşda qala bilər. Bu səhv adətən uzun müddət aydın şəkildə görünmür, amma yaddaş istifadəsinin artmasına və performansın pisləşməsinə gətirib çıxarır.

Səhv №2: hadisəni null yoxlamadan çağırmaq.
Əgər hadisədə abunəçi yoxdursa və onu birbaşa çağırmağa çalışsanız, NullReferenceException alacaqsınız. Yeni C# versiyalarında təhlükəsiz çağırış operatoru ?. kömək edir, amma əgər köhnə kodla işləyirsinizsə və ya handler-ləri əllə iterasiya edirsinizsə, null yoxlamasını unutmayın.

Səhv №3: bir handler-də istisna digər handler-lərin çağırılmasını dayandırır.
Əgər handler-lərdən biri istisna atırsa, sonrası çağırılmayacaq. Əgər bütün abunəçilərin xəbərdar olması vacibdirsə, handler-ləri döngü ilə çağırın və hər çağırışı try/catch ilə bükün.

2
Tapşırıq
C# SELF, səviyyə, dərs
Bağlanıb
İstisnaların işlənməsi ilə təhlükəsiz event çağırışı
İstisnaların işlənməsi ilə təhlükəsiz event çağırışı
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION