2008-10-04 17 views
78

he visto un par de menciones de este idioma (incluyendo on SO):¿Hay algún inconveniente en agregar un delegado vacío anónimo en la declaración de evento?

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

La ventaja es clara - que evita la necesidad de comprobar la nula antes de levantar el evento.

Sin embargo, estoy dispuesto a entender si hay inconvenientes. Por ejemplo, ¿es algo que tiene un uso generalizado y es lo suficientemente transparente como para no causar dolores de cabeza por mantenimiento? ¿Hay algún golpe de rendimiento apreciable de la llamada al suscriptor del evento vacío?

Respuesta

36

El único inconveniente es una penalización de rendimiento muy leve ya que está llamando delegado extra vacío. Aparte de eso, no hay penalización por mantenimiento u otro inconveniente.

+19

Si el evento tuviera exactamente un suscriptor (un caso * muy * común), el manejador simulado lo hará tener dos. Los eventos con un solo controlador se manejan * mucho * más eficientemente que aquellos con dos. – supercat

7

Si lo está haciendo a/lot /, es posible que desee tener un único delegado vacío, estático/compartido que reutilice, simplemente para reducir el volumen de instancias de delegado. Tenga en cuenta que el compilador guarda en caché este delegado por evento de todos modos (en un campo estático), por lo que solo es 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.

decir

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

Aparte de eso, parece bien.

+1

Parece de la respuesta aquí: http://stackoverflow.com/questions/703014/will-an-empty-delegate-eat-up-memory que el compilador ya realiza la optimización en una sola instancia. – Govert

41

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

Estas son algunas cifras que se ejecutan los 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 utiliza 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 eso. –

+8

Interesante. De acuerdo con sus hallazgos, es más rápido buscar nulo y llamar a un delegado que simplemente llamarlo sin el control. No me suena bien Pero de todos modos esta es una diferencia tan pequeña que no creo que sea notable en todos los casos, excepto en los más extremos. – Maurice

+22

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

2

Es mi entendimiento de que el delegado vacía es hilo seguro, mientras que el cheque nulo no lo es.

+2

Ninguno de los patrones hace que el hilo del evento sea seguro como se indica en la respuesta aquí: http://stackoverflow.com/questions/10282043/is-this-a-better-way-to-fire-invoke-events-without -a-null-check-in-c – Beachwalker

2

yo diría que es un poco de una construcción peligrosa, ya que le tienta a hacer algo como:

MyEvent(this, EventArgs.Empty); 

Si el cliente emite una excepción, el servidor va con ella.

Así pues, tal vez usted hacer:

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

Pero, si tiene varios abonados y un suscriptor inicia una excepción, lo que ocurre con los otros suscriptores?

Para ese fin, he estado utilizando algunos métodos de ayuda estáticos que hacen la comprobación nula y se traga cualquier excepción desde el 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 
     { } 
    } 
} 
+0

No es quisquilloso, pero ¿no cree usted que if (del == null) { return; } Delegate[] delegates = del.GetInvocationList(); es un candidato de condición de carrera? – Cherian

+0

No del todo. Como los delegados son tipos de valor, del es en realidad una copia privada de la cadena de delegados a la que solo se puede acceder con el cuerpo del método UnsafeFire. (Advertencia: Esto falla si UnsafeFire se inserta, por lo que necesitaría usar el atributo [MethodImpl (MethodImplOptions.NoInlining)] para contrarrestarlo.) –

+0

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

46

En lugar de inducir sobrecarga de rendimiento, por qué no use an extension method para aliviar ambos problemas:

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

Una vez definido, que nunca tenga que hacer otro cheque caso nulo de nuevo:

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

De hecho, esto pertenece en el lenguaje o el marco! –

+2

Consulte aquí para obtener una versión genérica: http://stackoverflow.com/questions/192980/boiler-plate-code-replacement-is-there-anything-bad-about-this-code – Benjol

+1

Exactamente lo que hace mi biblioteca auxiliar trinity incidentalmente: http://thehelpertrinity.codeplex.com/ –

0

lugar del enfoque de "delegado vacío" uno puede definir un método de extensión simple para encapsular el método convencional de comprobación del controlador de eventos frente a nulo. Se describe here y here.

-1

Una cosa se perdió como respuesta a esta pregunta hasta el momento: Es peligroso evitar la comprobació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 

Lo que pasa es que nunca se sabe quién utilizará 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 la verificación if.

Espero que ayude;)

PD: Gracias por el cálculo del rendimiento.

pps: se editó desde un caso de evento a 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 todavía se ajusta al hilo ... pero creo que es un principio importante. Compruebe también another thread of stackflow

+1

x.MyFunnyEvent = null; <- Eso ni siquiera compila (fuera de la clase). El punto de un evento solo es compatible con + = y - =. Y ni siquiera puedes hacer x.MyFunnyEvent- = x.MyFunnyEvent fuera de la clase ya que el getter del evento es casi 'protected'. Solo puede dividir el evento de la clase en sí (o de una clase derivada). – CodesInChaos

+0

tienes razón ... cierto para eventos ... tenía un caso con un simple controlador. Lo siento. Voy a tratar de editar. – Thomas

+0

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 el + = y - = usted mismo, eso no será un problema y la verificación nula será segura para subprocesos. – ForceMagic

2

No hay una penalización de rendimiento significativa para hablar, excepto, posiblemente, para algunas situaciones extremas.

Tenga en cuenta, sin embargo, que este truco se vuelve menos relevante en C# 6.0, debido a que el lenguaje proporciona una sintaxis alternativa a los delegados de llamadas que pueden ser nula:

delegateThatCouldBeNull?.Invoke(this, value); 

arriba, nula operador condicional ?. combina nula comprobación con una invocación condicional.

Cuestiones relacionadas