подія Дія <> подія EventHandler <>


144

Чи є різниця між оголошенням 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 <> буде занадто багато аргументів типу, потрібен буде додатковий клас.


2
plusOne (я просто бив систему) за "нікого не хвилює"
hyankov

@plusOne: Я фактично повинен знати відправника! Скажіть, що щось трапляється, і ви хочете знати, хто це зробив. Це вам знадобилося "джерело об'єкта" (він же відправник).
Kamran Bigdely

відправник може бути власністю у корисному навантаженні події
Thanasis Ioannidis

Відповіді:


67

Основна відмінність полягатиме в тому, що якщо ви використовуєте Action<>свою подію, ви не будете слідувати схемі дизайну практично будь-якої іншої події в системі, що я вважав би недоліком.

Одним із переваг домінуючого шаблону дизайну (крім сили однаковості) є те, що ви можете розширити EventArgsоб’єкт новими властивостями, не змінюючи підпис події. Це все-таки було б можливо, якщо ви використовували Action<SomeClassWithProperties>, але я не бачу сенсу в тому, щоб не використовувати звичайний підхід у такому випадку.


Чи можна використовувати Action<>результат у витоку пам'яті? Одним із недоліків EventHandlerдизайнерської схеми є витоки пам’яті. Також слід зазначити, що може бути декілька обробників подій, але лише одна дія
Luke T O'Brien

4
@ LukeTO'Brien: Події по суті є делегатами, тому такі самі можливості витоку пам'яті існують і для Action<T>. Крім того, Action<T> можна звернутися до декількох методів. Ось істота,
Fredrik Mörk

89

Грунтуючись на деяких попередніх відповідях, я буду розбивати свою відповідь на три області.

По-перше, фізичні обмеження використання Action<T1, T2, T2... >vs використання похідного класу EventArgs. Є три: По-перше, якщо ви зміните кількість або типи параметрів, кожен метод, на який ви підписуєтеся, повинен бути змінений, щоб відповідати новій схемі. Якщо це загальнодоступна подія, яку будуть використовувати сторонні збори, і є ймовірність зміни аргументів події, це буде приводом для використання спеціального класу, похідного від аргументів події, задля узгодженості (пам’ятайте, ви могли б досі використання an Action<MyCustomClass>) По-друге, використання Action<T1, T2, T2... >не дозволить вам передати зворотний зв'язок BACK на метод виклику, якщо у вас є якийсь об'єкт (наприклад, властивість Handled), який передається разом із дією. По-третє, ви не отримаєте іменованих параметрів, тож якщо ви передаєте 3 boolint, дваstringі, і DateTimeви, ви не маєте поняття, у чому значення цих цінностей. В якості побічної ноти, ви все ще можете мати "Безпечний спосіб події, коли все ще використовується Action<T1, T2, T2... >".

По-друге, послідовність наслідків. Якщо у вас є велика система, з якою ви вже працюєте, майже завжди краще слідувати тому, як розроблена решта системи, якщо у вас теж не дуже вагома причина. Якщо ви публічно стикаєтеся з подіями, які потрібно підтримувати, здатність замінювати похідні класи може бути важливою. Майте це на увазі.

По-третє, в реальній практиці я особисто вважаю, що я схильний створювати багато одноразових подій для таких речей, як зміни властивостей, з якими мені потрібно взаємодіяти (особливо під час роботи MVVM з переглядаючими моделями, які взаємодіють один з одним) або там, де подія має єдиний параметр. Більшість часу ці події проходять у формі public event Action<[classtype], bool> [PropertyName]Changed;або public event Action SomethingHappened;. У цих випадках є дві переваги. По-перше, я отримую тип для класу видачі. Якщо MyClassзаявляє і є єдиним класом, який запускає подію, я отримую явний екземпляр MyClassроботи з обробником подій. По-друге, для простих подій, таких як події зміни властивостей, значення параметрів очевидно і зазначено в імені обробника подій, і мені не потрібно створювати безліч класів для таких видів подій.


Дивовижна публікація в блозі. Безумовно, варто прочитати, якщо ви читаєте цю тему!
Вексир

1
Детальна і добре продумана відповідь, яка пояснює міркування, що стоять за висновком
MikeT

18

Здебільшого, я б сказав, дотримуйтесь схему. Я вже відхилився від нього, але дуже рідко, і з певних причин. У конкретному випадку, найбільша проблема, яку я мав би, полягає в тому, що я, мабуть, все-таки використовую Action<SomeObjectType>, що дозволяє мені додавати додаткові властивості пізніше та використовувати випадкові двосторонні властивості (подумайте Handled, чи інші відгуки-події, де абоненту потрібно встановити властивість на об'єкт події). І як тільки ви починаєте цю лінію, ви можете також використовувати EventHandler<T>для деяких T.


14

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

Використовуючи дію, як у вас є, немає способу сказати мені, що таке bool, int та Blah. Якщо ваша дія передала об'єкт, який визначив параметри, тоді добре.

Використовуючи EventHandler, який хотів EventArgs, і якщо ви доповнили свій приклад DiagnosticsArgs за допомогою Getters для властивостей, які коментують їх призначення, то ваша програма буде зрозумілішою. Також, будь ласка, прокоментуйте або повністю назвіть аргументи в конструкторі DiagnosticsArgs.


6

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

(Хоча я маю на увазі два думки, чи варто вам використовувати методи розширення на нульових об'єктах ...)

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> (ця дія <T> theEvent, T theEventArgs) повинна працювати, щоб ... і не потрібно використовувати "куди"
Beachwalker

6

Я усвідомлюю, що цьому питанню вже понад 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, щодо того, коли лямбда стане "живою" Це дуже відрізняється від запізнілого зв’язування делегата, що може траплятися не один раз (тобто лямбда завжди є, але прив'язка відбувається на вимогу так часто, як потрібно), на відміну від лямбда, яка, як тільки вона виникає, робиться - магія пішла, і метод (и) / властивість (и) завжди буде пов'язувати. Щось мати на увазі.


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 .
користувач1832484
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.