Event Action <> vs event EventHandler <>


144

Czy jest jakaś różnica między deklarowaniem event Action<>a event EventHandler<>.

Zakładając, że nie ma znaczenia, który obiekt faktycznie wywołał zdarzenie.

na przykład:

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

vs

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

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

użycie byłoby prawie takie samo w obu przypadkach:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

Jest kilka rzeczy, których nie lubię we event EventHandler<>wzorach:

  • Dodatkowa deklaracja typu pochodząca z EventArgs
  • Przymusowe przekazanie źródła obiektu - często nikogo to nie obchodzi

Więcej kodu oznacza więcej kodu do utrzymania bez wyraźnej przewagi.

W rezultacie wolę event Action<>

Jednak tylko wtedy, gdy w Action <> jest zbyt wiele argumentów typu, wymagana byłaby dodatkowa klasa.


2
plusOne (właśnie pokonałem system) za „nikt nie obchodzi”
hyankov

@plusOne: Właściwie muszę znać nadawcę! Powiedz, że coś się dzieje i chcesz wiedzieć, kto to zrobił. Tam właśnie potrzebowałeś „źródła obiektu” (aka nadawcy).
Kamran Bigdely

nadawcą może być właściwość w ładunku zdarzenia
Thanasis Ioannidis

Odpowiedzi:


67

Główną różnicą będzie to, że jeśli wykorzystasz Action<>swoje wydarzenie, nie będzie ono zgodne z wzorcem projektowym praktycznie żadnego innego wydarzenia w systemie, co uważam za wadę.

Jedną z zalet dominującego wzorca projektowego (oprócz siły podobieństwa) jest to, że możesz rozszerzyć EventArgsobiekt o nowe właściwości bez zmiany sygnatury zdarzenia. Byłoby to nadal możliwe, gdybyś użył Action<SomeClassWithProperties>, ale nie widzę sensu w niestosowaniu zwykłego podejścia w tym przypadku.


Czy użycie może Action<>spowodować wycieki pamięci? Wadą EventHandlerwzorca projektowego są wycieki pamięci. Należy również zauważyć, że może być wiele osób zarządzających wydarzeniami, ale tylko jedna akcja
Luke T O'Brien,

4
@ LukeTO'Brien: Wydarzenia są w istocie delegatami, więc te same możliwości wycieku pamięci istnieją w przypadku Action<T>. Również, Action<T> może odnosić się do kilku metod. Oto sedno, które pokazuje, że: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
Fredrik

89

Opierając się na niektórych z poprzednich odpowiedzi, podzielę moją odpowiedź na trzy obszary.

Po pierwsze, fizyczne ograniczenia używania Action<T1, T2, T2... >i używania klasy pochodnej EventArgs. Są trzy: Po pierwsze, jeśli zmienisz liczbę lub typy parametrów, każda subskrybowana metoda będzie musiała zostać zmieniona, aby była zgodna z nowym wzorcem. Jeśli jest to zdarzenie dostępne publicznie, z którego będą korzystać zespoły innych firm, i istnieje jakakolwiek możliwość, że argumenty zdarzenia ulegną zmianie, może to być powód do użycia klasy niestandardowej pochodzącej z argumentów zdarzenia ze względu na spójność (pamiętaj, że nadal MOŻESZ użyj a Action<MyCustomClass>) Po drugie, użycie Action<T1, T2, T2... >zapobiegnie przekazaniu informacji zwrotnej BACK do metody wywołującej, chyba że masz jakiś obiekt (na przykład z właściwością Handled), który jest przekazywany wraz z Action. Po trzecie, nie otrzymujesz nazwanych parametrów, więc jeśli przekazujesz 3 boolan int, twostringi a DateTime, nie masz pojęcia, jakie jest znaczenie tych wartości. Na marginesie, nadal możesz mieć metodę „Bezpieczne uruchamianie tego zdarzenia, gdy nadal używasz Action<T1, T2, T2... >”.

Po drugie, konsekwencje dla spójności. Jeśli masz duży system, z którym już pracujesz, prawie zawsze lepiej jest postępować zgodnie z projektem reszty systemu, chyba że masz bardzo dobry powód. Jeśli masz wydarzenia publiczne, które muszą być utrzymane, możliwość zastępowania klas pochodnych może być ważna. Miej to w pamięci.

