handling event delegate custom create and .net events delegates
clase EventsHelper de

delegate - La forma correcta de generar eventos en el marco.NET



event c# (6)

"Solo porque haya inicializado el evento con un delegado vacío no significa que un usuario de su clase no lo configure como nulo en algún momento y rompa su código".

No puede suceder Los eventos "solo pueden aparecer en el lado izquierdo de + = o - = (excepto cuando se usan desde dentro del tipo)" para citar el error que obtendrá al hacer esto. De acuerdo, el "excepto cuando se usa dentro del tipo" hace que esta sea una posibilidad teórica, pero no una que preocupe a ningún desarrollador sensato.

Actualmente, " Evitar buscar controladores de eventos nulos" se encuentra en la parte superior de las respuestas a la publicación titulada Características ocultas de C # y contiene información muy engañosa.

Si bien entiendo que Stack Overflow es una "democracia" y la respuesta se elevó a la cima en virtud de la votación pública, creo que muchas personas que votaron por la respuesta o no entendieron completamente C # /. NET o no se tomó el tiempo para comprender completamente las consecuencias de la práctica descrita en la publicación.

En resumen, la publicación aboga por el uso de la siguiente construcción para evitar tener que comprobar nulo al invocar el evento.

public event EventHandler SomeEvent = delegate {}; // Later.. void DoSomething() { // Invoke SomeEvent without having to check for null reference SomeEvent(this, EventArgs.Empty); }

A primera vista, esto puede parecer un atajo inteligente, pero puede ser la causa de algunos dolores de cabeza graves en una aplicación grande, especialmente si se trata de concurrencia.

Antes de llamar al delegado de un evento, debe verificar si hay una referencia nula . El hecho de que haya inicializado el evento con un delegado vacío no significa que un usuario de su clase no lo configure como nulo en algún momento y rompa su código.

Algo como esto es típico:

void DoSomething() { if(SomeEvent != null) SomeEvent(this, EventArgs.Empty); }

Pero incluso en el ejemplo anterior, existe la posibilidad de que mientras DoSomething () se puede ejecutar por un subproceso, otro podría eliminar los controladores de eventos, y podría producirse una condición de carrera.

Asuma este escenario:

