remarks practices cref best c# null

c# - practices - Comprobación nula profunda, ¿hay una mejor manera?



remarks c# (18)

Nota: esta pregunta fue hecha antes de la introducción del .? operador en C # 6 / Visual Studio 2015 .

Todos hemos estado allí, tenemos una propiedad profunda como cake.frosting.berries.loader que debemos verificar si es nula, así que no hay excepciones. La forma de hacerlo es usar una sentencia if en cortocircuito

if (cake != null && cake.frosting != null && cake.frosting.berries != null) ...

Esto no es exactamente elegante, y quizás debería haber una manera más fácil de verificar toda la cadena y ver si se compara con una variable / propiedad nula.

¿Es posible usar algún método de extensión o sería una característica del idioma, o es simplemente una mala idea?


Además de violar la Ley de Demeter, como Mehrdad Afshari ya ha señalado, me parece que necesita una "verificación profunda nula" para la lógica de decisión.

Este suele ser el caso cuando desea reemplazar objetos vacíos con valores predeterminados. En este caso, debería considerar implementar el Patrón de Objeto Nulo . Actúa como un sustituto de un objeto real, proporcionando valores predeterminados y métodos de "no acción".


Como se sugiere en la respuesta de John Leidegren , un enfoque para evitar esto es usar métodos de extensión y delegados. Usarlos podría verse más o menos así:

int? numberOfBerries = cake .NullOr(c => c.Frosting) .NullOr(f => f.Berries) .NullOr(b => b.Count());

La implementación es complicada porque debe hacer que funcione para tipos de valores, tipos de referencia y tipos de valores que aceptan valores. Puede encontrar una implementación completa en la respuesta de Timwi a ¿Cuál es la forma correcta de verificar valores nulos? .




He encontrado que esta extensión es bastante útil para escenarios de anidamiento profundo.

public static R Coal<T, R>(this T obj, Func<T, R> f) where T : class { return obj != null ? f(obj) : default(R); }

Es una idea que derivé del operador nulo coalescente en C # y T-SQL. Lo bueno es que el tipo de devolución siempre es el tipo de devolución de la propiedad interna.

De esa manera puedes hacer esto:

var berries = cake.Coal(x => x.frosting).Coal(x => x.berries);

... o una ligera variación de lo anterior:

var berries = cake.Coal(x => x.frosting, x => x.berries);

No es la mejor sintaxis que conozco, pero funciona.


Hemos considerado agregar una nueva operación "?." al lenguaje que tiene la semántica que quieres. (Y se ha agregado ahora, mira a continuación.) Es decir, dirías

cake?.frosting?.berries?.loader

y el compilador generará todos los controles de cortocircuito para usted.

No fue la barra para C # 4. Quizás para una versión futura hipotética del lenguaje.

Actualización (2014): el ?. el operador ahora está planeado para la próxima versión del compilador de Roslyn. Tenga en cuenta que todavía hay cierto debate sobre el análisis sintáctico y semántico exacto del operador.

Actualización (julio de 2015): Visual Studio 2015 se ha lanzado y se distribuye con un compilador C # que admite los operadores null-conditional ?. y ?[] .


Me gusta el enfoque adoptado por Objective C

"El lenguaje Objective-C toma otro enfoque para este problema y no invoca métodos en nil pero en su lugar devuelve nil para todas las invocaciones".

if (cake.frosting.berries != null) { var str = cake.frosting.berries...; }



Me inspiré en esta pregunta para tratar de descubrir cómo este tipo de comprobación profunda nula se puede hacer con una sintaxis más sencilla / bonita utilizando árboles de expresiones. Si bien estoy de acuerdo con las respuestas que afirman que podría ser un mal diseño si a menudo necesita acceder a instancias profundas en la jerarquía, también creo que en algunos casos, como la presentación de datos, puede ser muy útil.

Así que creé un método de extensión, que te permitirá escribir:

var berries = cake.IfNotNull(c => c.Frosting.Berries);

Esto devolverá las Bayas si ninguna parte de la expresión es nula. Si se encuentra null, se devuelve null. Aunque hay algunas advertencias, en la versión actual solo funcionará con el acceso de miembro simple, y solo funciona en .NET Framework 4, porque usa el método MemberExpression.Update, que es nuevo en v4. Este es el código para el método de extensión IfNotNull:

using System; using System.Collections.Generic; using System.Linq.Expressions; namespace dr.IfNotNullOperator.PoC { public static class ObjectExtensions { public static TResult IfNotNull<TArg,TResult>(this TArg arg, Expression<Func<TArg,TResult>> expression) { if (expression == null) throw new ArgumentNullException("expression"); if (ReferenceEquals(arg, null)) return default(TResult); var stack = new Stack<MemberExpression>(); var expr = expression.Body as MemberExpression; while(expr != null) { stack.Push(expr); expr = expr.Expression as MemberExpression; } if (stack.Count == 0 || !(stack.Peek().Expression is ParameterExpression)) throw new ApplicationException(String.Format("The expression ''{0}'' contains unsupported constructs.", expression)); object a = arg; while(stack.Count > 0) { expr = stack.Pop(); var p = expr.Expression as ParameterExpression; if (p == null) { p = Expression.Parameter(a.GetType(), "x"); expr = expr.Update(p); } var lambda = Expression.Lambda(expr, p); Delegate t = lambda.Compile(); a = t.DynamicInvoke(a); if (ReferenceEquals(a, null)) return default(TResult); } return (TResult)a; } } }

