Verificação nula profunda, existe uma maneira melhor?

130

Nota: Esta pergunta foi feita antes da introdução do .?operador em C # 6 / Visual Studio 2015 .

Todos nós já estivemos lá, temos algumas propriedades profundas, como cake.frosting.berries.loader, que precisamos verificar se é nulo, para que não haja exceção. A maneira de fazer é usar uma instrução if de curto-circuito

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

Isso não é exatamente elegante e talvez deva haver uma maneira mais fácil de verificar toda a cadeia e verificar se ela se depara com uma variável / propriedade nula.

É possível usar algum método de extensão ou seria um recurso de linguagem ou é apenas uma má idéia?

Homde
fonte
3
Eu já desejei isso com bastante frequência - mas todas as idéias que tive foram piores que o problema real.
Peterchen
Obrigado por todas as respostas e interessante por ver que outras pessoas tiveram os mesmos pensamentos. Eu comecei a pensar em como eu gostaria que isso fosse resolvido e, embora as soluções de Eric sejam boas, eu acho que simplesmente escreveria algo assim se (IsNull (abc)) ou se (IsNotNull (abc)), mas talvez isso é apenas para o meu gosto :)
Homde 17/01/2010
Quando você instancia o glacê, ele tem uma propriedade de bagas; portanto, nesse ponto do construtor, você pode dizer ao glacê que sempre que é instado a criar um bagas vazias (não nulas)? e sempre que as bagas são modificadas, a geada faz a verificação do valor ????
Doug Chamberlain
De alguma forma pouco relacionada, algumas das técnicas aqui consideradas preferíveis para o problema dos "nulos profundos" que eu estava tentando contornar. stackoverflow.com/questions/818642/…
AaronLS

Respostas:

223

Consideramos adicionar uma nova operação "?." para o idioma que possui a semântica desejada. (E foi adicionado agora; veja abaixo.) Ou seja, você diria

cake?.frosting?.berries?.loader

e o compilador geraria todas as verificações de curto-circuito para você.

Não chegou ao C # 4. Talvez para uma versão futura hipotética da linguagem.

Atualização (2014): O ?.operador agora está planejado para a próxima versão do compilador Roslyn. Observe que ainda há algum debate sobre a análise sintática e semântica exata do operador.

Atualização (julho de 2015): o Visual Studio 2015 foi lançado e é fornecido com um compilador C # que suporta os operadores ?.e condicionais nulos?[] .

Eric Lippert
fonte
10
Sem o ponto, ele se torna sintaticamente ambíguo com o operador condicional (A? B: C). Tentamos evitar construções lexicais que exigem que "olhemos adiante" arbitrariamente longe no fluxo de tokens. (Embora, infelizmente, já existem tais construções em C #; que prefiro não adicionar mais.)
Eric Lippert
33
@ Ian: este problema é extremamente comum. Este é um dos pedidos mais frequentes que recebemos.
Eric Lippert
7
@ Ian: Eu também prefiro usar o padrão de objetos nulos quando possível, mas a maioria das pessoas não tem o luxo de trabalhar com modelos de objetos que eles mesmos projetaram. Muitos modelos de objetos existentes usam nulos e esse é o mundo com o qual temos que conviver.
Eric Lippert
12
@ John: Recebemos esse pedido de recurso quase inteiramente de nossos programadores mais experientes. Os MVPs pedem isso o tempo todo . Mas entendo que as opiniões variam; se você gostaria de dar uma sugestão construtiva de design de linguagem, além de suas críticas, fico feliz em considerá-la.
Eric Lippert
28
@ Lazyberezovsky: Eu nunca entendi a chamada "lei" de Demeter; Em primeiro lugar, parece ser chamado com mais precisão de "A sugestão de Deméter". E segundo, o resultado de ter "acesso de apenas um membro" à sua conclusão lógica é "objetos de Deus", onde todos os objetos são obrigados a fazer tudo para cada cliente, em vez de poder distribuir objetos que sabem como fazer o que o cliente quer. Eu prefiro o exato oposto da lei de Deméter: cada objeto resolve um pequeno número de problemas bem, e uma dessas soluções pode ser "aqui está outro objeto que resolve o seu problema melhor"
Eric Lippert
27

Eu me inspirei nessa pergunta para tentar descobrir como esse tipo de verificação nula profunda pode ser feita com uma sintaxe mais fácil / mais bonita usando árvores de expressão. Embora eu concorde com as respostas afirmando que pode ser um design ruim se você precisar acessar instâncias profundas na hierarquia, também acho que, em alguns casos, como a apresentação de dados, pode ser muito útil.

Então, eu criei um método de extensão, que permitirá que você escreva:

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

Isso retornará os Berries se nenhuma parte da expressão for nula. Se nulo for encontrado, nulo será retornado. No entanto, existem algumas ressalvas: na versão atual, ele funciona apenas com acesso simples a membros e funciona no .NET Framework 4, porque usa o método MemberExpression.Update, que é novo na v4. Este é o código para o método de extensão 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;            
        }
    }
}

