11-Bob: Hodisalar (Events)
Hodisalarni aniqlash, kompilyator hodisalarni qanday amalga oshirishi, hodisalarni tinglash va aniq (explicit) hodisa amalga oshirish
Ushbu bobda biz turlar aniqlay oladigan a'zolarning oxirgi turini ko'rib chiqamiz: hodisalar (events). Hodisa a'zosini aniqlaydigan tur (yoki tur instansiyalari) boshqa obyektlarga biror maxsus narsa sodir bo'lganligi haqida xabar berish imkonini beradi. Masalan, Button klassi Click deb nomlangan hodisani taqdim etadi. Button obyekti bosilganda, ilovadagi bir yoki bir nechta obyekt bu hodisa haqida xabarnoma olishni va biror amal bajarishni xohlashi mumkin. Hodisalar (events) bu o'zaro aloqani ta'minlaydigan tur a'zolaridir. Aniqrog'i, hodisa a'zosini aniqlash turning quyidagi imkoniyatlarni taqdim etishini anglatadi:
- Metod hodisaga o'z qiziqishini ro'yxatga olishi (register) mumkin.
- Metod hodisaga bo'lgan qiziqishini bekor qilishi (unregister) mumkin.
- Hodisa sodir bo'lganda ro'yxatga olingan metodlarga xabarnoma yuboriladi.
Turlar bu funksionallikni taqdim eta oladi, chunki ular ro'yxatga olingan metodlar ro'yxatini saqlaydi. Hodisa sodir bo'lganda, tur to'plamdagi barcha ro'yxatga olingan metodlarni xabardor qiladi.
CLR ning hodisalar modeli delegatlarga (delegates) asoslangan. Delegat — bu callback metodini turga xavfsiz tarzda chaqirish usuli. Callback metodlari — obyektlar obuna bo'lgan xabarnomalarni qabul qilish vositasidir. Ushbu bobda men delegatlardan foydalanaman, lekin ularning barcha tafsilotlarini 17-bob "Delegatlar" da to'liq tushuntiraman.
CLR da hodisalarning qanday ishlashini to'liq tushunishingizga yordam berish uchun, men hodisalar foydali bo'ladigan stsenariydan boshlayman. Tasavvur qiling, siz elektron pochta ilovasini loyihalashni xohlaysiz. Yangi elektron pochta xabari kelganda, foydalanuvchi xabarni faks apparatiga yoki peyjerga yo'naltirishni xohlashi mumkin. Ushbu ilovani arxitekturalash uchun, keling, kiruvchi elektron pochta xabarlarini qabul qiladigan MailManager deb nomlangan turni loyihalaylik. MailManager NewMail deb nomlangan hodisani oshkor qiladi (expose). Boshqa turlar (masalan, Fax va Pager) ushbu hodisaga qiziqish bildirishi mumkin. MailManager yangi elektron pochta xabarini qabul qilganda, u hodisani ko'taradi (raise) va bu xabar ro'yxatga olingan barcha obyektlarga tarqatiladi. Har bir obyekt xabarni o'zi xohlagancha qayta ishlashi mumkin.
Ilova ishga tushganda, keling, bitta MailManager instansiyasini yarataylik — ilova keyin istalgancha Fax va Pager turlarini yaratishi mumkin. 11-1-rasm ilova qanday ishga tushishini va yangi elektron pochta xabari kelganda nima sodir bo'lishini ko'rsatadi.
- Fax obyektidagi metod MailManager hodisasiga qiziqishini ro'yxatga oladi.
- Pager obyektidagi metod MailManager hodisasiga qiziqishini ro'yxatga oladi.
- Yangi pochta xabari MailManager ga keladi.
- MailManager obyekti barcha ro'yxatga olingan metodlarga xabarnoma yuboradi va ular pochta xabarini xohlagancha qayta ishlaydi.
11-1-rasmda tasvirlangan ilova quyidagicha ishlaydi: ilova MailManager instansiyasini yaratish orqali ishga tushadi. MailManager NewMail hodisasini taqdim etadi. Fax va Pager obyektlari yaratilganda, ular o'zlarining instansiya metodini MailManager ning NewMail hodisasi bilan ro'yxatga oladi, shunda MailManager yangi elektron pochta xabarlari kelganda Fax va Pager obyektlarini xabardor qilishni biladi. Endi MailManager yangi elektron pochta xabarini qabul qilganda (kelajakda qaysidir vaqtda), u NewMail hodisasini ko'taradi va barcha ro'yxatga olingan metodlarga yangi xabarni o'zlari xohlagancha qayta ishlash imkoniyatini beradi.
Hodisani Oshkor Qiluvchi Turni Loyihalash
Bir yoki bir nechta hodisa a'zolarini oshkor qiladigan turni aniqlash uchun dasturchi ko'plab qadamlarni bajarishi kerak. Ushbu bo'limda men har bir zaruriy qadamni bosqichma-bosqich ko'rib chiqaman. MailManager namunali ilovasi (uni Resources bo'limidan http://wintellect.com/Books manzilidan yuklab olish mumkin) MailManager turi, Fax turi va Pager turining barcha manba kodini ko'rsatadi. Pager turi deyarli Fax turiga o'xshash ekanligini sezasiz.
1-qadam: Hodisa xabarnomasi qabul qiluvchilarga yuborilishi kerak bo'lgan qo'shimcha ma'lumotni saqlaydigan turni aniqlash
Hodisa ko'tarilganda, hodisani ko'tarayotgan obyekt hodisa xabarnomasini qabul qilayotgan obyektlarga ba'zi qo'shimcha ma'lumot uzatishni xohlashi mumkin. Bu qo'shimcha ma'lumot o'z klassiga joylashtirilishi kerak, bu klass odatda bir nechta xususiy maydonlarni va ularni ochish uchun bir nechta faqat-o'qish uchun ommaviy xossalarni o'z ichiga oladi. Kelishuv bo'yicha, hodisa ma'lumotini saqlaydigan klasslar System.EventArgs dan hosil bo'lishi kerak va klass nomi EventArgs bilan tugashi kerak. Ushbu misolda NewMailEventArgs klassi xabarni kim yuborgan (m_from), kim qabul qilayotgan (m_to) va xabar mavzusi (m_subject) ni aniqlaydigan maydonlarga ega.
// 1-qadam: Hodisa xabarnomasi qabul qiluvchilarga yuborilishi kerak bo'lgan
// har qanday qo'shimcha ma'lumotni saqlaydigan turni aniqlash
internal class NewMailEventArgs : EventArgs {
private readonly String m_from, m_to, m_subject;
public NewMailEventArgs(String from, String to, String subject) {
m_from = from; m_to = to; m_subject = subject;
}
public String From { get { return m_from; } }
public String To { get { return m_to; } }
public String Subject { get { return m_subject; } }
}
EventArgs klassi Microsoft .NET Framework Class Library (FCL) da aniqlangan bo'lib, quyidagicha amalga oshirilgan:
[ComVisible(true), Serializable]
public class EventArgs {
public static readonly EventArgs Empty = new EventArgs();
public EventArgs() { }
}
Ko'rib turganingizdek, bu tur haqida yozadigan maxsus narsa yo'q. U shunchaki boshqa turlar hosil bo'lishi mumkin bo'lgan bazaviy tur sifatida xizmat qiladi. Ko'pgina hodisalar hech qanday qo'shimcha ma'lumot uzatishi shart emas. Masalan, Button ro'yxatga olingan qabul qiluvchilarga bosilganligini xabar berganda, shunchaki callback metodini chaqirish yetarli ma'lumotdir. Uzatiladigan qo'shimcha ma'lumoti bo'lmagan hodisani aniqlashda, yangi EventArgs obyekt yaratish o'rniga, shunchaki EventArgs.Empty dan foydalaning.
2-qadam: Hodisa a'zosini aniqlash
Hodisa a'zosi C# ning event kalit so'zi yordamida aniqlanadi. Har bir hodisa a'zosiga kirish darajasi beriladi (bu deyarli har doim public bo'ladi, shunda boshqa kod hodisaga murojaat qilishi mumkin), chaqiriladigan metod(lar) prototipini ko'rsatuvchi delegat turi va nom (istalgan to'g'ri identifikator bo'lishi mumkin). Mana MailManager klassimizdagi hodisa a'zosi qanday ko'rinishda:
internal class MailManager {
// 2-qadam: Hodisa a'zosini aniqlash
public event EventHandler<NewMailEventArgs> NewMail;
...
}
NewMail — bu hodisa nomi. Hodisa turi EventHandler<NewMailEventArgs> bo'lib, bu hodisa xabarnomasi qabul qiluvchilarning barchasi EventHandler<NewMailEventArgs> delegat turiga mos keladigan callback metodini taqdim etishi kerakligini anglatadi. Generik System.EventHandler delegati quyidagicha aniqlangan:
public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e);
shuning uchun metod prototiplari quyidagicha ko'rinishi kerak:
void MethodName(Object sender, NewMailEventArgs e);
Ko'pchilik hodisa pattern nima uchun sender parametri har doim Object turiga ega bo'lishi kerakligini so'raydi. Axir, MailManager NewMailEventArgs bilan hodisa ko'taradigan yagona tur bo'lgani uchun, callback metodi quyidagicha prototipga ega bo'lsa ma'noliroq bo'lar edi:
void MethodName(MailManager sender, NewMailEventArgs e);
Pattern sender parametrining Object turida bo'lishini asosan meros (inheritance) sabablarga ko'ra talab qiladi. Agar MailManager SmtpMailManager uchun bazaviy klass sifatida ishlatilsa nima bo'ladi? Bu holda callback metod sender parametrini SmtpMailManager sifatida prototiplashi kerak edi, lekin SmtpMailManager shunchaki NewMail hodisasini meros olgan bo'lgani uchun bu mumkin emas. Shunday qilib, hodisani ko'tarishni kutgan kod baribir sender argumentini SmtpMailManager ga cast qilishi kerak bo'ladi. Cast baribir talab qilingani uchun, sender parametri Object turida bo'lishi maqsadga muvofiq.
Keyingi sabab — bu shunchaki moslashuvchanlik. Bu delegatning bir nechta turlar tomonidan ishlatilishiga imkon beradi. Masalan, PopMailManager klassi ham NewMailEventArgs uzatadigan hodisa taqdim etishi mumkin, hatto bu klass MailManager dan hosil bo'lmagan bo'lsa ham.
Hodisa patterni shuningdek delegat ta'rifi va callback metodi EventArgs dan hosil bo'lgan parametr nomini e deb nomlashni talab qiladi. Bu faqat patternning izchilligini oshirish uchun bo'lib, dasturchilar va kod yaratish vositalariga (masalan, Microsoft Visual Studio) parametrni e deb chaqirishni bilishni osonlashtiradi.
Nihoyat, hodisa patterni barcha hodisa ishlov beruvchilarining qaytarish turini void bo'lishini talab qiladi. Bu zarurdir, chunki hodisa ko'tarish bir nechta callback metodlarni chaqirishi mumkin va ularning barchasidan qaytarish qiymatlarini olishning iloji yo'q. void qaytarish turi callback metodlarga qiymat qaytarishga ruxsat bermaydi. Afsuski, FCL da Microsoft ning o'z belgilagan patterniga amal qilmagan ba'zi hodisa ishlov beruvchilari bor, masalan, ResolveEventHandler, chunki u Assembly turidagi obyektni qaytaradi.
3-qadam: Hodisa sodir bo'lganligini ro'yxatga olingan obyektlarga xabar berish uchun mas'ul bo'lgan metodni aniqlash
Kelishuv bo'yicha, klass ichida va uning hosil bo'lgan (derived) klasslarida ichki ravishda chaqiriladigan protected, virtual metod aniqlanishi kerak. Bu metod bitta parametr qabul qiladi — NewMailEventArgs obyekti, u xabarnomani qabul qilayotgan obyektlarga uzatiladigan ma'lumotni o'z ichiga oladi. Ushbu metodning standart amalga oshirilishi hodisaga biror obyekt qiziqish bildirganligini tekshiradi va agar shunday bo'lsa, hodisani ko'tarib, hodisa sodir bo'lganligini ro'yxatga olingan metodlarga xabar beradi. Mana MailManager klassimizdagi ushbu metod qanday ko'rinishda:
internal class MailManager {
...
// 3-qadam: Hodisani ko'tarish uchun mas'ul bo'lgan metodni aniqlash
// ro'yxatga olingan obyektlarga hodisa sodir bo'lganligini xabar berish
// Agar bu klass sealed bo'lsa, bu metodni private va novirtual qiling
protected virtual void OnNewMail(NewMailEventArgs e) {
// Delegat maydoniga havolani thread xavfsizligi uchun
// vaqtinchalik maydonga nusxalash
EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
// Agar bizning hodisaga qiziqish bildirgan metodlar bo'lsa, ularni xabardor qiling
if (temp != null) temp(this, e);
}
...
}
Hodisani Thread-Xavfsiz Tarzda Ko'tarish
.NET Framework birinchi marta chiqarilganda, hodisani ko'tarish uchun dasturlarga tavsiya etilgan usul quyidagiga o'xshash kod ishlatish edi:
// 1-versiya
protected virtual void OnNewMail(NewMailEventArgs e) {
if (NewMail != null) NewMail(this, e);
}
OnNewMail metodining muammosi shundaki, oqim (thread) NewMail null emasligini ko'rishi mumkin va keyin, NewMail ni chaqirishdan oldin, boshqa oqim zanjirdan delegatni olib tashlashi va NewMail ni null qilishi mumkin, natijada NullReferenceException tashlanadi. Bu poyga holatini (race condition) bartaraf etish uchun ko'p dasturchilar OnNewMail metodini quyidagicha yozadi:
// 2-versiya
protected virtual void OnNewMail(NewMailEventArgs e) {
EventHandler<NewMailEventArgs> temp = NewMail;
if (temp != null) temp(this, e);
}
Bu yerdagi fikr shundaki, NewMail ga havola temp vaqtinchalik o'zgaruvchisiga nusxalanadi, u tayinlash amalga oshirilgan paytdagi delegatlar zanjiriga ishora qiladi. Endi bu metod temp va null ni taqqoslaydi va temp ni chaqiradi, shuning uchun boshqa oqim tayinlashdan keyin NewMail ni o'zgartirsa ham muhim emas. Esda tutingki, delegatlar o'zgarmas (immutable) va shu sababli bu texnika nazariy jihatdan ishlaydi. Biroq, ko'p dasturchilar bilmagan narsa shundaki, bu kod kompilyator tomonidan lokal temp o'zgaruvchisini butunlay olib tashlash orqali optimizatsiya qilinishi mumkin. Agar bu sodir bo'lsa, bu versiya birinchi versiya bilan bir xil bo'lib qoladi va NullReferenceException hali ham mumkin.
Buni haqiqatan ham to'g'ri tuzatish uchun, OnNewMail ni quyidagicha qayta yozish kerak:
// 3-versiya
protected virtual void OnNewMail(NewMailEventArgs e) {
EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
if (temp != null) temp(this, e);
}
Volatile.Read chaqiruvi NewMail ni chaqiruv nuqtasida o'qishga va havolani haqiqatan ham temp o'zgaruvchisiga nusxalashga majbur qiladi. Keyin temp faqat null bo'lmagan taqdirda chaqiriladi. Volatile.Read metodi haqida ko'proq ma'lumot uchun 29-bob "Primitiv Thread Sinxronizatsiya Konstruksiyalari" ga qarang.
Garchi oxirgi versiya eng to'g'ri bo'lsa-da, siz aslida ikkinchi versiyani ishlatishingiz mumkin, chunki just-in-time (JIT) kompilyatori bu patterndan xabardor va lokal temp o'zgaruvchisini optimizatsiya qilib olib tashlamasligini biladi. Aniqrog'i, Microsoft ning barcha JIT kompilyatorlari heap xotirasiga yangi o'qishlarni kiritmaslik va shuning uchun heap havolasini lokal o'zgaruvchida keshlash heap havolasiga faqat bir marta murojaat qilinishini ta'minlash invariantini hurmat qiladi. Bu hujjatlashtirilmagan va nazariy jihatdan o'zgarishi mumkin, shuning uchun oxirgi versiyani ishlatish kerak. Lekin amalda Microsoft ning JIT kompilyatori bu patternni buzadigan o'zgarishni hech qachon qilmaydi, chunki juda ko'p ilovalar buzilishi mumkin.1 Bundan tashqari, hodisalar ko'pincha bitta oqimli stsenariylarda ishlatiladi (Windows Presentation Foundation va Windows Store ilovalari) va shuning uchun oqim xavfsizligi baribir muammo emas.
Bu oqim poyga holati tufayli, metod hodisaning delegat zanjiridan olib tashlangandan keyin ham chaqirilishi mumkin ekanligini ta'kidlash juda muhim.
Qulaylik uchun, siz 8-bob "Metodlar" da muhokama qilinganidek, bu thread-xavfsizlik mantiqini inkapsulatsiya qiladigan kengaytma metodi (extension method) aniqlay olasiz. Kengaytma metodini quyidagicha aniqlang:
public static class EventArgExtensions {
public static void Raise<TEventArgs>(this TEventArgs e,
Object sender, ref EventHandler<TEventArgs> eventDelegate) {
// Delegat maydoniga havolani thread xavfsizligi uchun
// vaqtinchalik maydonga nusxalash
EventHandler<TEventArgs> temp = Volatile.Read(ref eventDelegate);
// Agar bizning hodisaga qiziqish bildirgan metodlar bo'lsa, ularni xabardor qiling
if (temp != null) temp(sender, e);
}
}
Va endi OnNewMail metodini quyidagicha qayta yozishimiz mumkin:
protected virtual void OnNewMail(NewMailEventArgs e) {
e.Raise(this, ref m_NewMail);
}
MailManager ni bazaviy tur sifatida ishlatadigan klass OnNewMail metodini bekor qilishi (override) mumkin. Bu imkoniyat hosil bo'lgan klassga hodisa ko'tarilishi ustidan nazorat beradi. Hosil bo'lgan klass yangi email xabarini o'zi xohlagancha qayta ishlashi mumkin. Odatda, hosil bo'lgan tur bazaviy turning OnNewMail metodini chaqiradi, shunda ro'yxatga olingan metod(lar) xabarnomani oladi. Biroq, hosil bo'lgan klass hodisani yo'naltirishni taqiqlashga qaror qilishi ham mumkin.
4-qadam: Kirishni (input) kerakli hodisaga tarjima qiladigan metodni aniqlash
Sizning klassingiz biror kirishni qabul qiluvchi va uni hodisa ko'tarishga tarjima qiladigan metodga ega bo'lishi kerak. MailManager misolimizda SimulateNewMail metodi MailManager ga yangi elektron pochta xabari kelganligini bildirish uchun chaqiriladi.
internal class MailManager {
// 4-qadam: Kirishni kerakli hodisaga tarjima qiladigan
// metodni aniqlash
public void SimulateNewMail(String from, String to, String subject) {
// Xabarnomamiz qabul qiluvchilarga uzatishni xohlagan
// ma'lumotni saqlaydigan obyektni yaratish
NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
// Hodisa sodir bo'lganligini obyektimizga xabar beradigan
// virtual metodimizni chaqirish. Agar hech qanday tur
// bu metodni bekor qilmasa, bizning obyektimiz hodisaga
// qiziqish bildirgan barcha obyektlarga xabar beradi
OnNewMail(e);
}
}
SimulateNewMail xabar haqidagi ma'lumotni qabul qiladi va NewMailEventArgs obyektini yaratadi, xabar ma'lumotini konstruktoriga uzatadi. Keyin MailManager ning o'ziga xos OnNewMail virtual metodi yangi elektron pochta xabari haqida MailManager obyektini rasman xabardor qilish uchun chaqiriladi. Odatda, bu hodisani ko'tarishga sabab bo'lib, barcha ro'yxatga olingan metodlarni xabardor qiladi. (Yuqorida aytib o'tilganidek, MailManager ni bazaviy klass sifatida ishlatadigan klass bu xatti-harakatni bekor qilishi mumkin.)
Kompilyator Hodisani Qanday Amalga Oshiradi
Endi siz hodisa a'zosini taqdim etadigan klassni qanday aniqlashni bilganingizdan so'ng, keling, hodisa (event) aslida nima ekanligini va u qanday ishlashini yaqinroq ko'rib chiqaylik. MailManager klassida hodisa a'zosini aniqlaydigan kod quyidagi ko'rinishga ega:
public event EventHandler<NewMailEventArgs> NewMail;
C# kompilyator yuqoridagi bitta satrni kompilyatsiya qilganda, u uni quyidagi uchta konstruksiyaga tarjima qiladi:
// 1. null ga initsializatsiya qilingan XUSUSIY (private) delegat maydoni
private EventHandler<NewMailEventArgs> NewMail = null;
// 2. OMMAVIY add_Xxx metodi (bu yerda Xxx hodisa nomi)
// Hodisaga qiziqishni ro'yxatga olish imkonini beradi.
public void add_NewMail(EventHandler<NewMailEventArgs> value) {
// Tsikl va CompareExchange chaqiruvi hodisaga delegatni
// thread-xavfsiz tarzda qo'shishning go'zal usuli
EventHandler<NewMailEventArgs> prevHandler;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do {
prevHandler = newMail;
EventHandler<NewMailEventArgs> newHandler =
(EventHandler<NewMailEventArgs>) Delegate.Combine(prevHandler, value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(
ref this.NewMail, newHandler, prevHandler);
} while (newMail != prevHandler);
}
// 3. OMMAVIY remove_Xxx metodi (bu yerda Xxx hodisa nomi)
// Hodisaga qiziqishni bekor qilish imkonini beradi.
public void remove_NewMail(EventHandler<NewMailEventArgs> value) {
// Tsikl va CompareExchange chaqiruvi hodisadan delegatni
// thread-xavfsiz tarzda olib tashlashning go'zal usuli
EventHandler<NewMailEventArgs> prevHandler;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do {
prevHandler = newMail;
EventHandler<NewMailEventArgs> newHandler =
(EventHandler<NewMailEventArgs>) Delegate.Remove(prevHandler, value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(
ref this.NewMail, newHandler, prevHandler);
} while (newMail != prevHandler);
}
Birinchi konstruksiya shunchaki tegishli delegat turining maydoni. Bu maydon hodisa sodir bo'lganda xabar beriladigan delegatlar ro'yxatining boshiga (head) ishora qiladi. Bu maydon null ga initsializatsiya qilingan bo'lib, bu hodisaga hech qanday tinglovchi (listener) ro'yxatga olinmaganligini anglatadi. Metod hodisaga qiziqish bildirsa, bu maydon EventHandler<NewMailEventArgs> delegatining instansiyasiga ishora qiladi, u esa qo'shimcha EventHandler<NewMailEventArgs> delegatlariga ishora qilishi mumkin. Tinglovchi hodisaga qiziqish bildirsa, tinglovchi shunchaki delegat turidagi instansiyani ro'yxatga qo'shadi. Tabiiyki, ro'yxatdan chiqarish delegatni ro'yxatdan olib tashlashni anglatadi.
E'tibor bering, delegat maydoni, NewMail, bu misolda har doim private bo'ladi, garchi asl manba kodi hodisani public deb aniqlagan bo'lsa ham. Delegat maydonini private qilishning sababi — aniqlovchi klassdan tashqaridagi kodning maydondagi qiymatni noto'g'ri manipulyatsiya qilishining oldini olish. Agar maydon public bo'lganida, istalgan kod maydondagi qiymatni o'zgartirishi va hodisaga qiziqish bildirgan barcha delegatlarni potensial ravishda o'chirib tashlashi mumkin edi.
C# kompilyator tomonidan yaratiladigan ikkinchi konstruksiya — boshqa obyektlarga hodisaga qiziqishlarini ro'yxatga olish imkonini beruvchi metod. C# kompilyator bu funksiyani hodisa nomiga add_ prefiksini qo'shib avtomatik nomlaydi (ya'ni add_NewMail). Bu metod ichidagi kod har doim System.Delegate ning statik Combine metodini chaqiradi, u delegat instansiyasini delegatlar ro'yxatiga qo'shadi va ro'yxatning yangi boshini (head) qaytaradi, bu esa qaytadan maydonga saqlanadi.
C# kompilyator tomonidan yaratiladigan uchinchi konstruksiya — obyektga hodisaga qiziqishini bekor qilish imkonini beruvchi metod. Yana, C# kompilyator bu funksiyani hodisa nomiga remove_ prefiksini qo'shib avtomatik nomlaydi (ya'ni remove_NewMail). Bu metod ichidagi kod har doim Delegate ning statik Remove metodini chaqiradi, u delegat instansiyasini delegatlar ro'yxatidan olib tashlaydi va ro'yxatning yangi boshini qaytaradi, bu qaytadan maydonga saqlanadi.
Agar siz hech qachon qo'shilmagan metodni olib tashlashga urinishingiz, Delegate ning Remove metodi ichki ravishda hech narsa qilmaydi. Ya'ni, siz hech qanday istisno yoki ogohlantirish olmaysiz; hodisaning metodlar to'plami o'zgarishsiz qoladi.
add va remove metodlari qiymatni thread-xavfsiz tarzda yangilash uchun taniqli patterndan foydalanadi. Bu pattern 29-bob "Interlocked Anything Pattern" bo'limida muhokama qilinadi.
Ushbu misolda add va remove metodlari publicdir. Buning sababi — asl manba kodidagi satr hodisani public deb e'lon qilgan. Agar hodisa protected deb e'lon qilingan bo'lsa, kompilyator tomonidan yaratilgan add va remove metodlari ham protected deb e'lon qilingan bo'lar edi. Shunday qilib, turda hodisani aniqlaganingizda, hodisaning kirish darajasi qaysi kod hodisaga qiziqish bildirishi va bekor qilishi mumkinligini belgilaydi, lekin faqat turning o'zi delegat maydoniga to'g'ridan-to'g'ri murojaat qilishi mumkin. Hodisa a'zolari static yoki virtual sifatida ham e'lon qilinishi mumkin, bu holda kompilyator tomonidan yaratilgan add va remove metodlari mos ravishda static yoki virtual bo'ladi.
Yuqorida aytilgan uchta konstruksiyani chiqarishga qo'shimcha ravishda, kompilyatorlar boshqariladigan assembliyaning metama'lumotlariga (metadata) hodisa ta'rifi yozuvini (event definition entry) ham chiqaradi. Bu yozuv ba'zi bayroqchalarni (flags), asosiy delegat turini va add hamda remove kirish (accessor) metodlarini o'z ichiga oladi. Bu ma'lumot shunchaki "hodisa" ning mavhum tushunchasi va uning accessor metodlari o'rtasida aloqa o'rnatish uchun mavjud. Kompilyatorlar va boshqa vositalar bu metama'lumotdan foydalanishi mumkin va bu ma'lumotni System.Reflection.EventInfo klassidan foydalanib ham olish mumkin. Biroq, CLR ning o'zi bu metama'lumotdan foydalanmaydi va faqat accessor metodlarini talab qiladi.
Hodisani Tinglovchi Turni Loyihalash
Eng qiyin ish allaqachon ortda qoldi. Ushbu bo'limda men sizga boshqa tur tomonidan taqdim etilgan hodisadan foydalanadigan turni qanday aniqlashni ko'rsataman. Keling, Fax turi uchun kodni ko'rib chiqishdan boshlamiz:
internal sealed class Fax {
// MailManager obyektini konstruktorga uzatish
public Fax(MailManager mm) {
// EventHandler<NewMailEventArgs> delegat instansiyasini
// yaratish, u bizning FaxMsg callback metodimizga ishora qiladi.
// Callbackimizni MailManager ning NewMail hodisasi bilan ro'yxatga olish
mm.NewMail += FaxMsg;
}
// Bu metodni MailManager yangi email xabar
// kelganda chaqiradi
private void FaxMsg(Object sender, NewMailEventArgs e) {
// 'sender' MailManager obyektini aniqlaydi, agar biz
// u bilan aloqa qilmoqchi bo'lsak.
// 'e' MailManager biz uchun taqdim etmoqchi bo'lgan
// qo'shimcha hodisa ma'lumotini aniqlaydi.
// Odatda, bu yerdagi kod email xabarini faks qiladi.
// Bu test amalga oshirish konsolda ma'lumotni ko'rsatadi
Console.WriteLine("Faxing mail message:");
Console.WriteLine(" From={0}, To={1}, Subject={2}",
e.From, e.To, e.Subject);
}
// Bu metod Fax obyektining o'zini hodisadan
// ro'yxatdan chiqarishi uchun ishlatilishi mumkin,
// shunda u endi xabarnomalar olmaydi
public void Unregister(MailManager mm) {
// MailManager ning NewMail hodisasidan ro'yxatdan chiqarish
mm.NewMail -= FaxMsg;
}
}
Elektron pochta ilovasi ishga tushganida, u avval MailManager obyektini yaratadi va havolani o'zgaruvchida saqlaydi. Keyin ilova Fax obyektini yaratadi, MailManager obyektiga havolani parametr sifatida uzatadi. Fax konstruktorida Fax obyekti MailManager ning NewMail hodisasiga qiziqishini C# ning += operatori yordamida ro'yxatga oladi:
mm.NewMail += FaxMsg;
C# kompilyator hodisalar uchun ichki qo'llab-quvvatlashga ega bo'lgani uchun, kompilyator += operatoridan foydalanishni quyidagi kod satriga tarjima qiladi, u hodisaga obyektning qiziqishini qo'shadi:
mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));
Ko'rib turganingizdek, C# kompilyator Fax klassining FaxMsg metodini o'rab oladigan EventHandler<NewMailEventArgs> delegat obyektini yaratadigan kodni generatsiya qilmoqda. Keyin C# kompilyator MailManager ning add_NewMail metodini chaqirib, unga yangi delegatni uzatadi. Albatta, siz bularning barchasini kodni kompilyatsiya qilib va ILDasm.exe kabi vosita yordamida IL ga qarab tekshirishingiz mumkin.
Hatto hodisalarni to'g'ridan-to'g'ri qo'llab-quvvatlamaydigan dasturlash tilidan foydalanayotgan bo'lsangiz ham, hodisaga delegatni add accessor metodini aniq chaqirish orqali ro'yxatga olishingiz mumkin. Natija bir xil; faqat manba kodi unchalik chiroyli ko'rinmaydi. Delegatni hodisaga ro'yxatga oladigan narsa aynan add metodi bo'lib, u delegatni hodisaning delegatlar ro'yxatiga qo'shadi.
MailManager obyekti hodisani ko'targanda, Fax obyektining FaxMsg metodi chaqiriladi. Metodga birinchi parametr sifatida MailManager obyektiga havola, sender, uzatiladi. Ko'pincha bu parametr e'tiborga olinmaydi, lekin agar Fax obyekti hodisa xabarnomasiga javoban MailManager obyektining a'zolariga murojaat qilishni xohlasa, undan foydalanish mumkin. Ikkinchi parametr NewMailEventArgs obyektiga havoladir. Bu obyekt MailManager va NewMailEventArgs dizayneri hodisa qabul qiluvchilari uchun foydali bo'ladi deb o'ylagan har qanday qo'shimcha ma'lumotni o'z ichiga oladi.
NewMailEventArgs obyektidan FaxMsg metodi xabar yuboruvchisiga, xabar qabul qiluvchisiga va xabar mavzusiga oson murojaat qilish imkoniyatiga ega. Haqiqiy Fax obyektida bu ma'lumot faks yuborilishi kerak bo'lgan joyga uzatiladi. Ushbu misolda ma'lumot shunchaki konsol oynasida ko'rsatiladi.
Agar obyekt hodisa xabarnomalarini qabul qilishga endi qiziqmasa, u o'z qiziqishini bekor qilishi kerak. Masalan, Fax obyekti foydalanuvchi endi o'z emaillarini faksga yo'naltirilishini xohlamasa, NewMail hodisasiga qiziqishini bekor qilishi kerak. Agar obyekt hodisa bilan ro'yxatga olingan metodlaridan biriga ega bo'lsa, obyekt garbage collect qilinishi mumkin emas. (21-bob "Managed Heap va Garbage Collection" da IDisposable haqida ko'proq ma'lumot qarang.)
Hodisadan ro'yxatdan chiqish kodi Fax ning Unregister metodida ko'rsatilgan. Bu metod Fax konstruktoridagi kodga deyarli o'xshash. Yagona farq shundaki, bu kod += o'rniga -= ishlatadi. C# kompilyator -= operatorini ko'rganida hodisaga delegatni ro'yxatdan chiqarish uchun, kompilyator hodisaning remove metodiga chaqiruv chiqaradi:
mm.remove_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));
+= operatori bilan bir xil tarzda, hatto hodisalarni to'g'ridan-to'g'ri qo'llab-quvvatlamaydigan dasturlash tilidan foydalansangiz ham, remove accessor metodini aniq chaqirish orqali delegatni hodisadan ro'yxatdan chiqarishingiz mumkin. remove metodi hodisadan delegatni ro'yxatdan chiqaradi, u uzatilgan metod bilan bir xil metodni o'rab oladigan delegatni topib ro'yxatdan olib tashlaydi. Agar mos keladigan delegat topilsa, mavjud delegat hodisaning delegatlar ro'yxatidan olib tashlanadi. Agar mos keladigan narsa topilmasa, hech qanday xato sodir bo'lmaydi va ro'yxat o'zgarishsiz qoladi.
Aytgancha, C# delegatlarni ro'yxatga qo'shish va olib tashlash uchun += va -= operatorlaridan foydalanishni talab qiladi. Agar add yoki remove metodini aniq chaqirishga harakat qilsangiz, C# kompilyator CS0571 xatosini chiqaradi: cannot explicitly call operator or accessor (operatorni yoki accessorni aniq chaqirish mumkin emas).
Hodisani Aniq (Explicit) Amalga Oshirish
System.Windows.Forms.Control turi taxminan 70 ta hodisani aniqlaydi. Agar Control turi kompilyatorga add va remove accessor metodlari va delegat maydonlarini bilvosita yaratishga ruxsat berish orqali hodisalarni amalga oshirsa, har bir Control obyektida faqat hodisalar uchun 70 ta delegat maydoni bo'lar edi! Ko'pchilik dasturchilar bir nechtaginagina hodisalar bilan shug'ullangani uchun, Control dan hosil bo'lgan turlardan yaratilgan har bir obyekt uchun katta hajmdagi xotira isrof bo'lar edi. Aytgancha, ASP.NET ning System.Web.UI.Control va Windows Presentation Foundation (WPF) ning System.Windows.UIElement turlari ham ko'pchilik dasturchilar ishlatmaydigan ko'plab hodisalarni taqdim etadi.
Ushbu bo'limda men C# kompilyator klass dasturchilariga hodisani aniq (explicit) amalga oshirish imkonini qanday berishi haqida gaplashaman, bu dasturchiiga add va remove metodlari callback delegatlarni qanday boshqarishini nazorat qilishga imkon beradi. Men hodisa delegatlarini samarali saqlash uchun aniq hodisa amalga oshirishdan qanday foydalanish mumkinligini ko'rsataman. Biroq, albatta, turning hodisasini aniq amalga oshirishni xohlashingiz mumkin bo'lgan boshqa stsenariylar ham bor.
Hodisa delegatlarini samarali saqlash uchun, hodisalarni oshkor qiladigan har bir obyekt to'plamni (odatda lug'at, dictionary) saqlaydi, bunda qandaydir hodisa identifikatori kalit sifatida va delegat ro'yxati qiymat sifatida ishlatiladi. Yangi obyekt yaratilganda bu to'plam bo'sh bo'ladi. Hodisaga qiziqish ro'yxatga olinganda, hodisaning identifikatori to'plamdan qidiriladi. Agar hodisa identifikatori mavjud bo'lsa, yangi delegat ushbu hodisa uchun delegatlar ro'yxati bilan birlashtiriladi. Agar hodisa identifikatori to'plamda bo'lmasa, hodisa identifikatori delegat bilan birga qo'shiladi.
Obyekt hodisani ko'tarishi kerak bo'lganda, hodisa identifikatori to'plamdan qidiriladi. Agar to'plamda hodisa identifikatori uchun yozuv bo'lmasa, hodisaga hech kim qiziqish bildirmagan va hech qanday delegat chaqirilishi shart emas. Agar hodisa identifikatori to'plamda bo'lsa, hodisa identifikatori bilan bog'langan delegatlar ro'yxati chaqiriladi. Ushbu dizayn patternini amalga oshirish hodisalarni aniqlaydigan turning dasturchiisi mas'uliyatidadir; turdan foydalanadigan dasturchi hodisalar ichki ravishda qanday amalga oshirilganini bilmaydi.
EventSet Klassi
Mana bu patternni qanday amalga oshirish mumkinligi haqidagi misol. Avval, men hodisalar to'plamini va har bir hodisaning delegat ro'yxatini ifodalovchi EventSet klassini amalga oshirdim:
using System;
using System.Collections.Generic;
// Bu klass EventSet dan foydalanishda biroz ko'proq tur
// xavfsizligi va kod qulayligini ta'minlash uchun mavjud
public sealed class EventKey { }
public sealed class EventSet {
// EventKey -> Delegate xaritalarini saqlash uchun
// ishlatiladigan xususiy lug'at
private readonly Dictionary<EventKey, Delegate> m_events =
new Dictionary<EventKey, Delegate>();
// Agar EventKey -> Delegate xaritasi mavjud bo'lmasa,
// qo'shadi yoki mavjud EventKey ga delegat birlashtiradi
public void Add(EventKey eventKey, Delegate handler) {
Monitor.Enter(m_events);
Delegate d;
m_events.TryGetValue(eventKey, out d);
m_events[eventKey] = Delegate.Combine(d, handler);
Monitor.Exit(m_events);
}
// EventKey dan delegatni olib tashlaydi (agar mavjud bo'lsa) va
// oxirgi delegat olib tashlansa EventKey -> Delegate xaritasini o'chiradi
public void Remove(EventKey eventKey, Delegate handler) {
Monitor.Enter(m_events);
// Call TryGetValue to ensure that an exception is not thrown if
// attempting to remove a delegate from an EventKey not in the set
Delegate d;
if (m_events.TryGetValue(eventKey, out d)) {
d = Delegate.Remove(d, handler);
// Agar delegat qolsa, yangi boshini o'rnatish,
// aks holda EventKey ni o'chirish
if (d != null) m_events[eventKey] = d;
else m_events.Remove(eventKey);
}
Monitor.Exit(m_events);
}
// Ko'rsatilgan EventKey uchun hodisani ko'taradi
public void Raise(EventKey eventKey, Object sender, EventArgs e) {
// Agar EventKey to'plamda bo'lmasa, istisno tashlamaslik kerak
Delegate d;
Monitor.Enter(m_events);
m_events.TryGetValue(eventKey, out d);
Monitor.Exit(m_events);
if (d != null) {
// Lug'at bir nechta turli delegat turlarini o'z ichiga olishi
// mumkin bo'lgani uchun, kompilyatsiya vaqtida delegatga turga
// xavfsiz chaqiruvni yaratish imkoni yo'q. Shuning uchun men
// System.Delegate turining DynamicInvoke metodini chaqiraman,
// callback metod parametrlarini obyektlar massivi sifatida
// uzataman. Ichki ravishda DynamicInvoke chaqirilayotgan
// callback metodining tur xavfsizligini tekshiradi va metodni
// chaqiradi. Agar tur nomuvofiqligi bo'lsa, DynamicInvoke
// istisno tashlaydi.
d.DynamicInvoke(new Object[] { sender, e });
}
}
}
FCL System.Windows.EventHandlersStore deb nomlangan turni aniqlaydi, u mohiyatan mening EventSet klassim bilan bir xil ishni bajaradi. WPF ning turli turlari o'zlarining siyrak (sparse) hodisalar to'plamini saqlash uchun EventHandlersStore turidan foydalanadi. Agar xohlasangiz, FCL ning EventHandlersStore turidan foydalanishingiz mumkin. EventHandlersStore turi va mening EventSet turim o'rtasidagi katta farq shundaki, EventHandlersStore hodisalarga murojaat qilish uchun hech qanday thread-xavfsiz usulni taqdim etmaydi; agar bunga ehtiyoj bo'lsa, EventHandlersStore to'plami atrofida o'zingizning thread-xavfsiz o'ramangizni (wrapper) amalga oshirishingiz kerak.
EventSet dan Foydalanish
Endi men o'zimning EventSet klassimdan foydalanadigan klassni ko'rsataman. Bu klassda EventSet obyektiga ishora qiladigan maydon bor va bu klassning har bir hodisasi aniq (explicit) amalga oshirilgan, shunda har bir hodisaning add metodi belgilangan callback delegatni EventSet obyektida saqlaydi va har bir hodisaning remove metodi belgilangan callback delegatni (agar topilsa) olib tashlaydi.
using System;
// Bu hodisa uchun EventArgs dan hosil bo'lgan turni aniqlash.
public class FooEventArgs : EventArgs { }
public class TypeWithLotsOfEvents {
// To'plamga havolani saqlaydigan xususiy instansiya maydonini aniqlash.
// To'plam Event/Delegate juftliklar to'plamini boshqaradi.
// ESLATMA: EventSet turi FCL ning qismi emas, u mening shaxsiy turim.
private readonly EventSet m_eventSet = new EventSet();
// Himoyalangan xossasi hosil bo'lgan turlarga to'plamga murojaat imkonini beradi.
protected EventSet EventSet { get { return m_eventSet; } }
#region Foo hodisasini qo'llab-quvvatlash uchun kod (qo'shimcha hodisalar uchun takrorlang)
// Foo hodisasi uchun zarur bo'lgan a'zolarni aniqlash.
// 2a. Ushbu hodisani aniqlash uchun statik, faqat o'qish uchun obyekt yaratish.
// Har bir obyekt o'zining hodisa delegat bog'langan ro'yxatini
// qidirish uchun o'z hash kodiga ega.
protected static readonly EventKey s_fooEventKey = new EventKey();
// 2b. Hodisaning to'plamdan delegatni qo'shadigan/olib tashlaydigan
// accessor metodlarini aniqlash.
public event EventHandler<FooEventArgs> Foo {
add { m_eventSet.Add(s_fooEventKey, value); }
remove { m_eventSet.Remove(s_fooEventKey, value); }
}
// 2c. Ushbu hodisa uchun himoyalangan, virtual On metodini aniqlash.
protected virtual void OnFoo(FooEventArgs e) {
m_eventSet.Raise(s_fooEventKey, this, e);
}
// 2d. Kirishni ushbu hodisaga tarjima qiladigan metodni aniqlash.
public void SimulateFoo() { OnFoo(new FooEventArgs()); }
#endregion
}
TypeWithLotsOfEvents turidan foydalanadigan kod hodisalar kompilyator tomonidan bilvosita yoki dasturchi tomonidan aniq (explicit) amalga oshirilganligini aniqlay olmaydi. Ular oddiy sintaksis yordamida hodisalarni ro'yxatga olishadi. Buni ko'rsatuvchi kod quyidagicha:
public sealed class Program {
public static void Main() {
TypeWithLotsOfEvents twle = new TypeWithLotsOfEvents();
// Bu yerga callback qo'shish
twle.Foo += HandleFooEvent;
// Ishlashini isbotlash
twle.SimulateFoo();
}
private static void HandleFooEvent(object sender, FooEventArgs e) {
Console.WriteLine("Handling Foo Event here...");
}
}