CodeGym /Kurslar /C# SELF /Delegatlar və eventlərlə bağlı tipik səhvlər

Delegatlar və eventlərlə bağlı tipik səhvlər

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

1. Giriş

C#-da delegatlar və eventlərlə işləmək xoşdur və rahatdır — dil bir çox şeyi sizin üçün edir. Amma bu pərdə arxasında bir çox dönməz tələlər var: görünməyən yaddaş sızmaları, iki dəfə abunəlikdən yaranan qəribə buglar, handlerlərin sinxronizasiyasının pozulması və hətta bildiriş yayımlanarkən gözlənilməz istisnalar. Delegatların və eventlərin potensialı böyükdür, amma obyektlərin həyat dövrü, threadlər və çağırış/imtina mexanizmlərini yaxşı anlamağı tələb edir. Əgər sizin eventləriniz "demək olar ki, həmişə" işləyirsə, amma bəzən işləmir və ya qəribə səhvlər verirsə — tək deyilsiniz! Gəlin tez-tez harada səhv olunduğunu və necə qaçınmağı öyrənək.

Cari səhvlər cədvəli

Səhv Nəticə Necə qaçınmaq
İki dəfə abunə olmaq Handler bir neçə dəfə çağırılır Abunəliyi izləyin, əlavə etməzdən əvvəl çıxarın
Imtina edilməməsi (yaddaş sızması) Subscriber obyektləri yaddaşda qalır, “zombi”lər Həmişə imtina edin, IDisposable istifadə edin
Handlerdə istisna Qalan handlerlər çağırılmayacaq Handlerlərdə try/catch və ya əl ilə dövr etmək
Yayımla müddətində abunəliklərin dəyişdirilməsi Çağırışlar ötürülə və ya təkrarlana bilər Handlerlərin surətini dövr edin (GetInvocationList())
MyEvent == null vəziyyətində çağırış NullReferenceException null yoxlayın, ?.Invoke istifadə edin
Handler imzası uyğun gəlmir Kompilyasiya səhvi İmzanı yoxlayın
Eventi kənardan çağırmaq Kompilyasiya səhvi Yalnız OnEventName vasitəsilə çağırın
Statik eventlər düzgün deyil Abunəliklər qarışır Lazım olmadıqda static etməyin
Lambda closure problemləri Gözlənilməz dəyərlər Variable-nın kopyasını yaradın

2. Çoxlu abunə və çoxlu çağırışlar

Səhvin mahiyyəti

Eyni handleri bir eventə bir neçə dəfə əlavə etsəniz, hər += əməliyyatı metodu delegat sırasına əlavə edir. Nəticədə handler əlavə olunduğu qədər dəfə çağırılacaq.

Necə ortaya çıxır?

Farz edin bir button və onun click handleri var:

Button btn = new Button();
btn.Click += OnButtonClick; // Abunə oluruq
btn.Click += OnButtonClick; // Və yenə abunə oluruq!

İndi hər klikdə OnButtonClick metodu iki dəfə çağırılacaq. Əgər handler daxilində sayğacı yeniləyirsinizsə və ya log əlavə edirsinizsə, nəticələr ikiqat olacaq.

Necə düzəltmək olar?

Çox vaxt təkrarlanan abunəlik kod strukturunun pozulmasından yaranır — məsələn, += bir neçə dəfə çağırıla bilən metoda qoyulub.

  • Abunəliklərin harda baş verdiyini izləyin.
  • +=-ni bir neçə dəfə çağırıla biləcək həyat dövrlərinə qoymayın.
  • Bəzən “unikal” abunə faydalıdır — əlavə etməzdən əvvəl çıxarın:
myEvent -= MyHandler; // Ehtiyat üçün çıxarırıq
myEvent += MyHandler; // Sonra yenidən əlavə edirik

Bu təhlükəsizdir: əgər handler əvvəllər əlavə edilməyibsə, -= heç nə etməyəcək.

3. Zombi-subscriber

Səhvin mahiyyəti