Thread A. Thread B. ------------------------------------------------------------------------- 0: if(SomeEvent != null) 1: { // remove all handlers of SomeEvent 2: SomeEvent(this, EventArgs.Empty); 3: }

El subproceso B quita los controladores de eventos del evento SomeEvent después de que el código que provoca el evento ha comprobado que el delegado es una referencia nula, pero antes de llamar al delegado. Cuando el SomeEvent (esto, EventArgs.Empty); se realiza la llamada, SomeEvent es nulo y se genera una excepción.

Para evitar esa situación, un mejor patrón para plantear eventos es este:

void DoSomething() { EventHandler handler = SomeEvent; if(handler != null) { handler(this, EventArgs.Empty); } }

Para una extensa discusión sobre el tema de EventHandlers en .NET, sugiero leer " Framework Design Guidelines " por Krzysztof Cwalina y Brad Abrams, Capítulo 5, Sección 4 - Diseño de eventos. Especialmente las discusiones sobre el tema por Eric Gunnerson y Joe Duffy.

Como sugirió Eric, en una de las respuestas a continuación, debo señalar que se podría idear una mejor solución de sincronización que solucione el problema. Mi objetivo con este post fue crear conciencia y no proporcionar una solución única y verdadera para el problema. Como lo sugirieron Eric Lippert y Eric Gunnerson en el libro mencionado anteriormente, la solución particular al problema depende del programador, pero lo importante es que el problema no se descarte.

Es de esperar que un moderador anote la respuesta en cuestión para que los lectores desprevenidos no se deje engañar por un mal patrón.


Brumme es el padre de Eric y Abrams ... deberías leer su blog en lugar de predicar de cualquiera de los dos publicistas. El hombre es muy técnico (a diferencia de logos de peluqueros de alto nivel). Él le dará una explicación adecuada sin ''Redmond Flowers en 1TB land'' sobre por qué las razas y los modelos de memoria son un desafío para un entorno administrado (re: shield-the-children) como lo planteó otro póster anterior.

Por cierto, todo comienza con ellos, la implementación CLR de C ++ chicos:

blogs.msdn.com/cbrumme


Planteé el mismo problema hace una semana y llegué a la conclusión opuesta:

C # Eventos y seguridad de subprocesos

¡Su resumen no hace nada para persuadirme de lo contrario!

Primero, los clientes de la clase no pueden asignar null al evento. Ese es el objetivo de la palabra clave del event . Sin esa palabra clave, sería un campo que contiene un delegado. Con él, todas las operaciones en él son privadas, excepto el alistamiento y la exclusión.

Como resultado, la asignación del delegate {} al evento en construcción cumple completamente los requisitos de una implementación correcta de un origen de evento.

Por supuesto, dentro de la clase puede haber un error donde el evento se establece en null . Sin embargo, en cualquier clase que contenga un campo de cualquier tipo, puede haber un error que establece el campo como null . ¿Diría usted que cada vez que se accede a CUALQUIER campo miembro de una clase, escribimos un código como este?

// field declaration: private string customerName; private void Foo() { string copyOfCustomerName = customerName; if (copyOfCustomerName != null) { // Now we can use copyOfCustomerName safely... } }

Por supuesto que no lo harías Todos los programas serían dos veces más largos y la mitad legibles, sin una buena razón. La misma locura ocurre cuando las personas aplican esta "solución" a los eventos. Los eventos no son públicos para la asignación, al igual que los campos privados, por lo que es seguro usarlos directamente, siempre y cuando los inicialice en el delegado vacío en la construcción.

La única situación en la que no puedes hacer esto es cuando tienes un evento en una struct , pero eso no es exactamente un inconveniente, ya que los eventos tienden a aparecer en objetos mutables (lo que indica un cambio en el estado) y las struct son un truco notorio si se les permite mutar, por lo tanto, es mejor hacerlos inmutables, y por lo tanto los eventos son de poca utilidad con struct s.

Puede existir otra condición de carrera bastante separada, como describí en mi pregunta: ¿qué sucede si el cliente (el receptor del evento) quiere asegurarse de que no se llamará a su controlador una vez que se ha eliminado de la lista? Pero como Eric Lippert señaló, esa es la responsabilidad del cliente de resolver. En resumen: es imposible garantizar que no se invocará un controlador de eventos una vez que se haya eliminado de la lista. Esta es una consecuencia inevitable de que los delegados sean inmutables. Esto es cierto tanto si los hilos están involucrados o no.

En la publicación del blog de Eric Lippert, se vincula a mi pregunta SO, pero luego plantea una pregunta diferente pero similar . Hizo esto con un propósito retórico legítimo, creo, solo para preparar el escenario para una discusión sobre la condición de carrera secundaria, la que afecta a los que manejan el evento. Pero desafortunadamente, si sigues el enlace a mi pregunta y luego lees un poco descuidadamente la publicación de su blog, es posible que tengas la impresión de que está desechando la técnica del "delegado vacío".

De hecho, dice "Hay otras maneras de resolver este problema, por ejemplo, inicializando el controlador para tener una acción vacía que nunca se elimina", que es la técnica del "delegado vacío".

Cubre "hacer una comprobación nula" porque es "el patrón estándar"; mi pregunta era, ¿por qué es este el patrón estándar? Jon Skeet sugirió que dado que el consejo es anterior a las funciones anónimas que se agregan al lenguaje, es probable que sea solo una resaca de la versión 1 de C #, y creo que es casi seguro que así sea, así que acepté su respuesta.


Por lo que vale, realmente deberías mirar la clase EventsHelper de Juval Lowy en lugar de hacer las cosas tú mismo.



Solo para aclarar. El enfoque que usa el delegado vacío como el valor inicial para el evento funciona incluso cuando se usa con serialización:

// to run in linqpad: // - add reference to System.Runtime.Serialization.dll // - add using directives for System.IO and System.Runtime.Serialization.Formatters.Binary void Main() { var instance = new Foo(); Foo instance2; instance.Bar += (s, e) => Console.WriteLine("Test"); var formatter = new BinaryFormatter(); using(var stream = new MemoryStream()) { formatter.Serialize(stream, instance); stream.Seek(0, SeekOrigin.Begin); instance2 = (Foo)formatter.Deserialize(stream); } instance2.RaiseBar(); } [Serializable] class Foo { public event EventHandler Bar = delegate { }; public void RaiseBar() { Bar(this, EventArgs.Empty); } } // Define other methods and classes here