Po trzecie, praktyka z prawdziwego życia, osobiście uważam, że mam tendencję do tworzenia wielu jednorazowych zdarzeń, takich jak zmiany własności, z którymi muszę wchodzić w interakcję (szczególnie podczas wykonywania MVVM z modelami widoku, które wchodzą ze sobą w interakcję) lub gdy zdarzenie ma pojedynczy parametr. W większości przypadków te wydarzenia przybierają postać public event Action<[classtype], bool> [PropertyName]Changed;lub public event Action SomethingHappened;. W takich przypadkach są dwie korzyści. Najpierw otrzymuję typ klasy wystawiającej. Jeśli MyClassdeklaruje i jest jedyną klasą wyzwalającą zdarzenie, otrzymuję jawne wystąpienie MyClassdo pracy w programie obsługi zdarzeń. Po drugie, w przypadku prostych zdarzeń, takich jak zdarzenia zmiany właściwości, znaczenie parametrów jest oczywiste i podane w nazwie procedury obsługi zdarzeń i nie muszę tworzyć niezliczonych klas dla tego rodzaju zdarzeń.


Świetny post na blogu. Na pewno warto przeczytać, jeśli czytasz ten wątek!
Vexir

1
Szczegółowa i dobrze przemyślana odpowiedź, która wyjaśnia uzasadnienie tego wniosku
MikeT

18

W większości powiedziałbym, że postępuj zgodnie ze wzorem. I nie odbiegała od niego, ale bardzo rzadko, a w szczególnych przypadkach. W tym przypadku największym problemem, jaki miałbym, jest to, że prawdopodobnie nadal używałbym an Action<SomeObjectType>, pozwalając mi później dodać dodatkowe właściwości i używać okazjonalnej właściwości dwukierunkowej (myślenie Handledlub inne zdarzenia zwrotne, w których subskrybent musi ustawić właściwość obiektu zdarzenia). A kiedy już zaczniesz tę linię, równie dobrze możesz użyć EventHandler<T>dla niektórychT .


14

Zaleta bardziej wyrazistego podejścia pojawia się, gdy kod znajduje się wewnątrz projektu o długości 300 000 wierszy.

Używając akcji, tak jak masz, nie ma sposobu, aby powiedzieć mi, czym są bool, int i Blah. Jeśli twoja akcja przekazała obiekt, który zdefiniował parametry, to OK.

Korzystanie z EventHandler, który chciał EventArgs i gdybyś uzupełnił swój przykład DiagnosticsArgs z metodami pobierającymi dla właściwości, które komentowały ich przeznaczenie, wtedy aplikacja byłaby bardziej zrozumiała. Prosimy również o komentarz lub pełne nazwanie argumentów w konstruktorze DiagnosticsArgs.


6

Jeśli postępujesz zgodnie ze standardowym wzorcem zdarzenia, możesz dodać metodę rozszerzenia, aby sprawdzanie wyzwalania zdarzeń było bezpieczniejsze / łatwiejsze. (tj. poniższy kod dodaje metodę rozszerzającą o nazwie SafeFire (), która sprawdza wartość null, a także (oczywiście) kopiuje zdarzenie do oddzielnej zmiennej, aby zabezpieczyć się przed zwykłym stanem wyścigu o wartości null, który może wpływać na zdarzenia).

(Chociaż mam dwie zdanie na temat tego, czy powinieneś używać metod rozszerzających na obiektach 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
... czy nie możesz zrobić tego samego z Action <T>? SafeFire <T> (to Action <T> theEvent, T theEventArgs) powinno działać, aby ... i nie trzeba używać opcji „where”
Beachwalker,

6

Zdaję sobie sprawę, że to pytanie ma ponad 10 lat, ale wydaje mi się, że nie tylko nie udzielono odpowiedzi na najbardziej oczywistą odpowiedź, ale może nie do końca wynika z pytania dobre zrozumienie tego, co dzieje się pod kołdrą. Ponadto istnieją inne pytania dotyczące późnego wiązania i tego, co to oznacza w odniesieniu do delegatów i lambd (więcej o tym później).

Najpierw zajmij się słoniem / gorylem o wadze 800 funtów w pokoju, kiedy wybrać eventvs Action<T>/ Func<T>:

  • Użyj lambdy, aby wykonać jedną instrukcję lub metodę. Użyj, eventgdy chcesz mieć więcej modelu pub / sub z wieloma instrukcjami / lambdami / funkcjami, które będą wykonywane (jest to główna różnica od razu).
  • Użyj lambdy, gdy chcesz skompilować instrukcje / funkcje do drzew wyrażeń. Użyj delegatów / zdarzeń, jeśli chcesz uczestniczyć w bardziej tradycyjnym późnym wiązaniu, takim jak używane w odbiciu i międzyoperacyjności modelu COM.

Jako przykład zdarzenia pozwala połączyć prosty i „standardowy” zestaw zdarzeń za pomocą małej aplikacji konsoli w następujący sposób:

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}");
    }
}