Ele funciona examinando a árvore de expressões que representa sua expressão e avaliando as partes uma após a outra; sempre que verificar se o resultado não é nulo.

Estou certo de que isso pode ser estendido para que outras expressões que não sejam MemberExpression sejam suportadas. Considere isso como um código de prova de conceito e lembre-se de que haverá uma penalidade de desempenho ao usá-lo (o que provavelmente não será importante em muitos casos, mas não o use em um circuito fechado :-))

driis
fonte
Eu estou impressionado com suas habilidades lambda :) a sintaxe tem, no entanto parecem ser um pouco pouco mais complexa do que a gente gostaria, pelo menos para o cenário se-declaração
Homde
Legal, mas funciona como código 100x mais do que um if .. &&. Só vale a pena se ainda compilar para um if .. &&.
Monstieur
1
Ah e então eu vi DynamicInvokelá. I religiosamente evitar que :)
Nawfal
24

Eu achei essa extensão bastante útil para cenários de aninhamento profundo.

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

É uma ideia que tirei do operador coalescente nulo em C # e T-SQL. O bom é que o tipo de retorno é sempre o tipo de retorno da propriedade interna.

Dessa forma, você pode fazer isso:

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

... ou uma ligeira variação do anterior:

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

Não é a melhor sintaxe que conheço, mas funciona.

John Leidegren
fonte
Por que "Carvão", isso parece extremamente assustador. ;) No entanto, sua amostra falharia se a cobertura fosse nula. Deveria ter sido assim: var berries = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke
Ah, mas você está sugerindo que o segundo argumento não é um chamado à Coal que, é claro, deve ser. É apenas uma alteração conveniente. O seletor (x => x.berries) é passado para uma chamada Coal dentro do método Coal que recebe dois argumentos.
John Leidegren
O nome coalescência ou coalescência foi tirado do T-SQL, foi aí que eu tive a idéia. IfNotNull implica que algo ocorre se não for nulo, no entanto, o que é isso, não é explicado pela chamada do método IfNotNull. Carvão é de fato um nome estranho, mas esse é, de fato, um método estranho que vale a pena observar.
John Leidegren
O melhor nome literalmente para isso seria algo como "ReturnIfNotNull" ou "ReturnOrDefault" #
911 John Leidegren
@flq +1 ... no nosso projecto é também chamado IfNotNull :)
Marc Sigrist
16

Além de violar a Lei de Demeter, como Mehrdad Afshari já apontou, parece-me que você precisa de "verificação nula profunda" para a lógica de decisão.

Geralmente, esse é o caso em que você deseja substituir objetos vazios por valores padrão. Nesse caso, você deve considerar a implementação do Padrão de Objeto Nulo . Ele atua como substituto de um objeto real, fornecendo valores padrão e métodos de "não ação".

Johannes Rudolph
fonte
não, o objetivo-c permite enviar mensagens para objetos nulos e retorna o valor padrão apropriado, se necessário. Sem problemas lá.
Johannes Rudolph
2
Sim. Essa é a questão. Basicamente, você emulará o comportamento ObjC com o Null Object Pattern.
Mehrdad Afshari
10

Atualização: a partir do Visual Studio 2015, o compilador C # (versão 6 do idioma) agora reconhece o ?.operador, o que facilita a "verificação nula profunda". Veja esta resposta para detalhes.

Além de redesenhar seu código, como sugeriu esta resposta excluída , outra opção (ainda que terrível) seria usar um try…catchbloco para verificar se isso NullReferenceExceptionocorre em algum momento durante essa pesquisa profunda de propriedade.

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

Eu pessoalmente não faria isso pelos seguintes motivos:

  • Não parece legal.
  • Ele usa tratamento de exceção, que deve visar situações excepcionais e não algo que você espera que ocorra frequentemente durante o curso normal da operação.
  • NullReferenceExceptionProvavelmente, nunca deve ser capturado explicitamente. (Veja esta pergunta .)

Então, é possível usar algum método de extensão ou seria um recurso de linguagem, [...]

Isso seria quase certamente tem que ser um recurso de linguagem (que está disponível em C # 6 na forma do .?e ?[]operadores), a menos que C # já teve avaliação preguiçosa mais sofisticado, ou menos que você queira usar a reflexão (que provavelmente também não é uma boa ideia por razões de desempenho e segurança do tipo).

Como não há como simplesmente passar cake.frosting.berries.loaderpara uma função (ela seria avaliada e lançaria uma exceção de referência nula), você teria que implementar um método de pesquisa geral da seguinte maneira: Ele pega objetos e nomes de propriedades para olho para cima:

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.)

