evento Acción <> vs evento EventHandler <>


144

¿Hay alguna diferencia entre declarar event Action<>yevent EventHandler<> .

Asumiendo que no importa qué objeto realmente provocó un evento.

por ejemplo:

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

vs

public event EventHandler<DiagnosticsArgs> DiagnosticsEvent;

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

el uso sería casi el mismo en ambos casos:

obj.DiagnosticsEvent += HandleDiagnosticsEvent;

Hay varias cosas que no me gustan del event EventHandler<>patrón:

  • Declaración de tipo adicional derivada de EventArgs
  • Paso obligatorio de la fuente del objeto: a menudo a nadie le importa

Más código significa más código para mantener sin ninguna ventaja clara.

Como resultado, prefiero event Action<>

Sin embargo, solo si hay demasiados argumentos de tipo en Acción <>, se requeriría una clase adicional.


2
plusOne (acabo de vencer al sistema) por "a nadie le importa"
hyankov

@plusOne: ¡Realmente necesito conocer al remitente! Digamos que algo sucede y quieres saber quién lo hizo. Ahí es donde necesita 'fuente de objeto' (también conocido como remitente).
Kamran Bigdely

el remitente puede ser una propiedad en la carga útil del evento
Thanasis Ioannidis

Respuestas:


67

La principal diferencia será que si usas Action<> su evento no seguirá el patrón de diseño de prácticamente cualquier otro evento en el sistema, lo que consideraría un inconveniente.

Una ventaja del patrón de diseño dominante (aparte del poder de la similitud) es que puede extender el EventArgsobjeto con nuevas propiedades sin alterar la firma del evento. Esto aún sería posible si lo usaras Action<SomeClassWithProperties>, pero realmente no veo el punto de no usar el enfoque regular en ese caso.


¿Podría el uso Action<>provocar pérdidas de memoria? Una desventaja con el EventHandlerpatrón de diseño son las pérdidas de memoria. También debe señalarse que puede haber múltiples controladores de eventos pero solo una acción
Luke T O'Brien

44
@ LukeTO'Brien: Los eventos son en esencia delegados, por lo que existen las mismas posibilidades de pérdida de memoria Action<T>. Además, se Action<T> puede hacer referencia a varios métodos. Aquí hay un resumen que demuestra que: gist.github.com/fmork/4a4ddf687fa8398d19ddb2df96f0b434
Fredrik Mörk el

88

Basado en algunas de las respuestas anteriores, voy a dividir mi respuesta en tres áreas.

Primero, las limitaciones físicas de usar Action<T1, T2, T2... >vs usar una clase derivada de EventArgs. Hay tres: Primero, si cambia el número o los tipos de parámetros, cada método al que se suscriba tendrá que cambiarse para ajustarse al nuevo patrón. Si este es un evento público que utilizarán los ensambles de terceros, y existe la posibilidad de que los argumentos del evento cambien, esta sería una razón para usar una clase personalizada derivada de los argumentos del evento por razones de coherencia (recuerde, PODRÍA todavía use a Action<MyCustomClass>) En segundo lugar, el uso Action<T1, T2, T2... >le impedirá pasar comentarios al método de llamada a menos que tenga algún tipo de objeto (con una propiedad Handled, por ejemplo) que se pase junto con la Acción. Tercero, no obtienes parámetros con nombre, así que si estás pasando 3 bool's int, dosstring's, y a DateTime, no tienes idea de cuál es el significado de esos valores. Como nota al margen, aún puede tener el método "Disparar este evento de forma segura mientras se sigue usando Action<T1, T2, T2... >".

En segundo lugar, implicaciones de consistencia. Si tiene un sistema grande con el que ya está trabajando, casi siempre es mejor seguir la forma en que está diseñado el resto del sistema a menos que tenga una muy buena razón para no hacerlo. Si tiene eventos públicos que deben mantenerse, la capacidad de sustituir clases derivadas puede ser importante. Ten eso en mente.