Funciona examinando el árbol de expresiones que representa su expresión y evaluando las partes una después de la otra; cada vez verificando que el resultado no sea nulo.

Estoy seguro de que esto podría extenderse para que otras expresiones además de MemberExpression sean compatibles. Considere esto como un código de prueba de concepto, y tenga en cuenta que habrá una penalización de rendimiento al usarlo (que probablemente no importe en muchos casos, pero no lo use en un ciclo cerrado :-))


Modifiqué ligeramente el código de aquí para que funcione para la pregunta:

public static class GetValueOrDefaultExtension { public static TResult GetValueOrDefault<TSource, TResult>(this TSource source, Func<TSource, TResult> selector) { try { return selector(source); } catch { return default(TResult); } } }

Y sí, probablemente esta no sea la solución óptima debido a las implicaciones de rendimiento de prueba / captura, pero funciona:>

Uso:

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);


O puedes usar la reflexión :)

Función de reflexión:

public Object GetPropValue(String name, Object obj) { foreach (String part in name.Split(''.'')) { if (obj == null) { return null; } Type type = obj.GetType(); PropertyInfo info = type.GetProperty(part); if (info == null) { return null; } obj = info.GetValue(obj, null); } return obj; }

Uso:

object test1 = GetPropValue("PropertyA.PropertyB.PropertyC",obj);

Mi caso (devuelve DBNull.Value en lugar de nulo en la función de reflexión):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));


Prueba este código:

/// <summary> /// check deep property /// </summary> /// <param name="obj">instance</param> /// <param name="property">deep property not include instance name example "A.B.C.D.E"</param> /// <returns>if null return true else return false</returns> public static bool IsNull(this object obj, string property) { if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(property.Trim())) throw new Exception("Parameter : property is empty"); if (obj != null) { string[] deep = property.Split(''.''); object instance = obj; Type objType = instance.GetType(); PropertyInfo propertyInfo; foreach (string p in deep) { propertyInfo = objType.GetProperty(p); if (propertyInfo == null) throw new Exception("No property : " + p); instance = propertyInfo.GetValue(instance, null); if (instance != null) objType = instance.GetType(); else return true; } return false; } else return true; }


Publiqué esta última noche y luego un amigo me señaló esta pregunta. Espero eso ayude. Entonces puedes hacer algo como esto:

var color = Dis.OrDat<string>(() => cake.frosting.berries.color, "blue"); using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Linq.Expressions; namespace DeepNullCoalescence { public static class Dis { public static T OrDat<T>(Expression<Func><T>> expr, T dat) { try { var func = expr.Compile(); var result = func.Invoke(); return result ?? dat; //now we can coalesce } catch (NullReferenceException) { return dat; } } } }

Lea la publicación completa del blog aquí .

El mismo amigo también sugirió que mires esto .


Si bien la respuesta de Driis es interesante, creo que es un poco costoso en cuanto a rendimiento. En lugar de compilar muchos delegados, preferiría compilar una ruta lambda por propiedad, guardarla en caché y luego reinvocarla de muchos tipos.

NullCoalesce a continuación simplemente hace eso, devuelve una nueva expresión lambda con comprobaciones nulas y un retorno de valor predeterminado (TResult) en caso de que cualquier ruta sea nula.

Ejemplo:

NullCoalesce((Process p) => p.StartInfo.FileName)

Devolverá una expresión

(Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string));

Código:

static void Main(string[] args) { var converted = NullCoalesce((MethodInfo p) => p.DeclaringType.Assembly.Evidence.Locked); var converted2 = NullCoalesce((string[] s) => s.Length); } private static Expression<Func<TSource, TResult>> NullCoalesce<TSource, TResult>(Expression<Func<TSource, TResult>> lambdaExpression) { var test = GetTest(lambdaExpression.Body); if (test != null) { return Expression.Lambda<Func<TSource, TResult>>( Expression.Condition( test, lambdaExpression.Body, Expression.Default( typeof(TResult) ) ), lambdaExpression.Parameters ); } return lambdaExpression; } private static Expression GetTest(Expression expression) { Expression container; switch (expression.NodeType) { case ExpressionType.ArrayLength: container = ((UnaryExpression)expression).Operand; break; case ExpressionType.MemberAccess: if ((container = ((MemberExpression)expression).Expression) == null) { return null; } break; default: return null; } var baseTest = GetTest(container); if (!container.Type.IsValueType) { var containerNotNull = Expression.NotEqual( container, Expression.Default( container.Type ) ); return (baseTest == null ? containerNotNull : Expression.AndAlso( baseTest, containerNotNull ) ); } return baseTest; }



Yo también a menudo he deseado una sintaxis más simple. Se vuelve especialmente desagradable cuando tiene valores de retorno de método que pueden ser nulos, porque entonces necesita variables adicionales (por ejemplo: cake.frosting.flavors.FirstOrDefault().loader )

Sin embargo, aquí hay una alternativa bastante decente que uso: crear un método de ayuda Null-Safe-Chain. Me doy cuenta de que esto es muy similar a la respuesta de @ John anterior (con el método de extensión Coal ) pero me parece más sencillo y menos tipeo. Esto es lo que parece:

var loader = NullSafe.Chain(cake, c=>c.frosting, f=>f.berries, b=>b.loader);

Aquí está la implementación:

public static TResult Chain<TA,TB,TC,TResult>(TA a, Func<TA,TB> b, Func<TB,TC> c, Func<TC,TResult> r) where TA:class where TB:class where TC:class { if (a == null) return default(TResult); var B = b(a); if (B == null) return default(TResult); var C = c(B); if (C == null) return default(TResult); return r(C); }

También creé varias sobrecargas (con 2 a 6 parámetros), así como sobrecargas que permiten que la cadena finalice con un valor-tipo o predeterminado. ¡Esto funciona realmente bien para mí!


donde necesita lograr esto, haga esto.

Uso

Color color = someOrder.ComplexGet(x => x.Customer.LastOrder.Product.Color);

o

Color color = Complex.Get(() => someOrder.Customer.LastOrder.Product.Color);

Implementación de clase Helper

public static class Complex { public static T1 ComplexGet<T1, T2>(this T2 root, Func<T2, T1> func) { return Get(() => func(root)); } public static T Get<T>(Func<T> func) { try { return func(); } catch (Exception) { return default(T); } } }


Actualización: comenzando con Visual Studio 2015, el compilador de C # (versión de lenguaje 6) ahora reconoce el ?. operador, lo que hace que la "verificación profunda nula" sea muy sencilla. Vea esta respuesta para más detalles.

Además de rediseñar el código, al igual que esta sugerencia de respuesta eliminada , otra opción (aunque terrible) sería utilizar un bloque try…catch para ver si ocurre una NullReferenceException algún momento durante esa búsqueda profunda de propiedades.

try { var x = cake.frosting.berries.loader; ... } catch (NullReferenceException ex) { // either one of cake, frosting, or berries was null ... }

Yo personalmente no haría esto por las siguientes razones:

  • No se ve bien
  • Utiliza el manejo de excepciones, que debe enfocarse en situaciones excepcionales y no en algo que usted espera que ocurra a menudo durante el curso normal de la operación.
  • NullReferenceException s probablemente nunca debería ser capturada explícitamente. (Ver esta pregunta )

Entonces, ¿es posible usar algún método de extensión o sería una función de idioma, [...]

Esto casi seguramente tendría que ser una característica del lenguaje (que está disponible en C # 6 en la forma de los operadores .? Y ?[] ), A menos que C # ya tenga una evaluación perezosa más sofisticada, o a menos que desee usar la reflexión (que probablemente tampoco es una buena idea por razones de rendimiento y seguridad de tipo).

Dado que no hay forma de pasar simplemente cake.frosting.berries.loader a una función (se evaluaría y lanzaría una excepción de referencia nula), tendría que implementar un método de búsqueda general de la siguiente manera: toma una objetos y los nombres de propiedades para buscar:

static object LookupProperty( object startingPoint, params string[] lookupChain ) { // 1. if ''startingPoint'' is null, return null, or throw an exception. // 2. recursively look up one property/field after the other from ''lookupChain'', // using reflection. // 3. if one lookup is not possible, return null, or throw an exception. // 3. return the last property/field''s value. } ... var x = LookupProperty( cake, "frosting", "berries", "loader" );

(Nota: código editado)

Rápidamente ve varios problemas con este enfoque. En primer lugar, no obtiene ningún tipo de seguridad y posible encajonamiento de valores de propiedad de un tipo simple. En segundo lugar, puede devolver null si algo sale mal, y deberá verificarlo en su función de llamada, o lanzar una excepción, y volverá al punto de partida. En tercer lugar, podría ser lento. En cuarto lugar, se ve más feo que con lo que comenzaste.

[...], o es solo una mala idea?

Me quedaría con:

if (cake != null && cake.frosting != null && ...) ...

o ve con la respuesta anterior de Mehrdad Afshari.

PD: Cuando escribí esta respuesta, obviamente no consideré los árboles de expresión para las funciones lambda; ver, por ejemplo, la respuesta de @driis para una solución en esta dirección. También se basa en un tipo de reflexión y, por lo tanto, podría no funcionar tan bien como una solución más simple ( if (… != null & … != null) … ), pero se puede juzgar mejor desde un punto de vista de sintaxis .