Você vê rapidamente vários problemas com essa abordagem. Primeiro, você não obtém nenhum tipo de segurança e possível boxe de valores de propriedades de um tipo simples. Segundo, você pode retornar nullse algo der errado e terá que verificar isso em sua função de chamada ou lançar uma exceção e voltar ao ponto em que começou. Terceiro, pode ser lento. Quarto, parece mais feio do que você começou.

[...] ou é apenas uma má ideia?

Eu ficaria com:

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

ou vá com a resposta acima de Mehrdad Afshari.


PS: Quando escrevi esta resposta, obviamente não considerava árvores de expressão para funções lambda; veja, por exemplo, a resposta do @driis 'para uma solução nessa direção. Também é baseado em um tipo de reflexão e, portanto, pode não ter um desempenho tão bom quanto em uma solução mais simples ( if (… != null & … != null) …), mas pode ser considerado melhor do ponto de vista da sintaxe.

stakx - não está mais contribuindo
fonte
2
Eu não sei por que isso foi downvoted, eu fiz um upvote para o equilíbrio: A resposta é correta e traz um novo aspecto (e explicitamente menciona as desvantagens desta solução ...)
MartinStettner
onde está "a resposta acima de Mehrdad Afshari"?
Marson Mao
1
@MarsonMao: Essa resposta foi excluída nesse meio tempo. (Você ainda pode lê-lo se sua classificação de SO for suficientemente alta.) Obrigado por apontar meu erro: devo me referir a outras respostas usando um hiperlink, não usando palavras como "veja acima" / "veja abaixo" (desde as respostas não aparecem em uma ordem fixa). Eu atualizei minha resposta.
stakx - não contribui mais com
5

Embora a resposta dos driis seja interessante, acho que é um desempenho um pouco caro demais. Em vez de compilar muitos delegados, prefiro compilar um lambda por caminho de propriedade, armazená-lo em cache e reinvocá-lo de vários tipos.

NullCoalesce abaixo faz exatamente isso, ele retorna uma nova expressão lambda com verificações nulas e um retorno de padrão (TResult) no caso de qualquer caminho ser nulo.

Exemplo:

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

Retornará uma expressão

(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;
    }
Apostar
fonte
3

Eu também frequentemente desejei uma sintaxe mais simples! Fica especialmente feio quando você tem valores de retorno de método que podem ser nulos, porque você precisa de variáveis ​​extras (por exemplo cake.frosting.flavors.FirstOrDefault().loader:)

No entanto, aqui está uma alternativa bastante decente que eu uso: crie um método auxiliar Null-Safe-Chain. Percebo que isso é bastante semelhante à resposta de @ John acima (com o Coalmétodo de extensão), mas acho que é mais direto e menos digitado. Aqui está o que parece:

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

Aqui está a implementação:

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);
}

Também criei várias sobrecargas (com 2 a 6 parâmetros), bem como sobrecargas que permitem que a cadeia termine com um tipo de valor ou padrão. Isso funciona muito bem para mim!

Scott Rippey
fonte
1

Como sugerido na John Leidegren 's resposta , uma abordagem para trabalho em torno isso é usar métodos de extensão e delegados. Usá-los pode ser algo como isto:

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

A implementação é confusa porque é necessário fazê-la funcionar para tipos de valor, tipos de referência e tipos de valor anuláveis. Você pode encontrar uma implementação completa em Timwi 's resposta para o que é a maneira correta para verificar se há valores nulos? .

Sam
fonte
1

Ou você pode usar a reflexão :)

Função de reflexão:

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);

My Case (retorne DBNull.Value em vez de nulo na função de reflexão):

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

Tente 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;
    }
JKSUN
fonte
0

Postei isso ontem à noite e um amigo me indicou essa pergunta. Espero que ajude. Você pode fazer algo assim:

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;
      }
    }
  }
}

Leia o post completo do blog aqui .

O mesmo amigo também sugeriu que você assista a isso .

Tyler Jensen
fonte
3
Por que se preocupar com um Expressionse você está indo apenas para compilar e pegar? Basta usar um Func<T>.
Scott Rippey
0

Modifiquei levemente o código daqui para fazê-lo funcionar na pergunta:

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); }
    }
}

E sim, essa provavelmente não é a solução ideal devido a implicações de desempenho de tentativa / captura, mas funciona:>

Uso:

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

Onde você precisa conseguir isso, faça o seguinte:

Uso

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

ou

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

Implementação da classe auxiliar

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);
        }
    }
}
kernowcode
fonte
-3

Eu gosto da abordagem adotada pelo Objective-C:

"A linguagem Objective-C adota outra abordagem para esse problema e não invoca métodos em zero, mas retorna nulo para todas essas invocações".

if (cake.frosting.berries != null) 
{
    var str = cake.frosting.berries...;
}
Shyam vemuri
fonte
1
o que outro idioma faz (e sua opinião) é quase totalmente irrelevante para fazê-lo funcionar em c #. Ele não ajuda ninguém para resolver seu problema C #
Adyson