En tercer lugar, en la práctica de la vida real, personalmente encuentro que tiendo a crear muchos eventos únicos para cosas como cambios de propiedad con los que necesito interactuar (en particular cuando hago MVVM con modelos de vista que interactúan entre sí) o donde el evento tiene Un solo parámetro. La mayoría de las veces estos eventos toman la forma de public event Action<[classtype], bool> [PropertyName]Changed;o public event Action SomethingHappened;. En estos casos, hay dos beneficios. Primero, obtengo un tipo para la clase emisora. Si MyClassdeclara y es la única clase que activa el evento, obtengo una instancia explícita de MyClasstrabajar en el controlador de eventos. En segundo lugar, para eventos simples como los eventos de cambio de propiedad, el significado de los parámetros es obvio y se indica en el nombre del controlador de eventos y no tengo que crear una gran cantidad de clases para este tipo de eventos.


Impresionante publicación de blog. ¡Definitivamente vale la pena leerlo si estás leyendo este hilo!
Vexir

1
Respuesta detallada y bien pensada que explica el razonamiento detrás de la conclusión
MikeT

18

En su mayor parte, diría que sigue el patrón. Me he desviado de él, pero muy raramente, y por razones específicas. En este caso, el problema más grande que tengo es que probablemente todavía usaría un Action<SomeObjectType>, lo que me permite agregar propiedades adicionales más adelante y usar la propiedad ocasional de 2 vías (pensar Handledu otros eventos de retroalimentación donde el el suscriptor debe establecer una propiedad en el objeto de evento). Y una vez que haya comenzado esa línea, también podría usar EventHandler<T>para algunos T.


14

La ventaja de un enfoque más detallado se produce cuando su código está dentro de un proyecto de 300,000 líneas.

Usando la acción, como lo has hecho, no hay forma de decirme qué son bool, int y Blah. Si su acción pasó un objeto que definió los parámetros, entonces está bien.

Usando un EventHandler que quería un EventArgs y si completara su ejemplo de DiagnosticsArgs con getters para las propiedades que comentaron su propósito, entonces su aplicación sería más comprensible. Además, comente o nombre por completo los argumentos en el constructor DiagnosticsArgs.


6

Si sigue el patrón de eventos estándar, puede agregar un método de extensión para hacer que la comprobación de la activación de eventos sea más segura / fácil. (es decir, el siguiente código agrega un método de extensión llamado SafeFire () que realiza la verificación nula, así como (obviamente) copia el evento en una variable separada para estar a salvo de la condición de carrera nula habitual que puede afectar los eventos).

(Aunque estoy en una especie de dos mentes si debería usar métodos de extensión en 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();
    }
}

44
... no puedes hacer lo mismo con la Acción <T>? SafeFire <T> (esta Acción <T> theEvent, T theEventArgs) debería funcionar para ... y no es necesario usar "where"
Beachwalker

6

Me doy cuenta de que esta pregunta tiene más de 10 años, pero me parece que no solo no se ha abordado la respuesta más obvia, sino que tal vez no esté realmente claro de la pregunta una buena comprensión de lo que sucede debajo de las cubiertas. Además, hay otras preguntas sobre la vinculación tardía y lo que eso significa con respecto a los delegados y lambdas (más sobre eso más adelante).

Primero en abordar el elefante / gorila de 800 lb en la habitación, cuándo elegir eventvs Action<T>/ Func<T>:

  • Use una lambda para ejecutar una declaración o método. Úselo eventcuando desee más de un modelo pub / sub con múltiples declaraciones / lambdas / funciones que se ejecutarán (esta es una gran diferencia desde el principio).
  • Use una lambda cuando desee compilar declaraciones / funciones para árboles de expresión. Utilice delegados / eventos cuando desee participar en un enlace tardío más tradicional, como el utilizado en la reflexión y la interoperabilidad COM.