Əgər subscriber uzunömürlü publisher-a abunə olub və imtina etməyibsə, GC onu toplaya bilməz: publisher hələ də handler delegatına referans saxlayır, yəni subscriber obyektinə də. Nəticədə yaddaş sızması yaranır.

Nümunə

public class TemporaryPopup : IDisposable
{
    private Window _hostWindow;
    public TemporaryPopup(Window window)
    {
        _hostWindow = window;
        _hostWindow.Closed += OnHostClosed;
    }

    private void OnHostClosed(object sender, EventArgs e)
    {
        // ...
    }

    public void Dispose()
    {
        _hostWindow.Closed -= OnHostClosed; // İmtina etməyi unutmayın!
    }
}

Əgər Dispose()-u unutmusunuzsa və ya çağırmamısınızsa, bütün TemporaryPopup referanslarını sildikdə belə obyekt məhv olunmayacaq — pencərə hələ də onun handlerinə referans saxlayır.

Necə qaçınmaq olar?

  • Əgər subscriber-in həyat dövrü publisher-dan qısadırsa, IDisposable implement edin.
  • using pattern-i və ya əl ilə Dispose() çağırışı istifadə edin:
using (var popup = new TemporaryPopup(mainWindow))
{
    // ...
} // Burada Dispose avtomatik çağırılacaq

GUI tətbiqlərində — pəncərə/form bağlanarkən imtina edin (məsələn, close handlerlərində və ya formun Dispose()-unda).

4. Handlerlər içində istisnaların işlənməsi

Səhvin mahiyyəti

Event onlarla handleri çağıranda və onlardan biri istisna atırsa, qalan handlerlər çağırılmayacaq.

Nümunə

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent?.Invoke(this, EventArgs.Empty);
}

Əgər MyEvent-ə abunə olan metodlardan birində istisna baş verərsə, qalanları çağırılmayacaq — zəncir kəsiləcək.

Bu vəziyyətlə necə işləmək lazımdır?

  • Event handlerlərdə istisnaları lokal olaraq tutun (try/catch) və ya şüurlu şəkildə istisnaları buraxın.
  • Mürəkkəb scenariylərdə — handlerlər siyahısını əlinizlə dövr edin və hər birini izolə edin:
var handlers = MyEvent?.GetInvocationList();
foreach (var handler in handlers)
{
    try
    {
        handler.DynamicInvoke(this, EventArgs.Empty);
    }
    catch (Exception ex)
    {
        // Loglama, fövqəladə bərpa
    }
}

5. Yayımla müddətində abunə siyahısının dəyişdirilməsi

Səhvin mahiyyəti

Əgər handler öz içindən özünü və ya başqasını imtina edərsə, çağırışların sırası dəyişə və bəzi handlerlər atlana və ya təkrarlana bilər.

Necə qaçınmaq olar?

  • Handlerlərdən abunəlikləri dəyişdirməyin.
  • Əgər lazımdırsa — delegatların surətini dövr edin:
var handlers = MyEvent?.GetInvocationList();
foreach (EventHandler handler in handlers)
{
    handler(this, EventArgs.Empty);
}

6. Delegatın null olması (heç kim abunə olmayıb)

Səhvin mahiyyəti

Heç kim eventə abunə olmayanda delegat null-dır. Yoxlama olmadan çağırış NullReferenceException-a gətirə bilər.

Nümunə (pis)

public event EventHandler MyEvent;

public void Raise()
{
    MyEvent(this, EventArgs.Empty); // Əgər abunəçi yoxdursa — istisna!
}

Necə düzgün?

  • Təhlükəsiz çağırış istifadə edin: MyEvent?.Invoke(this, EventArgs.Empty).
  • Ya da klassik thread-safe yanaşma: delegatı lokal dəyişənə kopyalayıb onu çağırın.

7. Müxtəlif imzalı delegatların qarışması

Səhvin mahiyyəti

Delegatlar tipə görə sərtdir. Handler metodu ilə event delegatının imzası uyğun gəlməzsə — kompilyasiya xətası alınacaq.

