Llamada al método si no es nulo en C #

106

¿Es posible acortar de alguna manera esta afirmación?

if (obj != null)
    obj.SomeMethod();

porque escribo esto mucho y se vuelve bastante molesto. Lo único que se me ocurre es implementar un objeto nulo patrón de , pero eso no es lo que puedo hacer siempre y ciertamente no es una solución para acortar la sintaxis.

Y un problema similar con los eventos, donde

public event Func<string> MyEvent;

y luego invocar

if (MyEvent != null)
    MyEvent.Invoke();
Jakub Arnold
fuente
41
Consideramos agregar un nuevo operador a C # 4: "obj.?SomeMethod ()" significaría "llamar a SomeMethod si obj no es nulo, de lo contrario, devolver nulo". Desafortunadamente, no encajaba en nuestro presupuesto, por lo que nunca lo implementamos.
Eric Lippert
@Eric: ¿Este comentario sigue siendo válido? He visto en alguna parte que, ¿está disponible con 4.0?
CharithJ
@CharithJ: No. Nunca se implementó.
Eric Lippert
3
@CharithJ: Soy consciente de la existencia del operador de fusión nula. No hace lo que quiere Darth; quiere un operador de acceso de miembros elevado a nulo. (Y, por cierto, su comentario anterior da una caracterización incorrecta del operador coalescente nulo. Quería decir "v = m == null? Y: m. El valor se puede escribir v = m ?? y".)
Eric Lippert
4
Para lectores más nuevos: C # 6.0 implementa?., Entonces x? .Y? .Z? .ToString () devolverá nulo si x, yoz son nulos, o devolverá z.ToString () si ninguno de ellos es nulo.
David

Respuestas:

162

Desde C # 6 en adelante, puede usar:

MyEvent?.Invoke();

o:

obj?.SomeMethod();

El ?.es el operador de propagación nula, y provocará .Invoke()un cortocircuito cuando el operando esténull . Solo se accede al operando una vez, por lo que no hay riesgo de que se produzca el problema de "cambios de valor entre verificación e invocación".

===

Antes de C # 6, no: no hay magia segura para nulos, con una excepción; métodos de extensión, por ejemplo:

public static void SafeInvoke(this Action action) {
    if(action != null) action();
}

ahora esto es válido:

Action act = null;
act.SafeInvoke(); // does nothing
act = delegate {Console.WriteLine("hi");}
act.SafeInvoke(); // writes "hi"

En el caso de los eventos, esto tiene la ventaja de eliminar también la condición de carrera, es decir, no necesita una variable temporal. Entonces, normalmente necesitarías:

var handler = SomeEvent;
if(handler != null) handler(this, EventArgs.Empty);

pero con:

public static void SafeInvoke(this EventHandler handler, object sender) {
    if(handler != null) handler(sender, EventArgs.Empty);
}

podemos usar simplemente:

SomeEvent.SafeInvoke(this); // no race condition, no null risk
Marc Gravell
fuente
1
Estoy un poco en conflicto con esto. En el caso de una acción o un controlador de eventos, que suele ser más independiente, esto tiene cierto sentido. Sin embargo, no rompería la encapsulación por un método regular. Esto implica crear el método en una clase estática separada y no creo que la pérdida de encapsulación y la degradación de la legibilidad / organización del código en general valga la pena la pequeña mejora en la legibilidad local
tvanfosson
@tvanfosson - de hecho; pero mi punto es que este es el único caso que sé de dónde funcionará. Y la pregunta en sí plantea el tema de los delegados / eventos.
Marc Gravell
Ese código termina generando un método anónimo en algún lugar, lo que realmente estropea el seguimiento de la pila de cualquier excepción. ¿Es posible dar nombres a métodos anónimos? ;)
sisve
¿Incorrecto según 2015 / C # 6.0 ...? es la solucion. ReferenceTHatMayBeNull? .CallMethod () no llamará al método cuando sea nulo.
TomTom
1
@mercu debería ser ?.- en VB14 y superior
Marc Gravell
27

Lo que estamos buscando es el condicional nulo (no "coalescencia") del operador: ?.. Está disponible a partir de C # 6.

Tu ejemplo sería obj?.SomeMethod();. Si obj es nulo, no sucede nada. Cuando el método tiene argumentos, por ejemplo, obj?.SomeMethod(new Foo(), GetBar());los argumentos no se evalúan si objes nulo, lo que importa si la evaluación de los argumentos tendría efectos secundarios.

Y el encadenamiento es posible: myObject?.Items?[0]?.DoSomething()

Vimes
fuente
1
Esto es fantástico. Vale la pena señalar que esta es una característica de C # 6 ... (que estaría implícita en su declaración VS2015, pero aún así vale la pena señalarla). :)
Kyle Goode
10

Un método de extensión rápido:

    public static void IfNotNull<T>(this T obj, Action<T> action, Action actionIfNull = null) where T : class {
        if(obj != null) {
            action(obj);
        } else if ( actionIfNull != null ) {
            actionIfNull();
        }
    }

ejemplo:

  string str = null;
  str.IfNotNull(s => Console.Write(s.Length));
  str.IfNotNull(s => Console.Write(s.Length), () => Console.Write("null"));

o alternativamente:

    public static TR IfNotNull<T, TR>(this T obj, Func<T, TR> func, Func<TR> ifNull = null) where T : class {
        return obj != null ? func(obj) : (ifNull != null ? ifNull() : default(TR));
    }

