Я понимаю, что этому вопросу более 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.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делегата на protected. Теперь это позволяет разработчикам добавлять собственные хуки к хукам, такие как логирование или хуки безопасности. Это действительно создает некоторые очень мощные функции, которые теперь позволяют настраивать доступ к подписке на основе ролей пользователей и т. Д. Можно ли это сделать с помощью лямбда-выражений? (На самом деле вы можете самостоятельно компилировать деревья выражений, но это выходит за рамки этого ответа).
Чтобы ответить на пару моментов из некоторых ответов здесь:
На самом деле нет никакой разницы в «хрупкости» между изменением списка аргументов Action<T>и изменением свойств в классе, производном от EventArgs. Любой из них не только потребует изменения компиляции, но и изменит публичный интерфейс, и потребует управления версиями. Нет разницы.
Что касается отраслевого стандарта, это зависит от того, где он используется и почему. Action<T>и это часто используется в IoC и DI, и eventчасто используется в маршрутизации сообщений, например в структурах типа GUI и MQ. Заметьте, что я говорил часто , не всегда .
У делегатов другое время жизни, чем у лямбд. Также нужно знать о захвате ... не только с закрытием, но и с понятием «посмотрите, что затащила кошка». Это влияет на объем памяти / время жизни, а также на утечки управления.
Еще одна вещь, о которой я упоминал ранее ... понятие позднего связывания. Вы часто будете видеть это при использовании фреймворка, такого как LINQ, относительно того, когда лямбда становится «живой». Это сильно отличается от позднего связывания делегата, которое может происходить более одного раза (т. Е. Лямбда присутствует всегда, но связывание происходит по запросу так часто, как это необходимо), в отличие от лямбда-выражения, которое, как только оно происходит, выполняется - магия ушла, и методы / свойства всегда будут связываться. Что нужно иметь в виду.