evento Ação <> vs evento EventHandler <>


144

Existe alguma diferença entre declarar event Action<>e event EventHandler<>.

Supondo que não importa qual objeto realmente gerou um evento.

por exemplo:

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

vs

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

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

o uso seria quase o mesmo nos dois casos:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

Há várias coisas que eu não gosto no event EventHandler<>padrão:

  • Declaração de tipo extra derivada de EventArgs
  • Passagem obrigatória da fonte do objeto - geralmente ninguém se importa

Mais código significa mais código a ser mantido sem nenhuma vantagem clara.

Como resultado, eu prefiro event Action<>

No entanto, somente se houver muitos argumentos de tipo em Ação <>, uma classe extra será necessária.


2
plusOne (acabei de derrotar o sistema) de "ninguém liga"
hyankov 28/01

@plusOne: Eu realmente preciso conhecer o remetente! Diga que algo acontece e você quer saber quem fez isso. É aí que você precisa de 'fonte de objeto' (também conhecido como remetente).
Kamran Bigdely

remetente pode ser uma propriedade na carga útil do evento
Thanasis Ioannidis 23/01

Respostas:


67

A principal diferença será que, se você usar Action<>seu evento, não seguirá o padrão de design de praticamente nenhum outro evento no sistema, o que eu consideraria uma desvantagem.

Uma vantagem do padrão de design dominante (além do poder da mesmice) é que você pode estender o EventArgsobjeto com novas propriedades sem alterar a assinatura do evento. Isso ainda seria possível se você usasse Action<SomeClassWithProperties>, mas eu realmente não entendo o motivo de não usar a abordagem regular nesse caso.


Pode Action<>resultar em vazamentos de memória? Uma desvantagem do EventHandlerpadrão de design são os vazamentos de memória. Também deve ser salientado que pode haver múltiplos manipuladores de eventos, mas apenas uma ação
Luke T O'Brien

4
@ LukeTO'Brien: Os eventos são essencialmente delegados, então as mesmas possibilidades de vazamento de memória existem Action<T>. Além disso, um Action<T> pode se referir a vários métodos. Aqui é uma essência que demonstra que: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
Fredrik Mörk

88

Com base em algumas das respostas anteriores, vou dividir minha resposta em três áreas.

Primeiro, limitações físicas de usar Action<T1, T2, T2... >vs usando uma classe derivada de EventArgs. Existem três: Primeiro, se você alterar o número ou os tipos de parâmetros, todos os métodos inscritos terão que ser alterados para se adequarem ao novo padrão. Se este for um evento público que os assemblies de terceiros estarão usando e houver possibilidade de que os argumentos do evento sejam alterados, esse seria um motivo para usar uma classe personalizada derivada dos argumentos do evento por razões de consistência (lembre-se, você ainda pode use an Action<MyCustomClass>) Segundo, o uso Action<T1, T2, T2... >impedirá que você repasse feedback para o método de chamada, a menos que você tenha algum tipo de objeto (com uma propriedade Handled, por exemplo) que seja passado junto com a Action. Terceiro, você não recebe parâmetros nomeados; portanto, se você passa 3 bool's inte 2string, e a DateTime, você não tem idéia de qual é o significado desses valores. Como uma observação lateral, você ainda pode ter o método "Disparar este evento com segurança enquanto ainda estiver usando Action<T1, T2, T2... >".

Em segundo lugar, implicações de consistência. Se você possui um sistema grande com o qual já está trabalhando, é quase sempre melhor seguir a maneira como o resto do sistema é projetado, a menos que você não tenha um bom motivo. Se você enfrentar publicamente eventos que precisam ser mantidos, a capacidade de substituir classes derivadas pode ser importante. Tenha isso em mente.

Em terceiro lugar, na prática da vida real, eu pessoalmente acho que tenho a tendência de criar muitos eventos pontuais para coisas como alterações de propriedade com as quais preciso interagir (particularmente ao fazer MVVM com modelos de exibição que interagem entre si) ou onde o evento ocorreu. um único parâmetro. Na maioria das vezes esses eventos assumem a forma de public event Action<[classtype], bool> [PropertyName]Changed;ou public event Action SomethingHappened;. Nestes casos, existem dois benefícios. Primeiro, recebo um tipo para a classe de emissão. Se MyClassdeclara e é a única classe que dispara o evento, recebo uma instância explícita MyClasspara trabalhar no manipulador de eventos. Em segundo lugar, para eventos simples, como eventos de alteração de propriedade, o significado dos parâmetros é óbvio e indicado no nome do manipulador de eventos, e não preciso criar uma infinidade de classes para esses tipos de eventos.


Impressionante post no blog. Definitivamente vale a pena ler se você estiver lendo este tópico!
Vexir

1
Detalhado e bem pensado resposta que explica o raciocínio por trás da conclusão
miket

18

Na maioria das vezes, eu diria seguir o padrão. Eu me desviei disso, mas muito raramente, e por razões específicas. No caso em questão, o maior problema que eu teria é que provavelmente ainda usaria um Action<SomeObjectType>, permitindo-me adicionar propriedades extras posteriormente e usar a propriedade bidirecional ocasional (pense Handledou outros eventos de feedback nos quais o O assinante precisa definir uma propriedade no objeto de evento). E depois que você começar a seguir essa linha, poderá usar EventHandler<T>alguns T.


14

A vantagem de uma abordagem mais detalhada ocorre quando o código está dentro de um projeto de 300.000 linhas.

Usando a ação, como você tem, não há como me dizer o que bool, int e Blah são. Se sua ação passou por um objeto que definiu os parâmetros, ok.