ejemplo:

    string str = null;
    Console.Write(str.IfNotNull(s => s.Length.ToString());
    Console.Write(str.IfNotNull(s => s.Length.ToString(), () =>  "null"));
katbyte
fuente
Tenía un intento de hacerlo con los métodos de extensión y que terminó con más o menos el mismo código. Sin embargo, el problema con esta implementación es que no funcionará con expresiones que devuelvan un tipo de valor. Entonces tenía que tener un segundo método para eso.
orad
4

Los eventos se pueden inicializar con un delegado predeterminado vacío que nunca se elimina:

public event EventHandler MyEvent = delegate { };

No se necesita verificación de nulos.

[ Actualización , gracias a Bevan por señalar esto]

Sin embargo, tenga en cuenta el posible impacto en el rendimiento. Un micro benchmark rápido que hice indica que manejar un evento sin suscriptores es 2-3 veces más lento cuando se usa el patrón de "delegado predeterminado". (En mi portátil de 2,5 GHz de doble núcleo, eso significa 279 ms: 785 ms para generar 50 millones de eventos no suscritos). Para los puntos calientes de la aplicación, ese podría ser un tema a considerar.

Sven Künzler
fuente
1
Entonces, evita una verificación nula invocando un delegado vacío ... ¿un sacrificio medible de memoria y tiempo para ahorrar algunas pulsaciones de teclas? YMMV, pero para mí, una mala compensación.
Bevan
También he visto puntos de referencia que muestran que es considerablemente más caro invocar un evento con varios delegados suscritos que con uno solo.
Greg D
2

Este artículo de Ian Griffiths ofrece dos soluciones diferentes al problema que, según él, son trucos ingeniosos que no debes usar.

Darrel Miller
fuente
3
Y hablando como autor de ese artículo, agregaría que definitivamente no debería usarlo ahora que C # 6 resuelve esto de inmediato con los operadores condicionales nulos (?. Y. []).
Ian Griffiths
2

Cerar el método de extensión como el sugerido no resuelve realmente los problemas con las condiciones de carrera, sino que los oculta.

public static void SafeInvoke(this EventHandler handler, object sender)
{
    if (handler != null) handler(sender, EventArgs.Empty);
}

Como se dijo, este código es el equivalente elegante a la solución con variable temporal, pero ...

El problema con ambos es que es posible que el suscriptor del evento se llame DESPUÉS de que se haya cancelado la suscripción del evento . Esto es posible porque la cancelación de la suscripción puede ocurrir después de que la instancia de delegado se copia a la variable temporal (o se pasa como parámetro en el método anterior), pero antes de que se invoque al delegado.

En general, el comportamiento del código del cliente es impredecible en tal caso: el estado del componente ya no podía permitir manejar la notificación de eventos. Es posible escribir el código del cliente en la forma de manejarlo, pero le pondría una responsabilidad innecesaria al cliente.

La única forma conocida de garantizar la seguridad del hilo es utilizar la declaración de bloqueo para el remitente del evento. Esto asegura que todas las suscripciones \ cancelaciones de suscripción \ invocaciones estén serializadas.

Para ser más preciso, el bloqueo debe aplicarse al mismo objeto de sincronización utilizado en los métodos de acceso de evento add / remove, que es el predeterminado 'this'.

andrey.tsykunov
fuente
Esta no es la condición de carrera a la que se hace referencia. La condición de carrera es si (MyEvent! = Null) // MyEvent no es nulo ahora MyEvent.Invoke (); // MyEvent es nulo ahora, suceden cosas malas Entonces, con esta condición de carrera, no puedo escribir un controlador de eventos asíncrono que esté garantizado para funcionar. Sin embargo, con su 'condición de carrera', puedo escribir un controlador de eventos asíncrono que esté garantizado para funcionar. Por supuesto, las llamadas al suscriptor y al cancelador de suscripción deben ser sincrónicas, o se requiere un código de bloqueo allí.
jyoung
En efecto. Mi análisis de este problema está publicado aquí: blogs.msdn.com/ericlippert/archive/2009/04/29/…
Eric Lippert
2

Estoy de acuerdo con la respuesta de Kenny Eliasson. Vaya con los métodos de extensión. Aquí hay una breve descripción general de los métodos de extensión y el método IfNotNull requerido.

Métodos de extensión (método IfNotNull)

Rohit
fuente
1

Tal vez no sea mejor, pero en mi opinión es más legible crear un método de extensión.

public static bool IsNull(this object obj) {
 return obj == null;
}
Kenny Eliasson
fuente
1
que return obj == nullsignifica. ¿Qué devolverá
Hammad Khan
1
Significa que si objes así, nullel método volverá true, creo.
Joel
1
¿Cómo responde esto siquiera a la pregunta?
Kugel
Respuesta tardía, pero ¿estás seguro de que esto funcionaría? ¿Puede llamar a un método de extensión en un objeto que es null? Estoy bastante seguro de que esto no funciona con los métodos propios del tipo, por lo que dudo que funcione tampoco con los métodos de extensión. Creo que la mejor forma de comprobar si un objeto lo es null es obj is null. Desafortunadamente, para verificar si un objeto no lo es, es nullnecesario envolverlo entre paréntesis, lo cual es desafortunado.
natiiix