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 EventHandler və EventHandler<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. add və remove 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.
GO TO FULL VERSION