Wynik będzie wyglądał następująco:

wprowadź opis obrazu tutaj

Jeśli zrobiłeś to samo z Action<int> lub Action<object, SomeStandardArgs>, zobaczyłbyś tylko SomeSimpleEvent2i SomeStandardEvent2.

Więc co się dzieje w środku event?

Jeśli się rozszerzymy FireNiceEvent , kompilator faktycznie generuje następujące elementy (pominąłem niektóre szczegóły dotyczące synchronizacji wątków, które nie są istotne w tej dyskusji):

   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)

        }
    }

Kompilator generuje prywatną zmienną delegata, która nie jest widoczna dla przestrzeni nazw klasy, w której jest generowana. Ten delegat jest używany do zarządzania subskrypcjami i udziału w późnym wiązaniu, a interfejs publiczny jest dobrze znany+= i-= wszyscy operatorzy, których wszyscy znamy i kochamy :)

Możesz dostosować kod dla programów obsługi dodawania / usuwania, zmieniając zakres FireNiceEvent delegata na protected. Umożliwia to programistom dodawanie niestandardowych punktów zaczepienia do punktów zaczepienia, takich jak punkty zaczepienia logowania lub zabezpieczenia. To naprawdę tworzy bardzo potężne funkcje, które teraz pozwalają na dostosowaną dostępność do subskrypcji w oparciu o role użytkowników itp. Czy możesz to zrobić z lambdami? (Właściwie możesz przez niestandardowe kompilowanie drzew wyrażeń, ale to wykracza poza zakres tej odpowiedzi).

Aby odnieść się do kilku punktów z niektórych odpowiedzi tutaj:

  • Naprawdę nie ma różnicy w „kruchości” między zmianą listy argumentów w programie Action<T>a zmianą właściwości w klasie pochodnej EventArgs. Oba będą wymagały nie tylko zmiany kompilacji, ale także zmienią publiczny interfejs i będą wymagały wersjonowania. Bez różnicy.

  • W odniesieniu do tego, który jest standardem branżowym, zależy to od tego, gdzie jest on używany i dlaczego. Action<T>i taki jest często używany w IoC i DI, i eventjest często używany w routingu komunikatów, takim jak struktury typu GUI i MQ. Zauważ, że mówiłem często , nie zawsze .

  • Delegaci mają inne okresy istnienia niż lambdy. Trzeba też być świadomym złapania ... nie tylko z zamknięciem, ale także z pojęciem „patrz, co kot wciągnął”. Ma to wpływ na zużycie pamięci / czas życia, a także zarządzanie, czyli wycieki.

Jeszcze jedno, coś, o czym wspomniałem wcześniej ... pojęcie późnego wiązania. Często zobaczysz to podczas korzystania z frameworka takiego jak LINQ, dotyczącego tego, kiedy lambda staje się „aktywna”. To bardzo różni się od późnego wiązania delegata, które może się zdarzyć więcej niż jeden raz (tzn. Lambda jest zawsze, ale wiązanie występuje na żądanie tak często, jak jest to potrzebne), w przeciwieństwie do lambda, które po wystąpieniu jest wykonywane - magia zniknęła, a metoda (-y) / właściwość (-y) zawsze będą się wiązać. Coś, o czym warto pamiętać.


4

Patrząc na standardowe wzorce zdarzeń .NET, które znajdujemy

Standardowy podpis delegata zdarzenia .NET to:

void OnEventRaised(object sender, EventArgs args);

[…]

Lista argumentów zawiera dwa argumenty: nadawcę i argumenty zdarzenia. Typ nadawcy w czasie kompilacji to System.Object, mimo że prawdopodobnie znasz bardziej pochodny typ, który zawsze byłby poprawny. Zgodnie z konwencją użyj object .

Poniżej na tej samej stronie znajduje się przykład typowej definicji zdarzenia, który jest podobny

public event EventHandler<EventArgs> EventName;

Czy zdefiniowaliśmy

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

przewodnik mógł być

void OnEventRaised(MyClass sender, EventArgs args);

gdzie senderma poprawny ( bardziej pochodny ) typ.


Przepraszam, że nie zauważyłem, że różnica tkwi w sygnaturze programu obsługi, co byłoby korzystne w przypadku dokładniejszego wpisania sender .
user1832484
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.