Nümunə

public event EventHandler<string> TextChanged;

void WrongHandler(object sender, int number) { /* ... */ }

TextChanged += WrongHandler; // Kompilyasiya xətası!

Standart delegatlardan EventHandlerEventHandler<T> istifadə edin və imzaların tam uyğunluğunu yoxlayın.

8. Publisher sinfindən kənarda eventi çağırmağa cəhd

Səhvin mahiyyəti

event — inkapsulyasiya olunmuş delegatdır: xarici kod onu birbaşa çağırmaq imkanına malik deyil (yalnız əlavə/çıxarma mümkün).

Nümunə

public class MyPublisher
{
    public event EventHandler SomethingHappened;
}

var publisher = new MyPublisher();
publisher.SomethingHappened?.Invoke(publisher, EventArgs.Empty); // Səhv!

Necə düzgün?

Çağırış adətən publisher-in qorunan/açıq metodu vasitəsilə edilir — məsələn, OnEventName. Xaricdən isə yalnız +=/-= mümkündür.

9. addremove aksesorlarda səhvlər

Səhvin mahiyyəti

Custom aksesorlara malik eventlər abunəliyi idarə etməyə imkan verir, amma çağırış sırasını və thread-safety-ni asanlıqla poza bilərsiniz.

Nümunə

public event EventHandler MyEvent
{
    add { /* ... */ }
    remove { /* ... */ }
}

Əgər əminsinizsə — standart event implementasiyasından istifadə edin. Əl ilə implementasiya edirsinizsə, sənədləşməni və sinxronizasiyanı nəzərə alın.

10. Statik və nümunə kontekstlərindən eventlərə giriş

Səhvin mahiyyəti

Asanlıqla eventi static elan etmək olar, halbuki o nümunəyə məxsus olmalı idi. Bu halda bütün obyektlər abonent sırasını paylaşacaq.

Nümunə

public static event EventHandler GlobalEvent; // Ups!
// Nümunələr fərdiliyini itirir, abunəliklər bir yerə toplanır

Necə qaçınmaq olar?

Eventi yalnız qlobal səviyyəli ehtiyaclar üçün statik edin (məsələn, qlobal log). Əks halda — nümunədə inkapsulyasiya edin.

11. Lambda ifadələrində dəyişənlərin capture olunması

Səhvin mahiyyəti

Lambda ifadələri dəyişənləri referansla capture edir. Dövrlərdə bu tez-tez “sonuncu dəyər” problem yaradır.

Nümunə

for (int i = 0; i < 5; i++)
{
    button.Click += (s, e) => Console.WriteLine(i);
}
// Bütün handlerlər "5" çap edəcək

Necə düzgün?

for (int i = 0; i < 5; i++)
{
    int copy = i; // Lokal kopya
    button.Click += (s, e) => Console.WriteLine(copy);
}

12. Zəif və güclü referansların qarışdırılması: Advanced “Weak Events”

Böyük tətbiqlərdə (məsələn, WPF) publisher subscriber-ə zəif referans saxlayan "weak events" mexanizmi istifadə edilir ki, GC-ə mane olmasın. Weak eventlər yaddaş sızmalarına qarşı kömək edir, amma subscriber toplanıb eventi ala bilməyə bilər.

Ətraflı: Weak Event Patterns (MSDN)

13. Adlandırma və event imzaları standartının olmaması

Səhvin mahiyyəti

Standart imza və adlara riayət edin: event adları keçmiş zamanda olsun (Changed, Closed, Completed), argümentlər EventArgs-dən törəmə olmalıdır.

“Yaxşı deyil” nümunəsi:

public delegate void SomethingHappens(int what);
// ...
public event SomethingHappens Something;

“Düzgün” nümunə:

public event EventHandler<EventArgs> SomethingHappened;

Öz eventləriniz üçün demək olar ki, həmişə EventHandler və ya EventHandler<T> istifadə edin. Komanda yoldaşlarınız (və gələcəkdə özünüz) buna görə təşəkkür edəcək.

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