17-Bob: Delegatlar
Callback funksiyalar, delegat turlari, chaining mexanizmi, generic delegatlar va C# ning sintaktik yorliqlari
Ushbu bobda men callback funksiyalar haqida gapiraman. Callback funksiyalar yillar davomida mavjud bo'lgan juda foydali dasturlash mexanizmidir. Microsoft .NET Framework callback mexanizmini delegatlar (delegates) yordamida ta'minlaydi. Boshqa platformalarda ishlatiladigan boshqarilmaydigan (unmanaged) callback mexanizmlaridan, masalan, C++ dan farqli o'laroq, delegatlar ancha ko'p imkoniyatlarni taklif qiladi. Masalan, delegatlar callback metodining tur-xavfsiz (type-safe) ekanligini ta'minlaydi, bu umumiy til ish vaqti muhitining (CLR) eng muhim maqsadlaridan biridir. Delegatlar shuningdek bir nechta metodlarni ketma-ket chaqirish imkoniyatini birlashtiradi va statik metodlarni ham, instansiya metodlarini ham chaqirishni qo'llab-quvvatlaydi.
Delegatlarga Birinchi Nazar
C runtime kutubxonasining qsort funksiyasi massiv elementlarini saralash uchun callback funksiyaga ko'rsatkich oladi. Windows da callback funksiyalar oyna protseduralar, hook protseduralar, asinxron protsedura chaqiruvlari va boshqa ko'p narsalar uchun talab qilinadi. .NET Framework da callback metodlar juda ko'p maqsadlarda ishlatiladi. Masalan, siz turli xil bildirishnomalarni olish uchun callback metodlarini ro'yxatga olishingiz mumkin: ishlanmagan istisnolar, oyna holati o'zgarishlari, menyu elementi tanlovlari, fayl tizimi o'zgarishlari, forma boshqaruvi hodisalari va tugallangan asinxron operatsiyalar.
Boshqarilmaydigan C/C++ da non-member funksiyaning manzili shunchaki xotira manzilidir. Bu manzil hech qanday qo'shimcha ma'lumot tashimaydi, masalan funksiya kutadigan parametrlar soni, ularning turlari, funksiyaning qaytarish qiymati turi va funksiyaning chaqiruv konventsiyasi. Qisqasi, boshqarilmaydigan C/C++ callback funksiyalari tur-xavfsiz emas (garchi ular juda engil mexanizm bo'lsa ham).
.NET Framework da callback funksiyalar boshqarilmaydigan Windows dasturlashda bo'lgani kabi foydali va keng tarqalgan. Biroq, .NET Framework delegatlar deb nomlangan tur-xavfsiz mexanizmni taqdim etadi. Men delegatlarni sizga qanday ishlatishni ko'rsatish orqali muhokamani boshlayman. Quyidagi kod delegatlarni e'lon qilish, yaratish va ishlatishni ko'rsatadi.
using System;
using System.Windows.Forms;
using System.IO;
// Delegat turini e'lon qilish; instansiyalar Int32
// parametr oladigan va void qaytaradigan metodga murojaat qiladi.
internal delegate void Feedback(Int32 value);
public sealed class Program {
public static void Main() {
StaticDelegateDemo();
InstanceDelegateDemo();
ChainDelegateDemo1(new Program());
ChainDelegateDemo2(new Program());
}
private static void StaticDelegateDemo() {
Console.WriteLine("----- Static Delegate Demo -----");
Counter(1, 3, null);
Counter(1, 3, new Feedback(Program.FeedbackToConsole));
Counter(1, 3, new Feedback(FeedbackToMsgBox)); // "Program." ixtiyoriy
Console.WriteLine();
}
private static void InstanceDelegateDemo() {
Console.WriteLine("----- Instance Delegate Demo -----");
Program p = new Program();
Counter(1, 3, new Feedback(p.FeedbackToFile));
Console.WriteLine();
}
private static void ChainDelegateDemo1(Program p) {
Console.WriteLine("----- Chain Delegate Demo 1 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain = (Feedback)
Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
private static void ChainDelegateDemo2(Program p) {
Console.WriteLine("----- Chain Delegate Demo 2 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain -= new Feedback(FeedbackToMsgBox);
Counter(1, 2, fbChain);
}
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
// Agar callback ko'rsatilgan bo'lsa, ularni chaqirish
if (fb != null)
fb(val);
}
}
private static void FeedbackToConsole(Int32 value) {
Console.WriteLine("Item=" + value);
}
private static void FeedbackToMsgBox(Int32 value) {
MessageBox.Show("Item=" + value);
}
private void FeedbackToFile(Int32 value) {
using (StreamWriter sw = new StreamWriter("Status", true)) {
sw.WriteLine("Item=" + value);
}
}
}
Endi men bu kod nima qilayotganini tushuntiraman. Yuqorida internal Feedback delegatining e'lon qilinishiga e'tibor bering. Delegat callback metodining imzosini (signature) ko'rsatadi. Bu misolda Feedback delegati bitta parametr (Int32) qabul qilib void qaytaradigan metodni aniqlaydi. Bir ma'noda, delegat boshqarilmaydigan C/C++ ning funksiya manzilini ifodalovchi typedef ga juda o'xshaydi.
Program klassi Counter nomli private, statik metodini aniqlaydi. Bu metod from argumentidan to argumentigacha butun sonlarni sanaydi. Counter metodi shuningdek Feedback delegat obyektiga havolani qabul qiladigan fb parametrini oladi. Counter barcha butun sonlar bo'ylab iteratsiya qiladi va har bir butun son uchun, agar fb o'zgaruvchisi null bo'lmasa, fb o'zgaruvchisi tomonidan belgilangan callback metodni chaqiradi. Bu callback metod qayta ishlanayotgan elementning qiymatini oladi. Callback metod har bir elementni mos deb hisoblangan har qanday usulda qayta ishlash va amalga oshirish uchun yaratilishi mumkin.
Delegatlar Yordamida Statik Metodlarni Callback Qilish
Endi siz Counter metodi qanday ishlab chiqilganligini va qanday ishlashini tushunganingizdan keyin, keling, delegatlar yordamida statik metodlarni callback qilishni ko'rib chiqaylik. Ushbu bo'limning asosiy e'tibori yuqoridagi kodda paydo bo'lgan StaticDelegateDemo metodiga qaratilgan.
StaticDelegateDemo metodi Counter metodini chaqirib, uchinchi parametrda null uzatadi. Bu Counter ning fb parametriga mos keladi. Counter ning fb parametri null qiymat olganligi sababli, har bir element hech qanday callback metodini chaqirmasdan qayta ishlanadi.
Keyinchalik, StaticDelegateDemo metodi ikkinchi marta Counter ni chaqiradi, metod chaqiruvining uchinchi parametrida yangi yaratilgan Feedback delegat obyektini uzatadi. Bu delegat obyekti metod atrofidagi o'rama (wrapper) bo'lib, metodni bilvosita o'rama orqali callback qilish imkonini beradi. Bu misolda Program.FeedbackToConsole statik metodining nomi Feedback tur konstruktoriga uzatiladi, bu o'ralishi kerak bo'lgan metod ekanligini bildiradi. new operatoridan qaytarilgan havola Counter ga uchinchi parametr sifatida uzatiladi. Endi Counter bajarilganda, u Program turining statik FeedbackToConsole metodini ketma-ketlikdagi har bir element uchun chaqiradi. FeedbackToConsole shunchaki konsolga qayta ishlanayotgan elementni ko'rsatuvchi satrni yozadi.
FeedbackToConsole metodi Program turi ichida private sifatida aniqlangan, lekin Counter metodi Program ning private metodini chaqira olmoqda. Bu holda, siz muammo kutmasligingiz mumkin, chunki Counter va FeedbackToConsole ikkalasi ham bitta turda aniqlangan. Biroq, bu kod Counter metodi boshqa turda aniqlangan bo'lsa ham ishlaydi. Qisqasi, bitta turning boshqa turning private a'zosini delegat orqali chaqiradigan kodga ega bo'lishi xavfsizlik yoki kirish muammosi emas, agar delegat obyekti yetarli kirish huquqiga ega kod tomonidan yaratilgan bo'lsa.
StaticDelegateDemo metodidagi uchinchi Counter chaqiruvi ikkinchi chaqiruvga deyarli bir xil. Yagona farq shundaki, Feedback delegat obyekti statik Program.FeedbackToMsgBox metodini o'raydi. FeedbackToMsgBox qayta ishlanayotgan elementni ko'rsatuvchi satrni yaratadi va bu satr message box ichida ko'rsatiladi.
Bu misoldagi hamma narsa tur-xavfsiz. Masalan, Feedback delegat obyektini yaratishda kompilyator Program ning FeedbackToConsole va FeedbackToMsgBox metodlarining imzolari Feedback delegati tomonidan aniqlangan imzoga mos kelishini tekshiradi. Ya'ni, ikkala metod ham bitta argument (Int32) qabul qilishi va ikkala metod ham bir xil qaytarish turiga (void) ega bo'lishi kerak. Agar FeedbackToConsole quyidagicha aniqlangan bo'lsa:
private static Boolean FeedbackToConsole(String value) {
...
}
C# kompilyatori kodni kompilyatsiya qilmas va quyidagi xatoni beradi: error CS0123: No overload for 'FeedbackToConsole' matches delegate 'Feedback'.
C# va CLR reference turlarning kovariantligi va kontravariantligini delegatga metod bog'lashda qo'llab-quvvatlaydi. Kovariantlik metod delegatning qaytarish turidan hosila bo'lgan turni qaytarishi mumkinligini bildiradi. Kontravariantlik esa metod delegatning parametr turining asosiy (base) turini parametr sifatida qabul qilishi mumkinligini bildiradi. Masalan, quyidagicha aniqlangan delegat berilsa:
delegate Object MyCallback(FileStream s);
bu delegat turi instansiyasini quyidagicha prototiplangan metodga bog'lash mumkin:
String SomeMethod(Stream s);
Bu yerda SomeMethod ning qaytarish turi (String) delegatning qaytarish turidan (Object) hosila bo'lgan tur; bu kovariantlik ruxsat etiladi. SomeMethod ning parametr turi (Stream) delegatning parametr turining (FileStream) asosiy klasi; bu kontravariantlik ruxsat etiladi.
E'tibor bering, kovariantlik va kontravariantlik faqat reference turlar uchun qo'llab-quvvatlanadi, value turlar yoki void uchun emas. Shuning uchun, masalan, men quyidagi metodni MyCallback delegatiga bog'lay olmayman:
Int32 SomeOtherMethod(Stream s);
Garchi SomeOtherMethod ning qaytarish turi (Int32) MyCallback ning qaytarish turidan (Object) hosila bo'lgan bo'lsa ham, bu kovariantlik shakli ruxsat etilmaydi, chunki Int32 value tur. Sababi value turlar va void uchun kovariantlik va kontravariantlik ishlatib bo'lmaydi, chunki bu turlar uchun xotira tuzilmasi turlicha, reference turlar uchun esa xotira tuzilmasi doimo ko'rsatkich. Yaxshiyamki, agar siz qo'llab-quvvatlanmaydigan biror narsani qilishga harakat qilsangiz, C# kompilyatori xato beradi.
Delegatlar Yordamida Instansiya Metodlarini Callback Qilish
Men hozirgina delegatlar statik metodlarni chaqirish uchun ishlatilishi mumkinligini tushuntirdim, lekin ular ma'lum bir obyekt uchun instansiya metodlarini chaqirish uchun ham ishlatilishi mumkin. Instansiya metodini callback qilish qanday ishlashini tushunish uchun, ushbu bobning boshida ko'rsatilgan koddagi InstanceDelegateDemo metodiga qarang.
InstanceDelegateDemo metodida p nomli Program obyekti yaratilganiga e'tibor bering. Bu Program obyektida hech qanday instansiya maydonlari yoki xususiyatlari yo'q; men uni faqat namoyish maqsadida yaratdim. Counter ga chaqiruvda yangi Feedback delegat obyekti yaratilganda, uning konstruktoriga p.FeedbackToFile uzatiladi. Bu delegatga FeedbackToFile metodiga havolani o'rash imkonini beradi, bu instansiya metodi (statik emas). Counter o'zining fb argumenti bilan aniqlangan callback metodini chaqirganda, FeedbackToFile instansiya metodi chaqiriladi va yaqinda yaratilgan p obyektining manzili this implicit argumenti sifatida instansiya metodiga uzatiladi.
FeedbackToFile metodi FeedbackToConsole va FeedbackToMsgBox metodlari kabi ishlaydi, faqat u faylni ochadi va satrni faylning oxiriga qo'shadi. (Metod yaratadigan Status faylini ilovaning AppBase katalogida topish mumkin.)
Yana bir bor, bu misolning maqsadi delegatlar instansiya metodlarini ham, statik metodlarni ham chaqirishni o'rashi mumkinligini ko'rsatishdir. Instansiya metodlari uchun delegat metod ishlash uchun qaratilgan obyektning instansiyasini bilishi kerak. Instansiya metodini o'rash foydali, chunki obyekt ichidagi kod obyektning instansiya a'zolariga murojaat qilishi mumkin. Bu shuni anglatadiki, obyekt callback metodi o'z ishini bajarayotganda foydalanilishi mumkin bo'lgan ba'zi holatga (state) ega bo'lishi mumkin.
Delegatlarning Sir-Asrori
Yuzaki qarashda delegatlar ishlatish oson ko'rinadi: siz ularni C# ning delegate kalit so'zi yordamida aniqlaysiz, tanish new operatori yordamida instansiyalarini yaratasiz va tanish metod-chaqiruv sintaksisi yordamida callback ni chaqirasiz (faqat metod nomi o'rniga delegat obyektiga murojaat qiluvchi o'zgaruvchidan foydalanasiz).
Biroq, aslida yuz berayotgan narsa oldingi misollar ko'rsatganidan ancha murakkab. Kompilyatorlar va CLR murakkablikni yashirish uchun juda ko'p sahna ortidagi ishlov berishni amalga oshiradi. Ushbu bo'limda men kompilyator va CLR delegatlarni amalga oshirish uchun qanday birgalikda ishlashiga e'tibor qarataman. Bu bilim delegatlarni samarali va ta'sirchan ishlatishga yordam beradi. Men shuningdek delegatlar taqdim etadigan ba'zi qo'shimcha imkoniyatlarga ham to'xtalaman.
Keling, ushbu kod satrini qayta ko'rib chiqishdan boshlaymiz:
internal delegate void Feedback(Int32 value);
Kompilyator bu satrni ko'rganda, u aslida taxminan quyidagicha ko'rinadigan to'liq klassni aniqlaydi:
internal class Feedback : System.MulticastDelegate {
// Konstruktor
public Feedback(Object @object, IntPtr method);
// Manba koddagi prototip bilan bir xil imzoga ega metod
public virtual void Invoke(Int32 value);
// Callback ni asinxron chaqirishga ruxsat beruvchi metodlar
public virtual IAsyncResult BeginInvoke(Int32 value,
AsyncCallback callback, Object @object);
public virtual void EndInvoke(IAsyncResult result);
}
Kompilyator tomonidan aniqlangan klass to'rtta metodga ega: konstruktor, Invoke, BeginInvoke va EndInvoke. Ushbu bobda men konstruktor va Invoke metodlariga e'tibor qarataman. BeginInvoke va EndInvoke metodlari .NET Framework ning Asinxron Dasturlash Modeli (APM) bilan bog'liq bo'lib, bu model hozirda eskirgan hisoblanadi va 27-Bobda muhokama qilinadigan tasklar bilan almashtirilgan.
Aslida, siz kompilyator haqiqatan ham bu klassni avtomatik ravishda yaratganini ILDasm.exe yordamida natijadagi assemblini tekshirish orqali tasdiqlashingiz mumkin (17-1 rasm).
Bu misolda kompilyator Framework Class Library (FCL) da aniqlangan System.MulticastDelegate turidan hosila bo'lgan Feedback nomli klassni aniqlagan. (Barcha delegat turlari MulticastDelegate dan hosila bo'ladi.)
System.MulticastDelegate klassi System.Delegate dan hosila bo'lgan, u esa o'z navbatida System.Object dan hosila bo'lgan. FCL da ikkita delegat klassi bo'lishining sababi tarixiy va baxtsiz; aslida bitta delegat klassi bo'lishi kerak edi. Afsuski, siz har ikkala klass haqida ham bilishingiz kerak, chunki siz yaratadigan barcha delegat turlari MulticastDelegate ni asosiy klass sifatida ishlatsa ham, ba'zan delegat turlaringizni MulticastDelegate klassi o'rniga Delegate klassi tomonidan aniqlangan metodlar yordamida boshqarishingiz kerak bo'ladi. Masalan, Delegate klassida Combine va Remove nomli statik metodlar mavjud. Bu ikkala metod imzolari Delegate parametrlarini qabul qilishini ko'rsatadi. Delegat turingiz Delegate dan hosila bo'lgan MulticastDelegate dan hosila bo'lganligi sababli, delegat turingiz instansiyalari bu metodlarga uzatilishi mumkin.
Klass private ko'rinishga ega, chunki delegat manba kodida internal sifatida e'lon qilingan. Agar manba kodi public ko'rinishni ko'rsatgan bo'lsa, kompilyator yaratgan Feedback klassi ham public bo'lar edi. Shuni bilish kerakki, delegat turlari tur ichida (ichki) yoki global miqyosda aniqlanishi mumkin. Asosan, delegatlar klasslar bo'lganligi sababli, klass aniqlash mumkin bo'lgan har qanday joyda delegat ham aniqlanishi mumkin.
MulticastDelegate ning Muhim Maydonlari
Barcha delegat turlari MulticastDelegate dan hosila bo'lganligi sababli, ular MulticastDelegate ning maydonlari, xususiyatlari va metodlarini meros qilib oladi. Bu a'zolar orasida uchta non-public maydoni eng muhim:
| Maydon | Tur | Tavsif |
|---|---|---|
_target |
System.Object |
Delegat obyekti statik metodni o'raganda bu maydon null. Delegat obyekti instansiya metodini o'raganda bu maydon callback metodi chaqirilganda qaratilishi kerak bo'lgan obyektga ishora qiladi. Boshqacha aytganda, bu maydon instansiya metodining implicit this parametri uchun uzatiladigan qiymatni ko'rsatadi. |
_methodPtr |
System.IntPtr |
CLR callback qilinadigan metodni aniqlash uchun ishlatadigan ichki butun son. |
_invocationList |
System.Object |
Bu maydon odatda null. Delegat zanjiri (chain) qurilganda u delegatlar massiviga murojaat qilishi mumkin (ushbu bobda keyinroq muhokama qilinadi). |
E'tibor bering, barcha delegatlarning konstruktori ikkita parametr qabul qiladi: obyektga havola va metodni aniqlaydigan butun son. Biroq, agar siz manba kodini ko'rib chiqsangiz, siz Program.FeedbackToConsole yoki p.FeedbackToFile kabi qiymatlarni uzatayotganingizni ko'rasiz. Dasturlash haqida bilganlaringiz sizga bu kod kompilyatsiya qilinmasligi kerakligini aytadi!
Biroq, C# kompilyatori delegat yaratilayotganini biladi va manba kodini tahlil qilib, qaysi obyekt va metod ko'rsatilayotganini aniqlaydi. Obyektga havola konstruktorning object parametri uchun uzatiladi va metodni aniqlaydigan maxsus IntPtr qiymati (MethodDef yoki MemberRef metama'lumot tokenidan olingan) method parametri uchun uzatiladi. Statik metodlar uchun object parametri sifatida null uzatiladi. Konstruktor ichida bu ikkala argument _target va _methodPtr private maydonlarida saqlanadi. Bundan tashqari, konstruktor _invocationList maydonini null ga o'rnatadi.
Shunday qilib, har bir delegat obyekti aslida metod va metod chaqirilganda qaratilishi kerak bo'lgan obyekt atrofidagi o'rama. Agar menda quyidagi ikkita kod satri bo'lsa:
Feedback fbStatic = new Feedback(Program.FeedbackToConsole);
Feedback fbInstance = new Feedback(new Program().FeedbackToFile);
fbStatic va fbInstance o'zgaruvchilari 17-2 rasmda ko'rsatilganidek boshlangan ikkita alohida Feedback delegat obyektlariga murojaat qiladi.
Statik delegatda _target maydoni null, _methodPtr esa FeedbackToConsole ga ishora qiladi. Instansiya delegatida esa _target Program obyektiga, _methodPtr esa FeedbackToFile ga ishora qiladi.
Callback Metod Qanday Chaqiriladi
Endi delegat obyektlari qanday yaratilishini va ularning ichki tuzilishini bilganingizdan keyin, keling, callback metod qanday chaqirilishini muhokama qilaylik. Qulaylik uchun Counter metodi kodini bu yerda takrorlayman:
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
// Agar callback ko'rsatilgan bo'lsa, ularni chaqirish
if (fb != null)
fb(val);
}
}
Izoh ostidagi kod satriga qarang. if iborasi avval fb ning null emasligini tekshiradi. Agar fb null bo'lmasa, keyingi satrda callback metodini chaqiradigan kodni ko'rasiz. null tekshiruvi kerak, chunki fb aslida Feedback delegat obyektiga murojaat qilishi mumkin bo'lgan o'zgaruvchi; u null ham bo'lishi mumkin. Bu fb nomli funksiyani val parametri bilan chaqirayotgandek ko'rinishi mumkin. Biroq, fb nomli funksiya yo'q. Kompilyator fb delegat obyektiga murojaat qiluvchi o'zgaruvchi ekanligini bilganligi sababli, delegat obyektining Invoke metodini chaqirish uchun kod yaratadi. Ya'ni, kompilyator quyidagini ko'radi:
fb(val);
Lekin kompilyator aslida manba kodi quyidagicha yozilgandek kod yaratadi:
fb.Invoke(val);
Aslida, siz Counter metodini Invoke ni aniq chaqiradigan qilib o'zgartirishingiz mumkin:
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
// Agar callback ko'rsatilgan bo'lsa, ularni chaqirish
if (fb != null)
fb.Invoke(val);
}
}
Invoke chaqirilganda, u private _target va _methodPtr maydonlaridan foydalanib, belgilangan obyektdagi kerakli metodni chaqiradi. E'tibor bering, Invoke metodining imzosi delegat imzosiga mos keladi; Feedback delegati bitta Int32 parametr qabul qilib void qaytarganligi sababli, Invoke metodi (kompilyator tomonidan yaratilgan) ham bitta Int32 parametr qabul qiladi va void qaytaradi.
Delegatlar Yordamida Ko'p Metodlarni Callback Qilish (Chaining)
O'z-o'zicha delegatlar juda foydali. Lekin ularga zanjir (chaining) qo'llab-quvvatlashini qo'shsang, delegatlar yanada foydaliroq bo'ladi. Chaining delegat obyektlarining to'plami yoki kolleksiyasi bo'lib, u to'plamdagi delegatlar tomonidan ifodalangan barcha metodlarni chaqirish imkoniyatini beradi. Buni tushunish uchun bobning boshidagi koddagi ChainDelegateDemo1 metodiga qarang. Bu metodda Console.WriteLine iborasidan keyin men uchta delegat obyektini yarataman va fb1, fb2, fb3 o'zgaruvchilariga murojaat qilaman.
Feedback delegat obyektiga havolani saqlash uchun mo'ljallangan fbChain reference o'zgaruvchisi callback qilinadigan metodlar zanjiri yoki to'plamiga murojaat qilishi mo'ljallangan. fbChain ni null ga o'rnatish hozirda hech qanday metod callback qilinmasligi kerakligini bildiradi. Delegate klassining public, statik Combine metodi zanjirga delegat qo'shish uchun ishlatiladi.
fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
Bu kod satri bajarilganda, Combine metodi biz null va fb1 ni birlashtirmoqchi ekanligimizni ko'radi. Ichki tarzda Combine shunchaki fb1 dagi qiymatni qaytaradi va fbChain o'zgaruvchisi fb1 o'zgaruvchisi ko'rsatgan delegat obyektiga murojaat qiladigan qilib o'rnatiladi.
Zanjirga yana bitta delegat qo'shish uchun Combine metodi yana chaqiriladi:
fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
Ichki tarzda Combine metodi fbChain allaqachon delegat obyektiga murojaat qilganini ko'radi, shuning uchun Combine yangi delegat obyektini yaratadi. Bu yangi delegat obyektining _target va _methodPtr maydonlari muhim bo'lmagan qiymatlarga o'rnatiladi. Biroq, muhimi shundaki, _invocationList maydoni delegat obyektlari massiviga murojaat qilish uchun o'rnatiladi. Bu massivning birinchi elementi (indeks 0) FeedbackToConsole metodini o'raydigan delegatga (ya'ni fbChain hozirda murojaat qilgan delegat) murojaat qilish uchun o'rnatiladi. Massivning ikkinchi elementi (indeks 1) FeedbackToMsgBox metodini o'raydigan delegatga (fb2 murojaat qilgan delegat) murojaat qilish uchun o'rnatiladi. Va nihoyat, fbChain yangi yaratilgan delegat obyektiga murojaat qiladigan qilib o'rnatiladi.
Uchinchi delegatni qo'shish uchun Combine yana bir bor chaqiriladi:
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
Yana, Combine fbChain allaqachon delegat obyektiga murojaat qilganini ko'radi va bu yangi delegat obyektining yaratilishiga olib keladi. Oldingiday, bu yangi delegat obyekt muhim bo'lmagan qiymatlarga o'rnatilgan _target va _methodPtr maydonlarini va delegat obyektlari massiviga murojaat qiluvchi _invocationList maydonini boshlaydi. Bu massivning birinchi va ikkinchi elementlari oldingi delegat obyektining massividagi bir xil delegatlarga murojaat qiladi. Massivning uchinchi elementi (indeks 2) FeedbackToFile metodini o'raydigan delegatga (fb3 murojaat qilgan delegat) murojaat qilish uchun o'rnatiladi. Va nihoyat, fbChain bu yangi yaratilgan delegat obyektiga murojaat qiladigan qilib o'rnatiladi.
Zanjirni o'rnatish uchun barcha kod bajarilgandan keyin, fbChain o'zgaruvchisi Counter metodiga uzatiladi:
Counter(1, 2, fbChain);
Counter metodi ichida Feedback delegat obyektidagi Invoke metodini bilvosita chaqiradigan kod mavjud. Invoke fbChain tomonidan ko'rsatilgan delegatda chaqirilganda, delegat private _invocationList maydoni null emasligini ko'radi va bu massivdagi barcha elementlarni aylanib, har bir delegat tomonidan o'ralgan metodni chaqiradigan tsiklni bajarishiga olib keladi. Bu misolda avval FeedbackToConsole, keyin FeedbackToMsgBox, keyin FeedbackToFile chaqiriladi.
Feedback ning Invoke metodi ichki tarzda taxminan quyidagicha amalga oshirilgan (psevdokod):
public void Invoke(Int32 value) {
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null) {
// Bu delegatning massivi chaqirilishi kerak bo'lgan
// delegatlarni ko'rsatadi
foreach (Feedback d in delegateSet)
d(value); // Har bir delegatni chaqirish
} else {
// Bu delegat bitta metodni callback qilish kerakligini bildiradi
// Callback metodni belgilangan maqsad obyektida chaqirish.
_methodPtr.Invoke(_target, value);
// Oldingi satr haqiqiy kodning taxminiy ko'rinishi.
// Aslida yuz berayotgan narsani C# da ifodalab bo'lmaydi.
}
}
E'tibor bering, delegatni zanjirdan olib tashlash ham mumkin. Buning uchun Delegate ning public, statik Remove metodi chaqiriladi. Bu ChainDelegateDemo1 metodining oxirida ko'rsatilgan:
fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Remove chaqirilganda, u birinchi parametr (fbChain) tomonidan ko'rsatilgan delegat obyekti ichidagi delegat massivini oxiridan boshiga skanerlaydi. Remove _target va _methodPtr maydonlari ikkinchi argumentdagilarga mos keladigan delegat yozuvini qidiradi. Agar moslik topilsa va massivda faqat bitta element qolsa, o'sha massiv elementi qaytariladi. Agar moslik topilsa va massivda bir nechta element qolsa, yangi delegat obyekti yaratiladi va yangi _invocationList massivi olib tashlangan elementdan tashqari asl massivdagi barcha elementlarga murojaat qilish uchun o'rnatiladi va bu yangi delegat obyektiga havola qaytariladi. Agar siz zanjiridagi yagona elementni olib tashlayotgan bo'lsangiz, Remove null qaytaradi. E'tibor bering, har bir Remove chaqiruvi zanjirdan faqat bitta delegatni olib tashlaydi.
C# ning Delegat Zanjirlari uchun Qo'llab-quvvatlashi
C# dasturchilari uchun ishlarni osonlashtirish maqsadida, C# kompilyatori avtomatik ravishda delegat turlari instansiyalari uchun += va -= operatorlarining ortiqcha yuklanishlarini taqdim etadi. Bu operatorlar mos ravishda Delegate.Combine va Delegate.Remove ni chaqiradi. Ushbu operatorlardan foydalanish delegat zanjirlarini qurishni soddalashtiradi. Bobning boshida ko'rsatilgan manba kodidagi ChainDelegateDemo1 va ChainDelegateDemo2 metodlari mutlaqo bir xil IL kodini hosil qiladi. Yagona farq shundaki, ChainDelegateDemo2 metodi C# ning += va -= operatorlaridan foydalanib manba kodini soddalashtiradi.
Agar natijadagi IL kodi ikkala metod uchun bir xil ekanligiga ishonch hosil qilmoqchi bo'lsangiz, kodni kompilyatsiya qilib ILDasm.exe yordamida ko'rishingiz mumkin. Bu C# kompilyatori haqiqatan ham barcha += va -= operatorlarini Delegate turining public statik Combine va Remove metodlariga chaqiruvlar bilan almashtirilganini tasdiqlaydi.
Agar delegat turi void emas qaytarish turiga ega bo'lsa, masalan:
public delegate Int32 Feedback(Int32 value);
uning Invoke metodi ichki tarzda taxminan quyidagicha ko'ringan bo'lar edi (psevdokod):
public Int32 Invoke(Int32 value) {
Int32 result;
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null) {
// Bu delegatning massivi chaqirilishi kerak bo'lgan
// delegatlarni ko'rsatadi
foreach (Feedback d in delegateSet)
result = d(value); // Har bir delegatni chaqirish
} else {
// Bu delegat bitta metodni callback qilish kerakligini bildiradi
result = _methodPtr.Invoke(_target, value);
}
return result;
}
Massivdagi har bir delegat chaqirilgan sari, uning qaytarish qiymati result o'zgaruvchisida saqlanadi. Tsikl tugaganda, result o'zgaruvchisi faqat oxirgi chaqirilgan delegatning natijasini o'z ichiga oladi (oldingi qaytarish qiymatlari e'tiborga olinmaydi); bu qiymat Invoke ni chaqirgan kodga qaytariladi.
Delegat Zanjiri Chaqiruvi Ustidan Kattaroq Nazoratga Ega Bo'lish
Shu paytgacha siz delegat obyektlari zanjirini qanday qurish va zanjiridagi barcha obyektlarni chaqirishni tushunasiz. Zanjiridagi barcha elementlar delegat turining Invoke metodi massivdagi barcha elementlarni aylanib, har bir elementni chaqiradigan kodni o'z ichiga olganligi sababli chaqiriladi. Bu juda oddiy algoritm. Garchi bu oddiy algoritm ko'p hollarda yetarli bo'lsa-da, uning ko'plab cheklovlari bor. Masalan, callback metodlarning qaytarish qiymatlari oxirgisidan tashqari barchasi e'tiborga olinmaydi. Bu oddiy algoritmdan foydalanib, barcha chaqirilgan callback metodlarning qaytarish qiymatlarini olishning imkoni yo'q. Lekin bu yagona cheklov emas. Agar chaqirilgan delegatlardan biri istisno tashlasa yoki juda uzoq vaqt bloklasa nima bo'ladi? Algoritm zanjiridagi har bir delegatni ketma-ket chaqirganligi sababli, delegat obyektlaridan birining "muammosi" keyingi barcha delegatlarning chaqirilishiga to'sqinlik qiladi. Aniqki, bu algoritm mustahkam emas.
Bu algoritm yetarli bo'lmagan holatlar uchun MulticastDelegate klassi GetInvocationList instansiya metodini taklif qiladi, uni har bir delegatni zanjiridagi har bir delegatni aniq chaqirish uchun, o'zingizning ehtiyojlaringizga mos keladigan har qanday algoritmdan foydalanib ishlatishingiz mumkin.
public abstract class MulticastDelegate : Delegate {
// Har bir element zanjiridagi delegatga murojaat
// qiladigan delegat massivini yaratadi.
public sealed override Delegate[] GetInvocationList();
}
GetInvocationList metodi MulticastDelegate dan hosila bo'lgan obyektda ishlaydi va Delegate havolalari massivini qaytaradi, bu yerda har bir havola zanjirdagi delegat obyektlaridan biriga ishora qiladi. Quyidagi kod massivedagi har bir obyektni aniq chaqiradigan algoritmni ko'rsatadi:
using System;
using System.Reflection;
using System.Text;
// Light komponentini aniqlash.
internal sealed class Light {
// Bu metod chiroqning holatini qaytaradi.
public String SwitchPosition() {
return "The light is off";
}
}
// Fan komponentini aniqlash.
internal sealed class Fan {
// Bu metod ventilyatorning holatini qaytaradi.
public String Speed() {
throw new InvalidOperationException("The fan broke due to overheating");
}
}
// Speaker komponentini aniqlash.
internal sealed class Speaker {
// Bu metod karnayning holatini qaytaradi.
public String Volume() {
return "The volume is loud";
}
}
public sealed class Program {
// Komponentning holatini so'rovchi delegat ta'rifi.
private delegate String GetStatus();
public static void Main() {
// Bo'sh delegat zanjirini e'lon qilish.
GetStatus getStatus = null;
// Uchta komponentni yaratish va ularning status
// metodlarini delegat zanjiriga qo'shish.
getStatus += new Light().SwitchPosition;
getStatus += new Fan().Speed;
getStatus += new Speaker().Volume;
// Uchta komponentning holatini aks
// ettiruvchi yig'ma hisobotni ko'rsatish.
Console.WriteLine(GetComponentStatusReport(getStatus));
}
// Bir nechta komponentni so'rovchi va status hisobotini qaytaruvchi metod
private static String GetComponentStatusReport(GetStatus status) {
// Agar zanjir bo'sh bo'lsa, hech narsa qilmaslik.
if (status == null) return null;
// Status hisobotini yaratish uchun foydalaniladi.
StringBuilder report = new StringBuilder();
// Har bir element zanjiridagi delegat bo'lgan massiv olish.
Delegate[] arrayOfDelegates = status.GetInvocationList();
// Har bir delegat ustidan iteratsiya qilish.
foreach (GetStatus getStatus in arrayOfDelegates) {
try {
// Komponentning status satrini olish va hisobotga qo'shish.
report.AppendFormat("{0}{1}{1}", getStatus(), Environment.NewLine);
}
catch (InvalidOperationException e) {
// Bu komponent uchun hisobotda xato yozuvi yaratish.
Object component = getStatus.Target;
report.AppendFormat(
"Failed to get status from {1}{2}{0} Error: {3}{0}{0}",
Environment.NewLine,
((component == null) ? "" : component.GetType() + "."),
getStatus.GetMethodInfo().Name,
e.Message);
}
}
// Yig'ilgan hisobotni chaqiruvchiga qaytarish.
return report.ToString();
}
}
Bu kodni kompilyatsiya qilib ishga tushirganingizda quyidagi natija paydo bo'ladi:
The light is off
Failed to get status from Fan.Speed
Error: The fan broke due to overheating
The volume is loud
Delegat Ta'riflarini Yetarli! (Generic Delegatlar)
Ko'p yillar oldin, .NET Framework endigina ishlab chiqila boshlaganida, Microsoft delegatlar tushunchasini joriy qildi. Dasturchilar FCL ga klasslar qo'shayotganlarida, ular callback metodni joriy qilgan har qanday joyda yangi delegat turlarini aniqlashadi. Vaqt o'tishi bilan ko'plab delegatlar aniqlandi. Aslida, faqat MSCorLib.dll da 50 ga yaqin delegat turi aniqlangan. Keling, ulardan bir nechtasini ko'raylik:
public delegate void TryCode(Object userData);
public delegate void WaitCallback(Object state);
public delegate void TimerCallback(Object state);
public delegate void ContextCallback(Object state);
public delegate void SendOrPostCallback(Object state);
public delegate void ParameterizedThreadStart(Object obj);
Men tanlagan bir nechta delegat ta'riflari haqida biror o'xshashlik sezdingizmi? Ular hammasi bir xil: bu delegat turlaridan har qandayining o'zgaruvchisi Object qabul qiluvchi va void qaytaruvchi metodga murojaat qilishi kerak. Bu delegat turlarining barchasiga ega bo'lishga haqiqatan ham sabab yo'q; aslida bitta bo'lishi kerak.
Haqiqatan ham, endi .NET Framework generikni qo'llab-quvvatlaganligi sababli, bizga faqat 16 tagacha argumentni qabul qiluvchi metodlarni ifodalovchi bir nechta generic delegat (System nom fazosida aniqlangan) kerak:
public delegate void Action(); // OK, bu generic emas
public delegate void Action<T>(T obj);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);
...
public delegate void Action<T1, ..., T16>(T1 arg1, ..., T16 arg16);
Shunday qilib, .NET Framework argumentsiz dan 16 tagacha argumentli bo'lgan 17 ta Action delegati bilan birga keladi. Agar sizga 16 dan ortiq argumentga ega metodni chaqirish kerak bo'lsa, o'z delegat turingizni aniqlashingizga to'g'ri keladi, lekin bu juda kam uchraydi.
Action delegatlaridan tashqari, .NET Framework callback metodning qiymat qaytarishiga imkon beruvchi 17 ta Func delegatini taqdim etadi:
public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T arg);
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
...
public delegate TResult Func<T1,..., T16, TResult>(T1 arg1, ..., T16 arg16);
Endi dasturchilarga o'z delegat turlarini aniqlash o'rniga imkon qadar bu delegat turlaridan foydalanish tavsiya etiladi. Bu tizimdagi turlar sonini kamaytiradi va shuningdek kodni soddalashtiradi. Biroq, agar siz ref yoki out kalit so'zi yordamida argumentni havola bo'yicha uzatishingiz kerak bo'lsa, o'z delegatingizni aniqlashingiz kerak bo'lishi mumkin:
delegate void Bar(ref Int32 z);
Bundan tashqari, agar delegatingiz C# ning params kalit so'zi orqali o'zgaruvchan sondagi argumentlarni qabul qilishini, delegat argumentlarining birortasi uchun standart qiymatlarni belgilashni yoki delegat generic tur argumentini cheklashni xohlasangiz ham o'z delegatingizni aniqlashingiz kerak bo'lishi mumkin.
Generic qaytarish qiymatlari va argumentlarni qabul qiluvchi delegatlardan foydalanganingizda kovariantlik va kontravariantlik o'yinga kiradi va bu xususiyatlardan doimo foydalanish tavsiya etiladi, chunki ular hech qanday salbiy ta'sir ko'rsatmaydi va delegatlaringizni ko'proq stsenariylarda ishlatish imkonini beradi. Bu haqida ko'proq ma'lumot uchun 12-Bob "Generiklar" dagi "Delegat va Interfeys Kontra-variant va Kovariant Generic Tur Argumentlari" bo'limiga qarang.
C# ning Delegatlar uchun Sintaktik Shakari
Ko'pchilik dasturchilar delegatlar bilan ishlashni noqulay deb bilishadi, chunki sintaksis juda g'alati. Masalan, ushbu kod satrini oling:
button1.Click += new EventHandler(button1_Click);
bu yerda button1_Click taxminan quyidagicha ko'rinadigan metod:
void button1_Click(Object sender, EventArgs e) {
// Tugma bosilganda biror narsa qilish...
}
Birinchi kod satri ortidagi g'oya button1_Click metodining manzilini tugma boshqaruvi bilan ro'yxatga olishdir, shunda tugma bosilganda metod chaqirilsin. Ko'pchilik dasturchilar uchun faqat button1_Click metodining manzilini ko'rsatish uchun EventHandler delegat obyektini yaratish g'ayritabiiy tuyuladi. Biroq, EventHandler delegat obyektini yaratish CLR uchun kerak, chunki bu obyekt metodning faqat tur-xavfsiz usulda chaqirilishini ta'minlaydigan o'rama beradi. O'rama shuningdek instansiya metodlarini chaqirish va zanjirlashni ta'minlaydi. Afsuski, ko'pchilik dasturchilar bu tafsilotlar haqida o'ylamoqchi emas. Dasturchilar oldingi kodni quyidagicha yozishni afzal ko'rishardi:
button1.Click += button1_Click;
Yaxshiyamki, Microsoft ning C# kompilyatori dasturchilarga delegatlar bilan ishlashda bir qancha sintaktik yorliqlarni taklif qiladi. Men ushbu bo'limda barcha yorliqlarni tushuntiraman. Boshlashdan oldin bitta muhim nuqta: men tasvirlamoqchi bo'lgan narsa aslida C# ning sintaktik shakari; bu yangi sintaktik yorliqlar dasturchilarga CLR va boshqa dasturlash tillari delegatlar bilan ishlashi uchun yaratilishi kerak bo'lgan IL ni hosil qilishning osonroq usulini bermoqda. Bu shuningdek men tasvirlamoqchi bo'lgan narsa C# ga xos ekanligini anglatadi; boshqa kompilyatorlar qo'shimcha delegat sintaktik yorliqlarini taklif qilmasligi mumkin.
Sintaktik Yorliq #1: Delegat Obyektini Yaratish Kerak Emas
Yuqorida ko'rsatilganidek, C# sizga delegat obyekti o'ramasini yaratmasdan callback metodning nomini ko'rsatish imkonini beradi. Mana yana bir misol:
internal sealed class AClass {
public static void CallbackWithoutNewingADelegateObject() {
ThreadPool.QueueUserWorkItem(SomeAsyncTask, 5);
}
private static void SomeAsyncTask(Object o) {
Console.WriteLine(o);
}
}
Bu yerda ThreadPool klassining statik QueueUserWorkItem metodi SomeAsyncTask metodiga havolani o'z ichiga olgan WaitCallback delegat obyektini kutadi. C# kompilyatori buni o'zi aniqlash qobiliyatiga ega bo'lganligi sababli, u menga WaitCallback delegat obyektini yaratuvchi kodni tushirib qoldirishga ruxsat beradi, bu kodni ancha o'qilishi va tushunarli qiladi. Albatta, kod kompilyatsiya qilinganida, C# kompilyatori WaitCallback delegat obyektini new qiluvchi IL ni hosil qiladi — biz shunchaki sintaktik yorliq oldik.
Sintaktik Yorliq #2: Callback Metodini Aniqlash Kerak Emas (Lambda Ifodalar)
Oldingi kodda SomeAsyncTask callback metodining nomi ThreadPool.QueueUserWorkItem metodiga uzatiladi. C# sizga callback metod uchun kodni o'zining alohida metodi ichiga yozish o'rniga inline yozish imkonini beradi. Masalan, oldingi kodni quyidagicha qayta yozish mumkin:
internal sealed class AClass {
public static void CallbackWithoutNewingADelegateObject() {
ThreadPool.QueueUserWorkItem( obj => Console.WriteLine(obj), 5);
}
}
QueueUserWorkItem metodiga birinchi "argument" kod ekanligiga e'tibor bering (men kursiv qildim)! Rasmiy ravishda, kursivlangan kod C# lambda ifodasi (lambda expression) deb ataladi va uni C# ning lambda ifoda operatori: => mavjudligi tufayli aniqlash oson. Lambda ifodani kodingizda kompilyator odatda delegatni ko'rishni kutadigan har qanday joyda ishlatishingiz mumkin. Kompilyator bu lambda ifoda ishlatilishini ko'rganda, avtomatik ravishda klassda (bu misolda AClass) yangi private metodini aniqlaydi. Bu yangi metod anonim funksiya deb ataladi, chunki kompilyator siz uchun metod nomini avtomatik yaratadi va odatda siz uning nomini bilmaysiz.
=> operatorining chap tomonida lambda ifodaga uzatiladigan har qanday argumentlarning nomlari ko'rsatiladi. Quyidagi misollarga qarang:
// Agar delegat argumentsiz bo'lsa, () ishlatiladi
Func<String> f = () => "Jeff";
// Agar delegat 1+ argument qabul qilsa, turlarni aniq ko'rsatish mumkin
Func<Int32, String> f2 = (Int32 n) => n.ToString();
Func<Int32, Int32, String> f3 = (Int32 n1, Int32 n2) => (n1 + n2).ToString();
// Agar delegat 1+ argument qabul qilsa, kompilyator turlarni aniqlashi mumkin
Func<Int32, String> f4 = (n) => n.ToString();
Func<Int32, Int32, String> f5 = (n1, n2) => (n1 + n2).ToString();
// Agar delegat 1 ta argument qabul qilsa, ()larni tushirib qoldirishingiz mumkin
Func<Int32, String> f6 = n => n.ToString();
// Agar delegat ref/out argumentlarga ega bo'lsa, ref/out va turni aniq ko'rsatish kerak
Bar b = (out Int32 n) => n = 5;
Oxirgi misol uchun Bar quyidagicha aniqlangan deb faraz qiling:
delegate void Bar(out Int32 z);
=> operatorining o'ng tomonida anonim funksiya tanasini ko'rsatasiz. Tana ko'pincha oddiy yoki murakkab ifodadan iborat bo'lib, u oxir-oqibat non-void qiymat qaytaradi. Oldingi kodda men String qaytaruvchi lambda ifodalarni Func delegat o'zgaruvchilariga tayinladim. Tana bitta ibora bo'lishi ham juda keng tarqalgan, masalan men ThreadPool.QueueUserWorkItem ni chaqirganimda Console.WriteLine ni chaqiruvchi lambda ifodani uzatgandim (bu void qaytaradi).
Agar tananing ikkita yoki undan ko'p iboradan iborat bo'lishini xohlasangiz, uni figurali qavslar ichiga olishingiz kerak. Va agar delegat qaytarish qiymatini kutsa, tanada return iborasi bo'lishi kerak:
Func<Int32, Int32, String> f7 = (n1, n2) => { Int32 sum = n1 + n2; return sum.ToString(); };
Agar aniq bo'lmasa, shuni aniq ta'kidlab o'tay: lambda ifodalarning asosiy afzalligi shundaki, ular manba kodingizdan bir darajali bilvositalikni olib tashlaydi. Odatda, siz alohida metod yozishingiz, unga nom berishingiz va keyin delegat talab qilinadigan joyda metod nomini uzatishingiz kerak bo'lardi. Lambda ifoda sizga kodni to'g'ridan-to'g'ri inline joylashtirish imkonini beradi va unga nom berish zaruratini yo'q qiladi, shu bilan dasturchining mahsuldorligini oshiradi.
C# 2.0 chiqqanda, u anonim metodlar (anonymous methods) deb nomlangan xususiyatni joriy qildi. Lambda ifodalar singari (C# 3.0 da joriy qilingan), anonim metodlar anonim funksiyalar yaratish uchun sintaksisni tavsiflaydi. Endi C# Til Spetsifikatsiyasining 7.14 bo'limida tavsiya etilanadigan (C# 2.0 da joriy qilingan) anonim metod sintaksisi o'rniga yangi lambda ifoda sintaksisidan foydalanish tavsiya etiladi, chunki lambda ifoda sintaksisi ancha ixcham va kodni yozish, o'qish va saqlashni osonlashtiradi.
Lambda ifoda kompilyator parametr/lokal o'zgaruvchilarni maydonlarga aylantirgan klass yaratishiga sabab bo'lganda, o'zgaruvchilar murojaat qilgan obyektlarning umri uzaytiriladi. Odatda parametr/lokal o'zgaruvchi metod ichida o'zgaruvchining oxirgi ishlatilishida amaliyot doirasidan chiqadi. Biroq, o'zgaruvchini maydonga aylantirish maydon o'z ichiga olgan obyektning butun umri davomida tirik qolish imkonini beradi. Bu aksariyat ilovalarda katta muammo emas, lekin siz bundan xabardor bo'lishingiz kerak.
Shubhasiz, dasturchilarning C# ning lambda ifoda xususiyatini suiiste'mol qilishni boshlashi uchun ko'p vaqt kerak bo'lmaydi. Men birinchi marta lambda ifodalardan foydalanishni boshlaganimda, ularga ko'nikishim uchun bir oz vaqt kerak bo'ldi. Axir, metod ichida yozgan kodingiz aslida o'sha metod ichida emas va bu debug qilish va kodni bosqichma-bosqich kuzatishni biroz qiyinlashtirishi mumkin. Men o'zim uchun qoida qo'yganman: agar callback metodim uch satrdan ko'p kodga ega bo'lishi kerak bo'lsa, men lambda ifodadan foydalanmayman; buning o'rniga metodni qo'lda yozaman va o'z nomimni beraman. Lekin aqlli ishlatilganda, lambda ifodalar dasturchining mahsuldorligini va kodning saqlanuvchanligini sezilarli darajada oshirishi mumkin.
Lambda ifodalar bilan ishlaydigan ba'zi tabiiy kod misollari:
// String massivini yaratish va boshlash
String[] names = { "Jeff", "Kristin", "Aidan", "Grant" };
// Kichik 'a' harfiga ega ismlarni olish
Char charToFind = 'a';
names = Array.FindAll(names, name => name.IndexOf(charToFind) >= 0);
// Har bir satrning belgilarini katta harfga aylantirish
names = Array.ConvertAll(names, name => name.ToUpper());
// Natijalarni ko'rsatish
Array.ForEach(names, Console.WriteLine);
Sintaktik Yorliq #3: Lokal O'zgaruvchilarni Callback Metodga Uzatish uchun Klassga O'rash Kerak Emas
Men allaqachon callback kodi klassda aniqlangan boshqa a'zolarga murojaat qilishi mumkinligini ko'rsatganman. Biroq, ba'zan callback kodi aniqlanayotgan metodda mavjud bo'lgan lokal parametrlar yoki o'zgaruvchilarga murojaat qilishni xohlashi mumkin. Mana qiziqarli misol:
internal sealed class AClass {
public static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo) {
// Ba'zi lokal o'zgaruvchilar
Int32[] squares = new Int32[numToDo];
AutoResetEvent done = new AutoResetEvent(false);
// Boshqa oqimlarda bir qancha vazifalarni bajarish
for (Int32 n = 0; n < squares.Length; n++) {
ThreadPool.QueueUserWorkItem(
obj => {
Int32 num = (Int32) obj;
// Bu vazifa odatda ko'proq vaqt sarflaydi
squares[num] = num * num;
// Agar oxirgi vazifa bo'lsa, asosiy oqimni davom ettirishga ruxsat
if (Interlocked.Decrement(ref numToDo) == 0)
done.Set();
},
n);
}
// Boshqa oqimlar tugashini kutish
done.WaitOne();
// Natijalarni ko'rsatish
for (Int32 n = 0; n < squares.Length; n++)
Console.WriteLine("Index {0}, Square={1}", n, squares[n]);
}
}
Bu misol C# ilgari juda murakkab bo'lgan narsani amalga oshirishni qanchalik osonlashtirishini ko'rsatadi. Oldingi metod bitta parametr (numToDo) va ikkita lokal o'zgaruvchi (squares va done) aniqlaydi. Lambda ifodaning tanasi bu o'zgaruvchilarga murojaat qiladi.
Endi tasavvur qiling, lambda ifoda tanasidagi kod alohida metodga joylashtiriladi (CLR talab qilganidek). O'zgaruvchilarning qiymatlari alohida metodga qanday uzatiladi? Buni amalga oshirishning yagona yo'li yangi yordamchi klass aniqlash bo'lib, u siz callback kodga uzatmoqchi bo'lgan har bir qiymat uchun maydonga ega bo'lishi kerak. Bundan tashqari, callback kodi bu yordamchi klassda instansiya metodi sifatida aniqlangan bo'lishi kerak. Keyin, UsingLocalVariablesInTheCallbackCode metodi yordamchi klassning instansiyasini yaratib, maydonlarni lokal o'zgaruvchilarning qiymatlaridan boshlaydi va delegat obyektini yordamchi obyekt/instansiya metodiga bog'lab yaratadi.
Bu juda mashaqqatli va xatoga moyil ish va, albatta, C# kompilyatori bularning barchasini siz uchun avtomatik bajaradi. Oldingi kodni yozganingizda, C# kompilyatori kodingizni taxminan quyidagicha qayta yozadi (sharhlar men tomonimdan kiritilgan):
internal sealed class AClass {
public static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo) {
// Ba'zi lokal o'zgaruvchilar
WaitCallback callback1 = null;
// Yordamchi klassning instansiyasini yaratish
<>c__DisplayClass2 class1 = new <>c__DisplayClass2();
// Yordamchi klass maydonlarini boshlash
class1.numToDo = numToDo;
class1.squares = new Int32[class1.numToDo];
class1.done = new AutoResetEvent(false);
// Boshqa oqimlarda vazifalarni bajarish
for (Int32 n = 0; n < class1.squares.Length; n++) {
if (callback1 == null) {
// Delegat obyektini yordamchi obyekt va
// uning anonim instansiya metodiga bog'lash
callback1 = new WaitCallback(
class1.<UsingLocalVariablesInTheCallbackCode>b__0);
}
ThreadPool.QueueUserWorkItem(callback1, n);
}
// Boshqa oqimlar tugashini kutish
class1.done.WaitOne();
// Natijalarni ko'rsatish
for (Int32 n = 0; n < class1.squares.Length; n++)
Console.WriteLine("Index {0}, Square={1}", n, class1.squares[n]);
}
// Yordamchi klassga g'alati nom beriladi
// va tashqaridan kirishni taqiqlash uchun private
[CompilerGenerated]
private sealed class <>c__DisplayClass2 : Object {
// Callback kodda ishlatiladigan har bir lokal o'zgaruvchi uchun bitta public maydon
public Int32[] squares;
public Int32 numToDo;
public AutoResetEvent done;
// Public parametrsiz konstruktor
public <>c__DisplayClass2() { }
// Callback kodni o'z ichiga olgan public instansiya metod
public void <UsingLocalVariablesInTheCallbackCode>b__0(Object obj) {
Int32 num = (Int32) obj;
squares[num] = num * num;
if (Interlocked.Decrement(ref numToDo) == 0)
done.Set();
}
}
}
Lambda ifoda kompilyator parametr/lokal o'zgaruvchilarni maydonlarga aylantirgan klass yaratishiga sabab bo'lganda, o'zgaruvchilar murojaat qilgan obyektlarning umri uzaytiriladi. Odatda parametr/lokal o'zgaruvchi metod ichida o'zgaruvchining oxirgi ishlatilishida amaliyot doirasidan chiqadi. Biroq, o'zgaruvchini maydonga aylantirish maydonni o'z ichiga olgan obyektning butun umri davomida tirik qolishiga olib keladi. Bu aksariyat ilovalarda katta muammo emas, lekin siz bundan xabardor bo'lishingiz kerak.
Delegatlar va Reflection
Shu paytgacha ushbu bobda delegatlardan foydalanish dasturchidan callback qilinadigan metod prototipini oldindan bilishini talab qildi. Masalan, agar fb Feedback delegatiga murojaat qiluvchi o'zgaruvchi bo'lsa, delegatni chaqirish uchun kod quyidagicha ko'rinadi:
fb(item); // item Int32 sifatida aniqlangan
Ko'rib turganingizdek, dasturchi kodni yozishda callback metod qancha parametr talab qilishi va bu parametrlarning turlarini bilishi kerak. Yaxshiyamki, dasturchi deyarli doimo bu ma'lumotga ega va shuning uchun oldingiday kod yozish muammo emas.
Biroq, ba'zi kamdan-kam hollarda dasturchi kompilyatsiya paytida bu ma'lumotga ega bo'lmaydi. Men buni 11-Bob "Hodisalar" da EventSet turini muhokama qilganimda ko'rsatgan edim. Bu misolda lug'at turli delegat turlarining to'plamini saqladi. Ish vaqtida hodisani qo'zg'atish uchun delegatlardan biri lug'atdan izlandi va chaqirildi. Kompilyatsiya paytida qaysi delegat chaqirilishini va delegatning callback metodiga qanday parametrlar kerakligini aniq bilish mumkin emas edi.
Yaxshiyamki, System.Reflection.MethodInfo kompilyatsiya paytida delegat haqidagi barcha kerakli ma'lumotga ega bo'lmaganingizda ham delegat yaratish imkonini beruvchi CreateDelegate metodini taklif qiladi:
public abstract class MethodInfo : MethodBase {
// Statik metodni o'raydigan delegat yaratish.
public virtual Delegate CreateDelegate(Type delegateType);
// Instansiya metodini o'raydigan delegat yaratish;
// target 'this' argumentiga murojaat qiladi.
public virtual Delegate CreateDelegate(Type delegateType, Object target);
}
Delegat yaratganingizdan keyin, uni Delegate ning DynamicInvoke metodi yordamida chaqirishingiz mumkin:
public abstract class Delegate {
// Delegatni parametrlarini uzatib chaqirish
public Object DynamicInvoke(params Object[] args);
}
Reflection API larini (23-Bob, "Assembly Yuklash va Reflection" da muhokama qilingan) ishlatib, siz avval delegat yaratmoqchi bo'lgan metodga murojaat qiluvchi MethodInfo obyektini olishingiz kerak. Keyin, CreateDelegate metodini chaqirib, birinchi parametr delegateType bilan aniqlangan Delegate dan hosila bo'lgan turning yangi obyektini yaratishiga buyurasiz. Agar delegat instansiya metodini o'rasa, siz CreateDelegate ga target parametrini ham uzatasiz, bu instansiya metodiga implicit this parametri sifatida uzatiladigan obyektni ko'rsatadi.
System.Delegate ning DynamicInvoke metodi delegat obyektining callback metodini ish vaqtida aniqlanadigan parametrlar to'plamini uzatib chaqirish imkonini beradi. DynamicInvoke chaqirilganda, u ichki tarzda siz uzatgan parametrlar callback metodi kutayotgan parametrlarga mos kelishini tekshiradi. Agar ular mos bo'lsa, callback metod chaqiriladi. Agar ular mos bo'lmasa, ArgumentException tashlanadi. DynamicInvoke callback metodi qaytargan obyektni qaytaradi.
Quyidagi kod CreateDelegate va DynamicInvoke metodlarini qanday ishlatishni ko'rsatadi:
using System;
using System.Reflection;
using System.IO;
// Turli delegat ta'riflari
internal delegate Object TwoInt32s(Int32 n1, Int32 n2);
internal delegate Object OneString(String s1);
public static class DelegateReflection {
public static void Main(String[] args) {
if (args.Length < 2) {
String usage =
@"Usage:" +
"{0} delType methodName [Arg1] [Arg2]" +
"{0} if delType is TwoInt32s, methodName must be Add or Subtract" +
"{0} if delType is OneString, methodName must be NumChars or Reverse" +
"{0}" +
"{0}Examples:" +
"{0} TwoInt32s Add 123 321" +
"{0} TwoInt32s Subtract 123 321" +
"{0} OneString NumChars \"Hello there\"" +
"{0} OneString Reverse \"Hello there\"";
Console.WriteLine(usage, Environment.NewLine);
return;
}
// delType argumentini delegat turiga aylantirish
Type delType = Type.GetType(args[0]);
if (delType == null) {
Console.WriteLine("Invalid delType argument: " + args[0]);
return;
}
Delegate d;
try {
// Arg1 argumentini metodga aylantirish
MethodInfo mi = typeof(DelegateReflection).GetTypeInfo().GetDeclaredMethod(args[1]);
// Statik metodni o'raydigan delegat obyektini yaratish
d = mi.CreateDelegate(delType);
}
catch (ArgumentException) {
Console.WriteLine("Invalid methodName argument: " + args[1]);
return;
}
// Faqat metod orqali delegat obyektiga uzatiladigan
// argumentlarni o'z ichiga olgan massiv yaratish
Object[] callbackArgs = new Object[args.Length - 2];
if (d.GetType() == typeof(TwoInt32s)) {
try {
// String argumentlarini Int32 argumentlarga aylantirish
for (Int32 a = 2; a < args.Length; a++)
callbackArgs[a - 2] = Int32.Parse(args[a]);
}
catch (FormatException) {
Console.WriteLine("Parameters must be integers.");
return;
}
}
if (d.GetType() == typeof(OneString)) {
// Shunchaki String argumentni nusxalash
Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);
}
try {
// Delegatni chaqirish va natijani ko'rsatish
Object result = d.DynamicInvoke(callbackArgs);
Console.WriteLine("Result = " + result);
}
catch (TargetParameterCountException) {
Console.WriteLine("Incorrect number of parameters specified.");
}
}
// Bu callback metod 2 ta Int32 argument qabul qiladi
private static Object Add(Int32 n1, Int32 n2) {
return n1 + n2;
}
// Bu callback metod 2 ta Int32 argument qabul qiladi
private static Object Subtract(Int32 n1, Int32 n2) {
return n1 - n2;
}
// Bu callback metod 1 ta String argument qabul qiladi
private static Object NumChars(String s1) {
return s1.Length;
}
// Bu callback metod 1 ta String argument qabul qiladi
private static Object Reverse(String s1) {
return new String(s1.Reverse().ToArray());
}
}