فصل ۴: Delegateها و Eventها در C#
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
۴. C# پیشرفته
در این فصل، موضوعات پیشرفتهٔ C# را پوشش میدهیم که بر مفاهیم بررسیشده در فصلهای ۲ و ۳ بنا شدهاند. چهار بخش نخست را باید بهترتیب بخوانید؛ بخشهای باقیمانده را میتوانید با هر ترتیبی مطالعه کنید.
Delegateها
Delegate شیئی است که میداند چگونه یک Method را فراخوانی کند.
یک Delegate Type نوع Methodی را تعریف میکند که نمونههای آن Delegate میتوانند فراخوانی کنند. بهطور مشخص، نوع بازگشتی Method و نوع Parameterهای آن را تعیین میکند. عبارت زیر یک Delegate Type به نام Transformer تعریف میکند:
delegate int Transformer (int x);
Transformer با هر Methodی که نوع بازگشتی int و یک Parameter از نوع int داشته باشد سازگار است، مانند:
int Square (int x) { return x * x; }
یا بهشکل کوتاهتر:
int Square (int x) => x * x;
اختصاص یک Method به متغیر Delegate، یک نمونهٔ Delegate ایجاد میکند:
Transformer t = Square;
میتوانید یک نمونهٔ Delegate را همانند یک Method فراخوانی کنید:
int answer = t(3); // answer is 9
نمونهٔ کامل:
Transformer t = Square; // Create delegate instance
int result = t(3); // Invoke delegate
Console.WriteLine (result); // 9
int Square (int x) => x * x;
delegate int Transformer (int x); // Delegate type declaration
یک نمونهٔ Delegate بهمعنای واقعی کلمه بهعنوان نمایندهٔ فراخواننده عمل میکند: فراخواننده Delegate را فراخوانی میکند و سپس Delegate، Method مقصد را صدا میزند. این سطحِ واسط، فراخواننده را از Method مقصد جدا میکند.
عبارت:
Transformer t = Square;
شکل کوتاهشدهٔ عبارت زیر است:
Transformer t = new Transformer (Square);
عبارت:
t(3)
شکل کوتاهشدهٔ این است:
t.Invoke(3)
نوشتن Methodهای Plug-in با Delegateها
در زمان اجرا یک Method به متغیر Delegate اختصاص داده میشود. این ویژگی برای نوشتن Methodهای Plug-in مفید است. در این مثال، Method کمکی Transform را داریم که یک تبدیل را روی هر عنصر از یک آرایهٔ عدد صحیح اعمال میکند. Method Transform یک Parameter از نوع Delegate دارد که میتوانید از آن برای مشخصکردن تبدیل Plug-in استفاده کنید:
int[] values = { 1, 2, 3 };
Transform (values, Square); // Hook in the Square method
foreach (int i in values)
Console.Write (i + " "); // 1 4 9
void Transform (int[] values, Transformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
int Square (int x) => x * x;
int Cube (int x) => x * x * x;
delegate int Transformer (int x);
تنها با تغییر Square به Cube در خط دوم کد، میتوانیم نوع تبدیل را عوض کنیم.
Method ما یعنی Transform یک Higher-Order Function است، زیرا تابعی است که یک تابع را بهعنوان Argument دریافت میکند. (Methodی که یک Delegate برگرداند نیز Higher-Order Function محسوب میشود.)
مقصدهای Instance Method و Static Method
Method مقصد یک Delegate میتواند Local، Static یا Instance باشد. نمونهٔ زیر یک Static Method را بهعنوان مقصد نشان میدهد:
Transformer t = Test.Square;
Console.WriteLine (t(10)); // 100
class Test { public static int Square (int x) => x * x; }
delegate int Transformer (int x);
نمونهٔ زیر یک Instance Method را بهعنوان مقصد نشان میدهد:
Test test = new Test();
Transformer t = test.Square;
Console.WriteLine (t(10)); // 100
class Test { public int Square (int x) => x * x; }
delegate int Transformer (int x);
وقتی یک Instance Method به یک شیء Delegate اختصاص داده میشود، Delegate نهتنها مرجعی به Method بلکه مرجعی به Instanceای که Method متعلق به آن است نیز نگه میدارد. Property با نام Target در کلاس System.Delegate همین Instance را نشان میدهد؛ و برای Delegateای که به Static Method اشاره دارد مقدار آن null است. مثال:
MyReporter r = new MyReporter();
r.Prefix = "%Complete: ";
ProgressReporter p = r.ReportProgress;
p(99); // %Complete: 99
Console.WriteLine (p.Target == r); // True
Console.WriteLine (p.Method); // Void ReportProgress(Int32)
r.Prefix = "";
p(99); // 99
public delegate void ProgressReporter (int percentComplete);
class MyReporter
{
public string Prefix = "";
public void ReportProgress (int percentComplete)
=> Console.WriteLine (Prefix + percentComplete);
}
از آنجا که Instance در Property با نام Target متعلق به Delegate ذخیره میشود، طول عمر آن دستکم تا زمانی که Delegate زنده است امتداد پیدا میکند.
Multicast Delegateها
تمام نمونههای Delegate قابلیت Multicast دارند. یعنی یک نمونهٔ Delegate میتواند نه فقط به یک Method مقصد، بلکه به فهرستی از Methodهای مقصد اشاره کند. Operatorهای + و += نمونههای Delegate را با هم ترکیب میکنند:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;
خط آخر از نظر عملکرد با عبارت زیر یکسان است:
d = d + SomeMethod2;
اکنون با فراخوانی d، هر دو Method یعنی SomeMethod1 و SomeMethod2 فراخوانی میشوند. Delegateها به همان ترتیبی اجرا میشوند که به آنها افزوده شدهاند.
Operatorهای - و -= Delegate سمت راست را از Delegate سمت چپ حذف میکنند:
d -= SomeMethod1;
اکنون فراخوانی d فقط باعث اجرای SomeMethod2 میشود.
استفاده از + یا += روی یک متغیر Delegate با مقدار null معتبر است و معادل اختصاص یک مقدار جدید به متغیر است:
SomeDelegate d = null;
d += SomeMethod1; // Equivalent (when d is null) to d = SomeMethod1;
به همین ترتیب، استفاده از -= روی متغیر Delegateای که فقط یک مقصدِ مطابق دارد، معادل اختصاص null به آن متغیر است.
اگر یک Multicast Delegate نوع بازگشتی غیر از void داشته باشد، فراخواننده مقدار بازگشتی آخرین Method فراخوانیشده را دریافت میکند. Methodهای قبلی همچنان اجرا میشوند اما مقدار بازگشتی آنها دور ریخته میشود. در بیشتر سناریوهایی که Multicast Delegateها استفاده میشوند، نوع بازگشتی آنها void است، بنابراین این نکته مطرح نمیشود.
نمونهٔ Multicast Delegate
فرض کنید Methodی نوشتهاید که اجرای آن زمان زیادی طول میکشد. آن Method میتواند با فراخوانی یک Delegate، بهطور منظم پیشرفت کار را به فراخواننده گزارش کند. در این مثال، Method با نام HardWork یک Parameter از نوع Delegate به نام ProgressReporter دارد و برای اعلام پیشرفت آن را فراخوانی میکند:
public delegate void ProgressReporter (int percentComplete);
public class Util
{
public static void HardWork (ProgressReporter p)
{
for (int i = 0; i < 10; i++)
{
p (i * 10); // Invoke delegate
System.Threading.Thread.Sleep (100); // Simulate hard work
}
}
}
برای پایش پیشرفت، میتوانیم یک نمونهٔ Multicast Delegate به نام p بسازیم بهگونهای که پیشرفت توسط دو Method مستقل پایش شود:
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile;
Util.HardWork (p);
void WriteProgressToConsole (int percentComplete)
=> Console.WriteLine (percentComplete);
void WriteProgressToFile (int percentComplete)
=> System.IO.File.WriteAllText ("progress.txt",
percentComplete.ToString());
Generic Delegate Typeها
یک Delegate Type میتواند Generic Type Parameter داشته باشد:
public delegate T Transformer<T> (T arg);
با این تعریف، میتوانیم Method کمکی Transform را بهشکل عمومیتری بنویسیم تا با هر Type کار کند:
int[] values = { 1, 2, 3 };
Util.Transform (values, Square); // Hook in Square
foreach (int i in values)
Console.Write (i + " "); // 1 4 9
int Square (int x) => x * x;
public class Util
{
public static void Transform<T> (T[] values, Transformer<T> t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
}
Delegateهای Func و Action
با Generic Delegateها میتوان مجموعهٔ کوچکی از Delegate Typeها نوشت که آنقدر عمومیاند که با Methodهایی با هر نوع بازگشتی و هر تعداد معقولی از Argumentها کار کنند. اینها Delegateهای Func و Action هستند که در Namespace با نام System تعریف شدهاند. علامتهای in و out نشاندهندهٔ Variance هستند که کمی بعد در زمینهٔ Delegateها بررسی میکنیم:
delegate TResult Func <out TResult> ();
delegate TResult Func <in T, out TResult> (T arg);
delegate TResult Func <in T1, in T2, out TResult> (T1 arg1, T2 arg2);
... and so on, up to T16
delegate void Action ();
delegate void Action <in T> (T arg);
delegate void Action <in T1, in T2> (T1 arg1, T2 arg2);
... and so on, up to T16
این Delegateها بسیار عمومیاند. Delegate با نام Transformer در مثال قبلی را میتوان با یک Delegate از نوع Func جایگزین کرد که یک Argument از نوع T میگیرد و مقداری از همان Type برمیگرداند:
public static void Transform<T> (T[] values, Func<T,T> transformer)
{
for (int i = 0; i < values.Length; i++)
values[i] = transformer (values[i]);
}
تنها سناریوهای عملی که این Delegateها پوشش نمیدهند، Parameterهای ref/out و Pointerها هستند.
Delegateها در برابر Interfaceها
مسئلهای که با Delegate حل میشود، میتواند با Interface نیز حل شود. برای مثال میتوانیم مثال اولیه را بهجای Delegate با Interfaceی به نام ITransformer بازنویسی کنیم:
int[] values = { 1, 2, 3 };
Util.TransformAll (values, new Squarer());
foreach (int i in values)
Console.WriteLine (i);
public interface ITransformer
{
int Transform (int x);
}
public class Util
{
public static void TransformAll (int[] values, ITransformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t.Transform (values[i]);
}
}
class Squarer : ITransformer
{
public int Transform (int x) => x * x;
}
اگر یک یا چند مورد از شرایط زیر برقرار باشد، طراحی مبتنی بر Delegate میتواند انتخاب بهتری نسبت به طراحی مبتنی بر Interface باشد:
- Interface فقط یک Method تعریف کند.
- به قابلیت Multicast نیاز باشد.
- Subscriber لازم باشد Interface را چند بار پیادهسازی کند.
در مثال ITransformer به Multicast نیاز نداریم. با این حال، Interface فقط یک Method تعریف میکند. علاوهبراین، Subscriber ممکن است لازم باشد ITransformer را چند بار پیادهسازی کند تا تبدیلهای متفاوتی مانند مربع و مکعب را پشتیبانی کند. با Interface مجبوریم برای هر تبدیل یک Type جداگانه بنویسیم، زیرا یک Class فقط یکبار میتواند ITransformer را پیادهسازی کند. این کار نسبتاً دستوپاگیر است:
int[] values = { 1, 2, 3 };
Util.TransformAll (values, new Cuber());
foreach (int i in values)
Console.WriteLine (i);
class Squarer : ITransformer
{
public int Transform (int x) => x * x;
}
class Cuber : ITransformer
{
public int Transform (int x) => x * x * x;
}
سازگاری Delegateها
سازگاری Type
همهٔ Delegate Typeها با یکدیگر ناسازگارند، حتی اگر Signature یکسانی داشته باشند:
D1 d1 = Method1;
D2 d2 = d1; // Compile-time error
void Method1() { }
delegate void D1();
delegate void D2();
نمونههای Delegate زمانی برابر در نظر گرفته میشوند که Method مقصد یکسانی داشته باشند:
D d1 = Method1;
D d2 = Method1;
Console.WriteLine (d1 == d2); // True
void Method1() { }
delegate void D();
Multicast Delegateها زمانی برابر هستند که به همان Methodها و با همان ترتیب اشاره کنند.
سازگاری Parameter
هنگام فراخوانی یک Method میتوانید Argumentهایی با Type خاصتر از Parameterهای آن Method بدهید. این رفتار معمول Polymorphism است. به همین دلیل، Delegate نیز میتواند Parameter Typeهایی خاصتر از Method مقصد خود داشته باشد. به این ویژگی Contravariance گفته میشود. مثال:
StringAction sa = new StringAction (ActOnObject);
sa ("hello");
void ActOnObject (object o) => Console.WriteLine (o); // hello
delegate void StringAction (string s);
همانند Variance در Type Parameter، Delegateها نیز فقط برای Reference Conversionها Variant هستند.
Delegate صرفاً یک Method را از طرف شخص دیگری فراخوانی میکند. در این نمونه، StringAction با Argumentی از Type string فراخوانی میشود. وقتی Argument به Method مقصد منتقل میشود، بهطور ضمنی به object Upcast میشود.
سازگاری نوع بازگشتی
هنگام فراخوانی یک Method ممکن است Typeای خاصتر از چیزی که درخواست کردهاید برگردد؛ این نیز رفتار معمول Polymorphism است. به همین دلیل، Method مقصد یک Delegate میتواند Typeای خاصتر از Type تعریفشده در Delegate برگرداند. این ویژگی Covariance نام دارد:
ObjectRetriever o = new ObjectRetriever (RetrieveString);
object result = o();
Console.WriteLine (result); // hello
string RetrieveString() => "hello";
delegate object ObjectRetriever();
ObjectRetriever انتظار دارد یک object دریافت کند، اما Subclassِ object نیز قابل قبول است؛ بنابراین نوع بازگشتی Delegateها Covariant است.
Variance در Generic Delegate Type Parameterها
در فصل ۳ دیدیم که Generic Interfaceها از Type Parameterهای Covariant و Contravariant پشتیبانی میکنند. همین قابلیت برای Delegateها نیز وجود دارد.
اگر یک Generic Delegate Type تعریف میکنید، انجام موارد زیر روش خوبی است:
- Type Parameterی را که فقط در مقدار بازگشتی استفاده میشود با
out بهعنوان Covariant علامتگذاری کنید. - Type Parameterهایی را که فقط در Parameterها استفاده میشوند با
in بهعنوان Contravariant علامتگذاری کنید.
این کار باعث میشود Conversionها با رعایت طبیعی روابط Inheritance بین Typeها عمل کنند.
Delegate زیر که در Namespace با نام System تعریف شده، یک TResult از نوع Covariant دارد:
delegate TResult Func<out TResult>();
بنابراین عبارت زیر مجاز است:
Func<string> x = ...;
Func<object> y = x;
Delegate زیر نیز که در System تعریف شده، T از نوع Contravariant دارد:
delegate void Action<in T> (T arg);
در نتیجه این تبدیل مجاز است:
Action<object> x = ...;
Action<string> y = x;
Eventها
هنگام استفاده از Delegateها معمولاً دو نقش شکل میگیرد: Broadcaster و Subscriber.
Broadcaster یک Type است که یک Delegate Field دارد. Broadcaster با فراخوانی Delegate تعیین میکند چه زمانی Broadcast انجام شود.
Subscriberها گیرندههای Method مقصد هستند. Subscriber با اجرای += و -= روی Delegate مربوط به Broadcaster تعیین میکند از چه زمانی گوشدادن را آغاز یا متوقف کند. یک Subscriber از Subscriberهای دیگر اطلاعی ندارد و در کار آنها دخالت نمیکند.
Event یک ویژگی زبان است که این الگو را رسمی میکند. Event ساختاری است که فقط زیرمجموعهای از قابلیتهای Delegate را که برای مدل Broadcaster/Subscriber لازم است در معرض دید قرار میدهد. هدف اصلی Eventها جلوگیری از دخالت Subscriberها در کار یکدیگر است.
سادهترین راه اعلان Event این است که کلمهٔ کلیدی event را پیش از یک عضو Delegate قرار دهید:
// Delegate definition
public delegate void PriceChangedHandler (decimal oldPrice,
decimal newPrice);
public class Broadcaster
{
// Event declaration
public event PriceChangedHandler PriceChanged;
}
کد داخل Type با نام Broadcaster دسترسی کامل به PriceChanged دارد و میتواند با آن مانند یک Delegate رفتار کند. کد خارج از Broadcaster فقط میتواند عملیات += و -= را روی Event با نام PriceChanged انجام دهد.
مثال زیر را در نظر بگیرید. Class با نام Stock هر بار که مقدار Property با نام Price تغییر میکند Event با نام PriceChanged را Fire میکند:
public delegate void PriceChangedHandler (decimal oldPrice,
decimal newPrice);
public class Stock
{
string symbol;
decimal price;
public Stock (string symbol) => this.symbol = symbol;
public event PriceChangedHandler PriceChanged;
public decimal Price
{
get => price;
set
{
if (price == value) return; // Exit if nothing has changed
decimal oldPrice = price;
price = value;
if (PriceChanged != null) // If invocation list not
PriceChanged (oldPrice, price); // empty, fire event.
}
}
}
اگر کلمهٔ کلیدی event را از مثال حذف کنیم تا PriceChanged به یک Delegate Field معمولی تبدیل شود، نتیجهٔ مثال همان خواهد بود. بااینحال Class با نام Stock استحکام کمتری خواهد داشت، زیرا Subscriberها میتوانند با انجام کارهای زیر در عملکرد یکدیگر دخالت کنند:
- با Assignment مجدد به
PriceChanged، Subscriberهای دیگر را جایگزین کنند، بهجای استفاده از Operator با نام +=. - همهٔ Subscriberها را با قرار دادن
PriceChanged روی null پاک کنند. - با فراخوانی Delegate برای Subscriberهای دیگر Broadcast انجام دهند.
الگوی استاندارد Event
تقریباً در همهٔ مواردی که Eventها در Libraryهای .NET تعریف میشوند، تعریف آنها از یک الگوی استاندارد پیروی میکند که برای ایجاد سازگاری بین کد Library و کد کاربر طراحی شده است. در مرکز این الگو Class از پیش تعریفشدهٔ System.EventArgs قرار دارد که جز Field استاتیک Empty عضوی ندارد. EventArgs یک Base Class برای انتقال اطلاعات مربوط به Event است. در مثال Stock، برای انتقال قیمت قبلی و قیمت جدید هنگام Fire شدن Event با نام PriceChanged، از EventArgs Subclass میسازیم:
public class PriceChangedEventArgs : System.EventArgs
{
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangedEventArgs (decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice;
NewPrice = newPrice;
}
}
برای قابلیت استفادهٔ مجدد، نام Subclass از EventArgs براساس اطلاعاتی که حمل میکند انتخاب میشود، نه براساس Eventی که قرار است در آن استفاده شود. معمولاً دادهها را بهصورت Property یا Fieldهای Read-only در معرض دید میگذارد.
پس از آمادهشدن Subclass از EventArgs، مرحلهٔ بعد انتخاب یا تعریف Delegate برای Event است. سه قانون وجود دارد:
- نوع بازگشتی باید
void باشد. - باید دو Argument بپذیرد: اولی از Type
object و دومی یک Subclass از EventArgs. Argument نخست Broadcaster مربوط به Event را مشخص میکند و Argument دوم اطلاعات اضافی را برای انتقال در خود دارد. - نام آن باید با
EventHandler پایان یابد.
.NET برای کمک به این کار Generic Delegateای با نام System.EventHandler<> تعریف میکند:
public delegate void EventHandler<TEventArgs> (object source, TEventArgs e)
مرحلهٔ بعد تعریف Event از Delegate Type انتخابشده است. در اینجا از Generic Delegate با نام EventHandler استفاده میکنیم:
public class Stock
{
...
public event EventHandler<PriceChangedEventArgs> PriceChanged;
}
در نهایت، الگو از شما میخواهد یک Method از نوع protected virtual بنویسید که Event را Fire کند. نام Method باید با نام Event یکسان باشد اما واژهٔ On در ابتدای آن قرار گیرد و یک Argument از نوع EventArgs دریافت کند:
public class Stock
{
...
public event EventHandler<PriceChangedEventArgs> PriceChanged;
protected virtual void OnPriceChanged (PriceChangedEventArgs e)
{
if (PriceChanged != null) PriceChanged (this, e);
}
}
این ساختار یک نقطهٔ مرکزی فراهم میکند که Subclassها میتوانند از آنجا Event را فراخوانی یا Override کنند، با فرض اینکه Class از نوع sealed نباشد.
مثال کامل:
using System;
Stock stock = new Stock ("THPW");
stock.Price = 27.10M;
// Register with the PriceChanged event
stock.PriceChanged += stock_PriceChanged;
stock.Price = 31.59M;
void stock_PriceChanged (object sender, PriceChangedEventArgs e)
{
if ((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M)
Console.WriteLine ("Alert, 10% stock price increase!");
}
public class PriceChangedEventArgs : EventArgs
{
public readonly decimal LastPrice;
public readonly decimal NewPrice;
public PriceChangedEventArgs (decimal lastPrice, decimal newPrice)
{
LastPrice = lastPrice; NewPrice = newPrice;
}
}
public class Stock
{
string symbol;
decimal price;
public Stock (string symbol) => this.symbol = symbol;
public event EventHandler<PriceChangedEventArgs> PriceChanged;
protected virtual void OnPriceChanged (PriceChangedEventArgs e)
{
PriceChanged?.Invoke (this, e);
}
public decimal Price
{
get => price;
set
{
if (price == value) return;
decimal oldPrice = price;
price = value;
OnPriceChanged (new PriceChangedEventArgs (oldPrice, price));
}
}
}
وقتی یک Event اطلاعات اضافی حمل نمیکند، میتوان از Delegate از پیش تعریفشده و غیرGeneric با نام EventHandler استفاده کرد. در این مثال، Stock را بهگونهای بازنویسی میکنیم که Event با نام PriceChanged پس از تغییر قیمت Fire شود و هیچ اطلاعاتی دربارهٔ Event جز رخدادن آن لازم نباشد. همچنین برای جلوگیری از ساخت غیرضروری یک Instance از EventArgs از Property با نام EventArgs.Empty استفاده میکنیم:
public class Stock
{
string symbol;
decimal price;
public Stock (string symbol) { this.symbol = symbol; }
public event EventHandler PriceChanged;
protected virtual void OnPriceChanged (EventArgs e)
{
PriceChanged?.Invoke (this, e);
}
public decimal Price
{
get { return price; }
set
{
if (price == value) return;
price = value;
OnPriceChanged (EventArgs.Empty);
}
}
}
Event Accessorها
Accessorهای یک Event پیادهسازی Functionهای += و -= آن هستند. بهطور پیشفرض، Compiler این Accessorها را بهصورت ضمنی پیادهسازی میکند. اعلان Event زیر را در نظر بگیرید:
public event EventHandler PriceChanged;
Compiler آن را به موارد زیر تبدیل میکند:
- یک Delegate Field خصوصی.
- یک جفت Function عمومی برای Event Accessorها با نامهای
add_PriceChanged و remove_PriceChanged که پیادهسازی آنها عملیات += و -= را به Delegate Field خصوصی هدایت میکند.
با تعریف Event Accessorهای صریح میتوانید کنترل این فرایند را در دست بگیرید. پیادهسازی دستی Event با نام PriceChanged از مثال قبلی:
private EventHandler priceChanged; // Declare a private delegate
public event EventHandler PriceChanged
{
add { priceChanged += value; }
remove { priceChanged -= value; }
}
این مثال از نظر عملکرد با پیادهسازی پیشفرض Accessor در C# یکسان است؛ با این تفاوت که C# هنگام بهروزرسانی Delegate با یک الگوریتم Lock-free از نوع Compare-and-swap، Thread Safety را نیز تضمین میکند. مرجع ذکرشده در متن اصلی: albahari.com/threading. با تعریف Event Accessorها بهصورت دستی، به C# دستور میدهیم منطق پیشفرض Field و Accessor را تولید نکند.
با Event Accessorهای صریح، میتوانید راهبردهای پیچیدهتری برای ذخیرهسازی و دسترسی به Delegate زیرین بهکار ببرید. سه سناریو وجود دارد که این قابلیت در آنها مفید است:
- وقتی Event Accessorها صرفاً Relay برای Class دیگری باشند که Event را Broadcast میکند.
- وقتی Class تعداد زیادی Event در معرض دید قرار میدهد اما بیشتر اوقات تعداد Subscriberها بسیار کم است، مانند یک Windows Control. در چنین مواردی بهتر است Instanceهای Delegate متعلق به Subscriberها در یک Dictionary ذخیره شوند، زیرا Dictionary سربار حافظهٔ کمتری نسبت به دهها Reference به Delegate Field با مقدار
null دارد. - وقتی Interfaceای را که Event اعلان میکند بهصورت صریح پیادهسازی میکنید.
در ادامه مثالی میآید که مورد آخر را نشان میدهد: