событие Action <> против события EventHandler <>


148

Есть ли разница между объявлением event Action<>и event EventHandler<>.

Предполагая, что не имеет значения, какой объект на самом деле вызвал событие.

например:

public event Action<bool, int, Blah> DiagnosticsEvent;

против

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

class DiagnosticsArgs : EventArgs
{
    public DiagnosticsArgs(bool b, int i, Blah bl)
    {...}
    ...
}

использование будет почти одинаковым в обоих случаях:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

Есть несколько вещей, которые мне не нравятся в event EventHandler<>паттернах:

  • Объявление дополнительного типа, производное от EventArgs
  • Обязательная передача источника объекта - часто никого не волнует

Больше кода означает больше кода для поддержки без каких-либо явных преимуществ.

В результате я предпочитаю event Action<>

Однако дополнительный класс может потребоваться только в том случае, если в Action <> слишком много аргументов типа.


3
plusOne (я просто обыграл систему) за «всем
— плевать

@plusOne: мне действительно нужно знать отправителя! Скажите, что что-то случилось, и вы хотите знать, кто это сделал. Вот где вам нужен «источник объекта» (он же отправитель).
— Kamran Bigdely

отправитель может быть свойством в полезной нагрузке события
— Танасис Иоаннидис

Ответы:


67

Основное отличие будет заключаться в том, что если вы используете Action<>свое событие, оно не будет следовать шаблону проектирования практически любого другого события в системе, что я считаю недостатком.

Одним из преимуществ доминирующего шаблона проектирования (помимо силы сходства) является то, что вы можете расширить EventArgsобъект с помощью новых свойств, не изменяя сигнатуру события. Это все равно было бы возможно, если бы вы использовали Action<SomeClassWithProperties>, но я действительно не вижу смысла в том, чтобы не использовать обычный подход в этом случае.


Могло ли использование Action<>привести к утечке памяти? Один из недостатков EventHandlerшаблона проектирования - утечки памяти. Также следует отметить, что может быть несколько обработчиков событий, но только одно действие
— Люк Т О'Брайен

4
@ LukeTO'Brien: События, по сути, являются делегатами, поэтому существуют те же возможности утечки памяти с Action<T>. Также Action<T> может относиться к нескольким методам. Вот суть, которая демонстрирует это: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
— Фредрик Мёрк

91

Основываясь на некоторых из предыдущих ответов, я собираюсь разбить свой ответ на три области.

Во-первых, физические ограничения использования Action<T1, T2, T2... >vs с использованием производного класса EventArgs. Их три: во-первых, если вы измените количество или типы параметров, каждый метод, на который подписывается, необходимо будет изменить, чтобы соответствовать новому шаблону. Если это публичное событие, которое будут использовать сторонние сборки, и есть какая-либо вероятность того, что аргументы события могут измениться, это может быть причиной для использования настраиваемого класса, производного от аргументов событий, для обеспечения согласованности (помните, вы все еще МОЖЕТЕ use an Action<MyCustomClass>) Во-вторых, использование Action<T1, T2, T2... >не позволит вам передать обратную связь BACK вызывающему методу, если у вас нет какого-либо объекта (например, со свойством Handled), который передается вместе с Action. В- третьих, вы не получите именованные параметры, так что если вы передаете 3 bool«S int, дваstring's и a DateTime, вы не понимаете, что означают эти значения. В качестве примечания, вы все еще можете иметь «Метод безопасного запуска этого события, продолжая использовать Action<T1, T2, T2... >».

Во-вторых, последствия согласованности. Если у вас есть большая система, с которой вы уже работаете, почти всегда лучше следовать тому, как спроектирована остальная система, если только у вас нет очень веской причины. Если у вас есть публичные события, которые необходимо поддерживать, возможность замены производных классов может быть важной. Запомни.

В-третьих, на практике я лично обнаружил, что склонен создавать много разовых событий для таких вещей, как изменения свойств, с которыми мне нужно взаимодействовать (особенно при выполнении MVVM с моделями представлений, которые взаимодействуют друг с другом) или там, где событие имеет единственный параметр. В большинстве случаев эти события принимают форму public event Action<[classtype], bool> [PropertyName]Changed;или public event Action SomethingHappened;. В этих случаях есть два преимущества. Сначала я получаю тип для выпускающего класса. Если MyClassобъявляет и является единственным классом, запускающим событие, я получаю явный экземпляр MyClassдля работы в обработчике событий. Во-вторых, для простых событий, таких как события изменения свойств, значение параметров очевидно и указано в имени обработчика событий, и мне не нужно создавать множество классов для событий такого типа.


Замечательный пост в блоге. Определенно стоит прочитать, если вы читаете эту ветку!
— Vexir

1
Подробный и хорошо продуманный ответ, объясняющий аргументы, лежащие в основе вывода
— MikeT

18

По большей части я бы сказал, следуя шаблону. Я уже отклонился от него, но очень редко, и по определенным причинам. В данном случае самая большая проблема, с которой я столкнулся, заключается в том, что я, вероятно, все еще использую Action<SomeObjectType>, позволяя мне добавлять дополнительные свойства позже и использовать случайное двухстороннее свойство (думать Handledили другие события обратной связи, где подписчик должен установить свойство для объекта события). И как только вы начнете двигаться по этой линии, вы можете использовать EventHandler<T>для некоторых T.


14

Преимущество более многословного подхода возникает, когда ваш код находится внутри проекта из 300 000 строк.

Используя действие, как и у вас, невозможно сказать мне, что такое bool, int и Blah. Если ваше действие передало объект, который определил параметры, тогда нормально.

Используя EventHandler, которому нужен EventArgs, и если бы вы завершили свой пример DiagnosticsArgs с геттерами для свойств, которые прокомментировали их назначение, ваше приложение было бы более понятным. Кроме того, прокомментируйте или полностью назовите аргументы в конструкторе DiagnosticsArgs.


6

Если вы следуете стандартному шаблону событий, вы можете добавить метод расширения, чтобы сделать проверку запуска события более безопасной / простой. (т.е. следующий код добавляет метод расширения SafeFire (), который выполняет проверку на null, а также (очевидно) копирует событие в отдельную переменную, чтобы обезопасить себя от обычного состояния гонки null, которое может влиять на события.)

(Хотя я совершенно не понимаю, следует ли вам использовать методы расширения для нулевых объектов ...)

public static class EventFirer
{
    public static void SafeFire<TEventArgs>(this EventHandler<TEventArgs> theEvent, object obj, TEventArgs theEventArgs)
        where TEventArgs : EventArgs
    {
        if (theEvent != null)
            theEvent(obj, theEventArgs);
    }
}

class MyEventArgs : EventArgs
{
    // Blah, blah, blah...
}

class UseSafeEventFirer
{
    event EventHandler<MyEventArgs> MyEvent;

    void DemoSafeFire()
    {
        MyEvent.SafeFire(this, new MyEventArgs());
    }

    static void Main(string[] args)
    {
        var x = new UseSafeEventFirer();

        Console.WriteLine("Null:");
        x.DemoSafeFire();

        Console.WriteLine();

        x.MyEvent += delegate { Console.WriteLine("Hello, World!"); };
        Console.WriteLine("Not null:");
        x.DemoSafeFire();
    }
}

4
... разве вы не можете сделать то же самое с Action <T>? SafeFire <T> (этот Action <T> theEvent, T theEventArgs) должен работать до ... и нет необходимости использовать «где»
— Beachwalker

5

Я понимаю, что этому вопросу более 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делегата на protected. Теперь это позволяет разработчикам добавлять собственные хуки к хукам, такие как логирование или хуки безопасности. Это действительно создает некоторые очень мощные функции, которые теперь позволяют настраивать доступ к подписке на основе ролей пользователей и т. Д. Можно ли это сделать с помощью лямбда-выражений? (На самом деле вы можете самостоятельно компилировать деревья выражений, но это выходит за рамки этого ответа).

Чтобы ответить на пару моментов из некоторых ответов здесь:

  • На самом деле нет никакой разницы в «хрупкости» между изменением списка аргументов Action<T>и изменением свойств в классе, производном от EventArgs. Любой из них не только потребует изменения компиляции, но и изменит публичный интерфейс, и потребует управления версиями. Нет разницы.

  • Что касается отраслевого стандарта, это зависит от того, где он используется и почему. Action<T>и это часто используется в IoC и DI, и eventчасто используется в маршрутизации сообщений, например в структурах типа GUI и MQ. Заметьте, что я говорил часто , не всегда .

  • У делегатов другое время жизни, чем у лямбд. Также нужно знать о захвате ... не только с закрытием, но и с понятием «посмотрите, что затащила кошка». Это влияет на объем памяти / время жизни, а также на утечки управления.

Еще одна вещь, о которой я упоминал ранее ... понятие позднего связывания. Вы часто будете видеть это при использовании фреймворка, такого как LINQ, относительно того, когда лямбда становится «живой». Это сильно отличается от позднего связывания делегата, которое может происходить более одного раза (т. Е. Лямбда присутствует всегда, но связывание происходит по запросу так часто, как это необходимо), в отличие от лямбда-выражения, которое, как только оно происходит, выполняется - магия ушла, и методы / свойства всегда будут связываться. Что нужно иметь в виду.


Может у этого есть TL; DR? Я изо всех сил , чтобы понять , почему это ответить на вопрос , чтобы принять решение, и почему лямбды будут , когда упоминалось , что мы говорим о event Action<T>против EventHandler. Кроме того, я тестировал замену public static event FireEvent OnFireEvent;на public static event Action<int> OnFireEvent;и другой, и происходит то же самое. Что я сделал не так ?
— Раменамая,

4

Рассматривая стандартные шаблоны событий .NET, мы находим

Стандартная подпись делегата события .NET:

void OnEventRaised(object sender, EventArgs args);

[...]

Список аргументов содержит два аргумента: отправитель и аргументы события. Тип отправителя во время компиляции - System.Object, хотя вы, вероятно, знаете более производный тип, который всегда будет правильным. По соглашению используйте объект .

Ниже на той же странице мы находим пример типичного определения события, которое выглядит примерно так:

public event EventHandler<EventArgs> EventName;

Если бы мы определили

class MyClass
{
  public event Action<MyClass, EventArgs> EventName;
}

обработчик мог быть

void OnEventRaised(MyClass sender, EventArgs args);

где senderимеет правильный ( более производный ) тип.


Извините, что не заметил, что разница заключается в подписи обработчика, которая выиграла бы от более точного ввода sender .
— user1832484
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.