Como ejemplo de un evento, conectemos un conjunto de eventos simple y 'estándar' usando una pequeña aplicación de consola de la siguiente manera:

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

La salida se verá de la siguiente manera:

ingrese la descripción de la imagen aquí

Si hicieras lo mismo con Action<int>o Action<object, SomeStandardArgs>, solo verías SomeSimpleEvent2y SomeStandardEvent2.

Entonces, ¿qué está pasando dentro event?

Si nos expandimos FireNiceEvent, el compilador en realidad está generando lo siguiente (he omitido algunos detalles con respecto a la sincronización de subprocesos que no son relevantes para esta discusión):

   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)

        }
    }

El compilador genera una variable delegada privada que no es visible para el espacio de nombres de clase en el que se genera. Ese delegado es lo que se usa para la gestión de suscripciones y la participación vinculante tardía, y la interfaz pública es familiar +=y-= operadores que todos conocemos y amamos:)

Puede personalizar el código para los controladores de agregar / quitar cambiando el alcance de FireNiceEvent delegado a protegido. Esto ahora permite a los desarrolladores agregar ganchos personalizados a los ganchos, como el registro o los ganchos de seguridad. Esto realmente ofrece algunas funciones muy potentes que ahora permiten una accesibilidad personalizada a la suscripción en función de los roles de los usuarios, etc. ¿Se puede hacer eso con lambdas? (En realidad, puede compilando árboles de expresión personalizados, pero eso está más allá del alcance de esta respuesta).

Para abordar algunos puntos de algunas de las respuestas aquí:

  • Realmente no hay diferencia en la 'fragilidad' entre cambiar la lista de argumentos Action<T>y cambiar las propiedades en una clase derivada de EventArgs. Cualquiera de los dos no solo requerirá un cambio de compilación, ambos cambiarán una interfaz pública y requerirán versiones. Ninguna diferencia.

  • Con respecto a cuál es un estándar de la industria, eso depende de dónde se esté utilizando y por qué. Action<T>y esto a menudo se usa en IoC y DI, y a eventmenudo se usa en el enrutamiento de mensajes, como los marcos de tipo GUI y MQ. Tenga en cuenta que dije a menudo , no siempre .

  • Los delegados tienen vidas diferentes a las lambdas. También hay que tener en cuenta la captura ... no solo con el cierre, sino también con la noción de "mira lo que el gato arrastró". Esto afecta la huella / vida útil de la memoria, así como la gestión, también conocida como fugas.

Una cosa más, algo que mencioné anteriormente ... la noción de enlace tardío. A menudo verá esto cuando use un marco como LINQ, en cuanto a cuándo una lambda se vuelve 'en vivo'. Eso es muy diferente a la vinculación tardía de un delegado, lo que puede suceder más de una vez (es decir, el lambda siempre está allí, pero la vinculación ocurre a pedido tantas veces como sea necesario), a diferencia de una lambda, que una vez que ocurre, se hace - la magia se ha ido, y los métodos / propiedad (s) siempre se unirán. Algo para tener en cuenta.


4

Mirando los patrones de eventos .NET estándar encontramos

La firma estándar para un delegado de eventos .NET es:

void OnEventRaised(object sender, EventArgs args);

[...]

La lista de argumentos contiene dos argumentos: el remitente y los argumentos del evento. El tipo de remitente en tiempo de compilación es System.Object, aunque probablemente conozca un tipo más derivado que siempre sería correcto. Por convención, use objeto .

A continuación, en la misma página, encontramos un ejemplo de la definición de evento típica que es algo así como

public event EventHandler<EventArgs> EventName;

Si hubiéramos definido

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

el controlador podría haber sido

void OnEventRaised(MyClass sender, EventArgs args);

donde sendertiene el tipo correcto ( más derivado ).


Lamento no haber comentado que la diferencia está en la firma del controlador, que se beneficiaría de una escritura más precisa sender .
user1832484
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.