¿Hay alguna desventaja de agregar un delegado vacío anónimo en la declaración del evento?


82

He visto algunas menciones de este idioma (incluso en SO ):

// Deliberately empty subscriber
public event EventHandler AskQuestion = delegate {};

La ventaja es clara: evita la necesidad de verificar el valor nulo antes de generar el evento.

Sin embargo, estoy ansioso por comprender si hay alguna desventaja. Por ejemplo, ¿es algo de uso generalizado y lo suficientemente transparente como para no causar un dolor de cabeza por mantenimiento? ¿Hay algún impacto apreciable en el rendimiento de la llamada vacía del suscriptor al evento?

Respuestas:


36

El único inconveniente es una penalización de rendimiento muy leve, ya que está llamando a un delegado vacío adicional. Aparte de eso, no hay penalización de mantenimiento ni ningún otro inconveniente.


19
Si el evento tuviera exactamente un suscriptor (un caso muy común), el manejador ficticio hará que tenga dos. Los eventos con un controlador se manejan de manera mucho más eficiente que aquellos con dos.
supercat

46

En lugar de inducir una sobrecarga de rendimiento, ¿por qué no utilizar un método de extensión para aliviar ambos problemas?

public static void Raise(this EventHandler handler, object sender, EventArgs e)
{
    if(handler != null)
    {
        handler(sender, e);
    }
}

Una vez definido, nunca más tendrá que hacer otra verificación de evento nulo:

// Works, even for null events.
MyButtonClick.Raise(this, EventArgs.Empty);

2
Vea aquí para una versión genérica: stackoverflow.com/questions/192980/…
Benjol

1
Exactamente lo que hace mi biblioteca helper trinity, por cierto: thehelpertrinity.codeplex.com
Kent Boogaart

3
¿No mueve esto simplemente el problema del hilo de la verificación nula a su método de extensión?
PatrickV

6
No. Handler se pasa al método, momento en el que esa instancia no se puede modificar.
Judah Gabriel Himango

@PatrickV No, Judah tiene razón, el parámetro handleranterior es un parámetro de valor para el método. No cambiará mientras se esté ejecutando el método. Para que eso ocurra, debería ser un refparámetro (obviamente no está permitido que un parámetro tenga tanto refel thismodificador como el), o un campo, por supuesto.
Jeppe Stig Nielsen

42

Para los sistemas que hacen un uso intensivo de eventos y son críticos para el rendimiento , definitivamente querrá al menos considerar no hacer esto. El costo de generar un evento con un delegado vacío es aproximadamente el doble que el de generarlo con un cheque nulo primero.

Aquí hay algunas cifras que ejecutan puntos de referencia en mi máquina:

For 50000000 iterations . . .
No null check (empty delegate attached): 530ms
With null check (no delegates attached): 249ms
With null check (with delegate attached): 452ms

Y aquí está el código que usé para obtener estas cifras:

using System;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        public event EventHandler<EventArgs> EventWithDelegate = delegate { };
        public event EventHandler<EventArgs> EventWithoutDelegate;

        static void Main(string[] args)
        {
            //warm up
            new Program().DoTimings(false);
            //do it for real
            new Program().DoTimings(true);

            Console.WriteLine("Done");
            Console.ReadKey();
        }

        private void DoTimings(bool output)
        {
            const int iterations = 50000000;

            if (output)
            {
                Console.WriteLine("For {0} iterations . . .", iterations);
            }

            //with anonymous delegate attached to avoid null checks
            var stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("No null check (empty delegate attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }


            //without any delegates attached (null check required)
            stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithoutAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("With null check (no delegates attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }


            //attach delegate
            EventWithoutDelegate += delegate { };


            //with delegate attached (null check still performed)
            stopWatch = Stopwatch.StartNew();

            for (var i = 0; i < iterations; ++i)
            {
                RaiseWithoutAnonDelegate();
            }

            stopWatch.Stop();

            if (output)
            {
                Console.WriteLine("With null check (with delegate attached): {0}ms", stopWatch.ElapsedMilliseconds);
            }
        }

        private void RaiseWithAnonDelegate()
        {
            EventWithDelegate(this, EventArgs.Empty);
        }

        private void RaiseWithoutAnonDelegate()
        {
            var handler = EventWithoutDelegate;

            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }
    }
}

11
Estás bromeando, ¿verdad? La invocación agrega 5 nanosegundos y ¿advierte que no lo haga? No puedo pensar en una optimización general más irrazonable que esa.
Brad Wilson

8
Interesante. De acuerdo con sus hallazgos, es más rápido verificar si hay nulos y llamar a un delegado que simplemente llamarlo sin la verificación. No me suena bien. Pero de todos modos, esta es una diferencia tan pequeña que no creo que se note en todos los casos excepto en los más extremos.
Maurice

22
Brad, dije específicamente para los sistemas de rendimiento crítico que hacen un uso intensivo de eventos. ¿Cómo es eso general?
Kent Boogaart

3
Estás hablando de penalizaciones por desempeño cuando llamas a delegados vacíos. Te pregunto: ¿qué pasa cuando alguien realmente se suscribe al evento? Debe preocuparse por el desempeño de los suscriptores, no de los delegados vacíos.
Liviu Trifoi

2
El gran coste de rendimiento no se produce en el caso de que no haya suscriptores "reales", sino en el caso de que haya uno. En ese caso, la suscripción, la cancelación de la suscripción y la invocación deberían ser muy eficientes porque no tendrán que hacer nada con las listas de invocación, pero el hecho de que el evento (desde la perspectiva del sistema) tenga dos suscriptores en lugar de uno forzará el uso del código que maneja "n" suscriptores, en lugar del código que está optimizado para el caso "cero" o "uno".
supercat

7