Usando um EventHandler que desejava um EventArgs e se você concluísse o exemplo DiagnosticsArgs com getters para as propriedades que comentavam sua finalidade, seu aplicativo seria mais compreensível. Além disso, comente ou nomeie totalmente os argumentos no construtor DiagnosticsArgs.


6

Se você seguir o padrão de evento padrão, poderá adicionar um método de extensão para tornar a verificação do disparo de eventos mais segura / fácil. (ou seja, o código a seguir adiciona um método de extensão chamado SafeFire () que faz a verificação nula, além de (obviamente) copiar o evento em uma variável separada para estar seguro da condição de corrida nula usual que pode afetar os eventos.)

(Embora eu esteja decidido se você deve usar métodos de extensão em objetos nulos ...)

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
... você não pode fazer o mesmo com a ação <T>? SafeFire <T> (este Action <T> TheEvent, T theEventArgs) deve trabalhar para ... e não há necessidade de utilização "onde"
Beachwalker

6

Percebo que essa pergunta tem mais de 10 anos, mas parece-me que não apenas a resposta mais óbvia não foi abordada, mas que talvez não esteja realmente claro da pergunta uma boa compreensão do que acontece nos bastidores. Além disso, há outras perguntas sobre o vínculo tardio e o que isso significa em relação aos delegados e lambdas (mais sobre isso mais tarde).

Primeiro a abordar o elefante / gorila de 300 kg na sala, quando escolher eventvs Action<T>/ Func<T>:

  • Use um lambda para executar uma instrução ou método. Use eventquando desejar mais de um modelo pub / sub com várias instruções / lambdas / funções que serão executadas (esta é uma das principais diferença logo de cara).
  • Use um lambda quando desejar compilar instruções / funções para árvores de expressão. Use delegados / eventos quando quiser participar de ligações tardias mais tradicionais, como as usadas na reflexão e na interoperabilidade COM.

Como exemplo de um evento, vamos conectar um conjunto de eventos simples e "padrão" usando um pequeno aplicativo de console da seguinte maneira:

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

A saída terá a seguinte aparência:

insira a descrição da imagem aqui

Se você fizesse o mesmo com Action<int>ou Action<object, SomeStandardArgs>, você apenas veria SomeSimpleEvent2eSomeStandardEvent2 .

Então, o que está acontecendo dentro de event ?

Se expandirmos FireNiceEvent, o compilador está realmente gerando o seguinte (omiti alguns detalhes em relação à sincronização de threads que não são relevantes para esta discussão):

   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)

        }
    }

O compilador gera uma variável delegada privada que não é visível para o espaço para nome da classe em que é gerada. Esse delegado é o que é usado para gerenciamento de assinaturas e participação tardia de ligações, e a interface voltada ao público é a familiar +=e-= operadores que todos conhecemos e amamos:)

Você pode personalizar o código para os manipuladores de adicionar / remover, alterando o escopo do FireNiceEvent delegado para protegido. Agora, os desenvolvedores podem adicionar ganchos personalizados aos ganchos, como ganchos de log ou segurança. Isso realmente cria alguns recursos muito poderosos que agora permitem acessibilidade personalizada à assinatura com base nas funções do usuário etc. Você pode fazer isso com lambdas? (Na verdade, você pode compilar árvores de expressão customizadas, mas isso está além do escopo desta resposta).

Para abordar alguns pontos de algumas das respostas aqui:

  • Realmente não há diferença na 'fragilidade' entre alterar a lista de argumentos Action<T>e alterar as propriedades em uma classe derivada EventArgs. Eles não apenas exigirão uma alteração de compilação, como também mudarão uma interface pública e exigirão controle de versão. Não faz diferença.

  • Com relação a qual é um padrão do setor, isso depende de onde isso está sendo usado e por quê. Action<T>e tal é frequentemente usado em IoC e DI, e eventé freqüentemente usado em roteamento de mensagens, como estruturas de tipo GUI e MQ. Note que eu disse muitas vezes , nem sempre .

  • Os delegados têm vidas diferentes das lambdas. Também é preciso estar ciente da captura ... não apenas com o fechamento, mas também com a noção de 'veja o que o gato arrastou'. Isso afeta a pegada / vida útil da memória e também o gerenciamento de vazamentos.

Mais uma coisa, algo que eu referenciei anteriormente ... a noção de ligação tardia. Você verá isso frequentemente ao usar uma estrutura como LINQ, sobre quando um lambda se torna 'ativo'. Isso é muito diferente da ligação tardia de um delegado, que pode acontecer mais de uma vez (ou seja, o lambda está sempre lá, mas a ligação ocorre sob demanda sempre que necessário), em oposição a um lambda, que, uma vez que ocorre, está pronto - a mágica se foi, e o (s) método (s) / propriedade (s) sempre será vinculado. Algo a ter em mente.


4

Examinando os padrões de eventos .NET padrão , encontramos

A assinatura padrão para um representante de evento .NET é:

void OnEventRaised(object sender, EventArgs args);

[...]

A lista de argumentos contém dois argumentos: o remetente e os argumentos do evento. O tipo de remetente em tempo de compilação é System.Object, mesmo que você provavelmente conheça um tipo mais derivado que sempre esteja correto. Por convenção, use object .

Abaixo na mesma página, encontramos um exemplo da definição típica de evento que é algo como

public event EventHandler<EventArgs> EventName;

Se tivéssemos definido

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

o manipulador poderia ter sido

void OnEventRaised(MyClass sender, EventArgs args);

onde sendertem o tipo correto ( mais derivado ).


Desculpe por não ter observado que a diferença está na assinatura do manipulador, o que se beneficiaria de uma digitação mais precisa sender .
user1832484
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.