Я усвідомлюю, що цьому питанню вже понад 10 років, але мені здається, що не тільки найочевидніша відповідь не була розглянута, але й можливо, не дуже зрозуміло з питання добре розуміння того, що відбувається під прикриттями. Крім того, є й інші питання щодо пізньої прив'язки та того, що це означає стосовно делегатів та лямбдів (докладніше про це пізніше).
Спершу зверніться до слона / горили на 800 фунтів у кімнаті, коли вибирати eventvs Action<T>/ Func<T>:
- Використовуйте лямбда для виконання одного оператора чи методу. Використовуйте,
eventколи вам більше потрібна модель pub / sub, з декількома операторами / лямбдашами / функціями, які будуть виконуватись (це головна
відмінність прямо від бати).
- Використовуйте лямбда, коли ви хочете складати оператори / функції для дерев виразів. Використовуйте делегатів / події, коли ви хочете взяти участь у більш традиційних пізніх прив’язках, таких як, що використовуються в роздумах та взаємодії COM.
Як приклад події, наводимо простий і "стандартний" набір подій, використовуючи невеликий додаток консолі, як описано нижче:
public delegate void FireEvent(int num);
public delegate void FireNiceEvent(object sender, SomeStandardArgs args);
public class SomeStandardArgs : EventArgs
{
public SomeStandardArgs(string id)
{
ID = id;
}
public string ID { get; set; }
}
class Program
{
public static event FireEvent OnFireEvent;
public static event FireNiceEvent OnFireNiceEvent;
static void Main(string[] args)
{
OnFireEvent += SomeSimpleEvent1;
OnFireEvent += SomeSimpleEvent2;
OnFireNiceEvent += SomeStandardEvent1;
OnFireNiceEvent += SomeStandardEvent2;
Console.WriteLine("Firing events.....");
OnFireEvent?.Invoke(3);
OnFireNiceEvent?.Invoke(null, new SomeStandardArgs("Fred"));
//Console.WriteLine($"{HeightSensorTypes.Keyence_IL030}:{(int)HeightSensorTypes.Keyence_IL030}");
Console.ReadLine();
}
private static void SomeSimpleEvent1(int num)
{
Console.WriteLine($"{nameof(SomeSimpleEvent1)}:{num}");
}
private static void SomeSimpleEvent2(int num)
{
Console.WriteLine($"{nameof(SomeSimpleEvent2)}:{num}");
}
private static void SomeStandardEvent1(object sender, SomeStandardArgs args)
{
Console.WriteLine($"{nameof(SomeStandardEvent1)}:{args.ID}");
}
private static void SomeStandardEvent2(object sender, SomeStandardArgs args)
{
Console.WriteLine($"{nameof(SomeStandardEvent2)}:{args.ID}");
}
}
Вихід буде виглядати наступним чином:

Якби ви зробили те саме з Action<int>або Action<object, SomeStandardArgs>, ви побачили б тільки SomeSimpleEvent2і SomeStandardEvent2.
Отже, що відбувається всередині event?
Якщо ми розширимо FireNiceEvent, компілятор насправді генерує наступне (я опустив деякі деталі щодо синхронізації потоків, які не стосуються даного обговорення):
private EventHandler<SomeStandardArgs> _OnFireNiceEvent;
public void add_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
{
Delegate.Combine(_OnFireNiceEvent, handler);
}
public void remove_OnFireNiceEvent(EventHandler<SomeStandardArgs> handler)
{
Delegate.Remove(_OnFireNiceEvent, handler);
}
public event EventHandler<SomeStandardArgs> OnFireNiceEvent
{
add
{
add_OnFireNiceEvent(value)
}
remove
{
remove_OnFireNiceEvent(value)
}
}
Компілятор генерує приватну змінну делегата, яку не видно простору імен класів, в якій вона генерується. Цей делегат - це те, що використовується для управління підпискою та участі у пізній прив’язці, а інтерфейс, що стикається з громадськістю, - це знайоме +=і -=операторів, яких ми всі дізналися та любимо:
Ви можете налаштувати код для обробників додавання / видалення, змінивши область FireNiceEventделегата на захищену. Тепер це дозволяє розробникам додавати спеціальні гачки до гачків, наприклад, лісоруби або гачки безпеки. Це дійсно спричиняє деякі дуже потужні функції, які тепер дозволяють налаштувати доступ до підписки на основі ролей користувача тощо. Чи можете ви це зробити з лямбдами? (Насправді ви можете, скопіювавши власні дерева виразів, але це виходить за рамки цієї відповіді).
Щоб вирішити декілька пунктів з деяких відповідей тут:
Дійсно немає різниці у «крихкості» між зміною списку аргументів у Action<T>та зміною властивостей класу, похідного від EventArgs. Або не буде потрібно лише зміна компіляції, вони обидва змінять публічний інтерфейс і вимагатимуть версії. Без різниці.
Що стосується галузевого стандарту, це залежить від того, де це використовується і чому. Action<T>і такий часто використовується в IoC і DI, і eventчасто використовується в маршрутизації повідомлень, таких як рамки типу GUI та MQ. Зауважте, що я говорив часто , не завжди .
Делегати мають різний термін життя, ніж лямбда. Слід також усвідомлювати захоплення ... не лише із закриттям, а й з поняттям "дивись, що затягнула кішка". Це впливає на слід / термін служби пам’яті, а також на витоки управління.
Ще одне, на що я посилався раніше ... поняття пізнього зв’язування. Ви часто будете бачити це при використанні фреймворку, подібного до LINQ, щодо того, коли лямбда стане "живою" Це дуже відрізняється від запізнілого зв’язування делегата, що може траплятися не один раз (тобто лямбда завжди є, але прив'язка відбувається на вимогу так часто, як потрібно), на відміну від лямбда, яка, як тільки вона виникає, робиться - магія пішла, і метод (и) / властивість (и) завжди буде пов'язувати. Щось мати на увазі.