Si lo está haciendo mucho, es posible que desee tener un delegado vacío único, estático / compartido que reutilice, simplemente para reducir el volumen de instancias de delegado. Tenga en cuenta que el compilador almacena en caché este delegado por evento de todos modos (en un campo estático), por lo que es solo una instancia de delegado por definición de evento, por lo que no es un gran ahorro, pero tal vez valga la pena.

El campo por instancia en cada clase seguirá ocupando el mismo espacio, por supuesto.

es decir

internal static class Foo
{
    internal static readonly EventHandler EmptyEvent = delegate { };
}
public class Bar
{
    public event EventHandler SomeEvent = Foo.EmptyEvent;
}

Aparte de eso, parece estar bien.


1
Parece de la respuesta aquí: stackoverflow.com/questions/703014/… que el compilador ya realiza la optimización en una sola instancia.
Govert

3

No hay una penalización de desempeño significativa de la que hablar, excepto, posiblemente, en algunas situaciones extremas.

Sin embargo, tenga en cuenta que este truco se vuelve menos relevante en C # 6.0, porque el lenguaje proporciona una sintaxis alternativa para llamar a los delegados que puede ser nula:

delegateThatCouldBeNull?.Invoke(this, value);

Arriba, el operador condicional nulo ?.combina la verificación nula con una invocación condicional.



2

Yo diría que es una construcción un poco peligrosa, porque te tienta a hacer algo como:

MyEvent(this, EventArgs.Empty);

Si el cliente lanza una excepción, el servidor la acompaña.

Entonces, tal vez lo hagas:

try
{
  MyEvent(this, EventArgs.Empty);
}
catch
{
}

Pero, si tiene varios suscriptores y un suscriptor lanza una excepción, ¿qué sucede con los otros suscriptores?

Con ese fin, he estado usando algunos métodos auxiliares estáticos que hacen la verificación nula y se tragan cualquier excepción del lado del suscriptor (esto es de idesign).

// Usage
EventHelper.Fire(MyEvent, this, EventArgs.Empty);


public static void Fire(EventHandler del, object sender, EventArgs e)
{
    UnsafeFire(del, sender, e);
}
private static void UnsafeFire(Delegate del, params object[] args)
{
    if (del == null)
    {
        return;
    }
    Delegate[] delegates = del.GetInvocationList();

    foreach (Delegate sink in delegates)
    {
        try
        {
            sink.DynamicInvoke(args);
        }
        catch
        { }
    }
}

No es quisquilloso, pero no creas <code> if (del == null) {return; } Delegate [] delegates = del.GetInvocationList (); </code> ¿es una condición de carrera candidata?
Cherian

No exactamente. Dado que los delegados son tipos de valor, del es en realidad una copia privada de la cadena de delegados a la que solo puede acceder el cuerpo del método UnsafeFire. (Advertencia: esto falla si UnsafeFire se inserta en línea, por lo que necesitaría usar el atributo [MethodImpl (MethodImplOptions.NoInlining)] para actuar contra él.)
Daniel Fortunov

1) Los delegados son tipos de referencia 2) Son inmutables, por lo que no es una condición de carrera 3) No creo que la inserción cambie el comportamiento de este código. Esperaría que el parámetro del se convierta en una nueva variable local cuando esté en línea.
CodesInChaos

Su solución a '¿qué sucede si se lanza una excepción' es ignorar todas las excepciones? Es por eso que siempre o casi siempre envuelvo todas las suscripciones a eventos en un intento (usando una Try.TryAction()función útil , no un try{}bloque explícito ), pero no ignoro las excepciones, las
informo

0

En lugar del enfoque de "delegado vacío", se puede definir un método de extensión simple para encapsular el método convencional de verificar el controlador de eventos contra nulo. Se describe aquí y aquí .


-2

Hasta ahora, una cosa se ha perdido como respuesta a esta pregunta: es peligroso evitar la verificación del valor nulo .

public class X
{
    public delegate void MyDelegate();
    public MyDelegate MyFunnyCallback = delegate() { }

    public void DoSomething()
    {
        MyFunnyCallback();
    }
}


X x = new X();

x.MyFunnyCallback = delegate() { Console.WriteLine("Howdie"); }

x.DoSomething(); // works fine

// .. re-init x
x.MyFunnyCallback = null;

// .. continue
x.DoSomething(); // crashes with an exception

La cuestión es: nunca se sabe quién usará su código de qué manera. Nunca se sabe, si en algunos años durante una corrección de errores de su código, el evento / controlador se establece en nulo.

Siempre, escriba el cheque si.

Espero que ayude ;)

ps: Gracias por el cálculo de rendimiento.

pps: lo editó desde un caso de evento hasta un ejemplo de devolución de llamada. Gracias por los comentarios ... "Codifiqué" el ejemplo sin Visual Studio y ajusté el ejemplo que tenía en mente a un evento. Perdón por la confusion.

ppps: No sé si aún encaja en el hilo ... pero creo que es un principio importante. Compruebe también otro hilo de stackflow


1
x.MyFunnyEvent = null; <- Eso ni siquiera se compila (fuera de la clase). El punto de un evento solo apoya + = y - =. Y ni siquiera puedes hacer x.MyFunnyEvent- = x.MyFunnyEvent fuera de la clase, ya que el captador de eventos es cuasi protected. Solo puede separar el evento de la propia clase (o de una clase derivada).
CodesInChaos

tiene razón ... cierto para los eventos ... tenía un caso con un manejador simple. Lo siento. Intentaré editar.
Thomas

Por supuesto, si su delegado es público, eso sería peligroso porque nunca se sabe lo que hará el usuario. Sin embargo, si configura los delegados vacíos en una variable privada y maneja + = y - = usted mismo, eso no será un problema y la verificación nula será segura para subprocesos.
ForceMagic
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.