C # maneira elegante de verificar se a propriedade de uma propriedade é nula

97

Em C #, digamos que você deseja extrair um valor de PropertyC neste exemplo e ObjectA, PropertyA e PropertyB podem ser nulos.

ObjectA.PropertyA.PropertyB.PropertyC

Como posso obter PropertyC com segurança com o mínimo de código?

Agora eu verificaria:

if(ObjectA != null && ObjectA.PropertyA !=null && ObjectA.PropertyA.PropertyB != null)
{
    // safely pull off the value
    int value = objectA.PropertyA.PropertyB.PropertyC;
}

Seria bom fazer algo mais parecido com isso (pseudo-código).

int value = ObjectA.PropertyA.PropertyB ? ObjectA.PropertyA.PropertyB : defaultVal;

Possivelmente, ainda mais colapsou com um operador de coalescência nula.

EDITAR Originalmente, eu disse que meu segundo exemplo era como js, ​​mas eu mudei para psuedo-code, pois foi corretamente apontado que não funcionaria em js.

Jon Kragh
fonte
não vejo como funciona o seu exemplo js. você deve obter um erro "objeto esperado" sempre que ObjectAou PropertyAforem nulos.
Lincolnk

Respostas:

117

Em C # 6, você pode usar o Operador Condicional Nulo . Portanto, o teste original será:

int? value = objectA?.PropertyA?.PropertyB?.PropertyC;
Phillip Ngan
fonte
2
você poderia explicar o que isso faz? O que é valueigual a se PropertyCfor nulo? ou se PropertyBfor nulo? e se Object Afor nulo?
Kolob Canyon
1
Se QUALQUER uma dessas propriedades for nula, a instrução inteira retornará como null. Começa da esquerda para a direita. Sem o açúcar sintático, isso é equivalente a uma série de declarações if em que if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......a última expressão eventual sendoif(propertyZ != null) { value = propertyZ }
DetetivePikachu
27

Método de extensão curta:

public static TResult IfNotNull<TInput, TResult>(this TInput o, Func<TInput, TResult> evaluator)
  where TResult : class where TInput : class
{
  if (o == null) return null;
  return evaluator(o);
}

Usando

PropertyC value = ObjectA.IfNotNull(x => x.PropertyA).IfNotNull(x => x.PropertyB).IfNotNull(x => x.PropertyC);

Este método de extensão simples e muito mais você pode encontrar em http://devtalk.net/csharp/chained-null-checks-and-the-maybe-monad/

EDITAR:

Depois de usá-lo por um momento, acho que o nome apropriado para esse método deveria ser IfNotNull () em vez de With () original.

Krzysztof Morcinek
fonte
15

Você pode adicionar um método à sua classe? Se não, você já pensou em usar métodos de extensão? Você pode criar um método de extensão para o seu tipo de objeto chamado GetPropC().

Exemplo:

public static class MyExtensions
{
    public static int GetPropC(this MyObjectType obj, int defaltValue)
    {
        if (obj != null && obj.PropertyA != null & obj.PropertyA.PropertyB != null)
            return obj.PropertyA.PropertyB.PropertyC;
        return defaltValue;
    }
}

Uso:

int val = ObjectA.GetPropC(0); // will return PropC value, or 0 (defaltValue)

A propósito, isso pressupõe que você esteja usando o .NET 3 ou superior.

Sam
fonte
11

A maneira como você está fazendo é correta.

Você pode usar um truque como o descrito aqui , usando expressões Linq:

int value = ObjectA.NullSafeEval(x => x.PropertyA.PropertyB.PropertyC, 0);

Mas é muito mais lento do que verificar manualmente cada propriedade ...

Thomas Levesque
fonte
10

Refatorar para observar a Lei de Demeter

rtalbot
fonte
Não considero um gráfico de objeto com apenas três níveis de profundidade que precise ser refatorado quando você está apenas lendo propriedades. Eu concordaria se o OP quisesse chamar um método em um objeto referenciado por PropertyC, mas não quando é uma propriedade que só precisa verificar se há nulo antes de ler. Neste exemplo, pode ser tão simples como Customer.Address.Country, em que Country pode ser um tipo de referência, como KeyValuePair. Como você refatoraria isso para evitar a necessidade de verificações de referência nula?
Darren Lewis
O exemplo de OP tem realmente 4 profundidades. Minha sugestão não é remover as verificações de referência nula, mas localizá-las nos objetos com maior probabilidade de tratá-las corretamente. Como a maioria das "regras práticas", há exceções, mas não estou convencido de que seja uma. Podemos concordar em discordar?
rtalbot de
3
Eu concordo com @rtalbot (embora, para ser justo, @Daz Lewis está propondo um exemplo de 4 profundidades, já que o último item é um KeyValuePair). Se algo está mexendo com um objeto Cliente, então não vejo que negócio isso tem, olhando através da hierarquia de objeto Endereço. Suponha que mais tarde você decida que um KeyValuePair não foi uma ótima ideia para a propriedade Country. Nesse caso, o código de todos tem que mudar. Esse não é um bom design.
Jeffrey L Whitledge
10

Atualização de 2014: C # 6 tem um novo operador ?.chamado 'navegação segura' ou 'propagação nula'

parent?.child

Leia http://blogs.msdn.com/b/jerrynixon/archive/2014/02/26/at-last-c-is-getting-sometimes-called-the-safe-navigation-operator.aspx para obter detalhes

Este tem sido um pedido extremamente popular https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3990187-add-operator-to-c-?tracking_code=594c10a522f8e9bc987ee4a5e2c0b38d

Coronel Panic
fonte
8

Obviamente, você está procurando pela Mônada Anulável :

string result = new A().PropertyB.PropertyC.Value;

torna-se

string result = from a in new A()
                from b in a.PropertyB
                from c in b.PropertyC
                select c.Value;

Isso retorna null, se alguma das propriedades anuláveis ​​for nula; caso contrário, o valor de Value.

class A { public B PropertyB { get; set; } }
class B { public C PropertyC { get; set; } }
class C { public string Value { get; set; } }

Métodos de extensão LINQ:

public static class NullableExtensions
{
    public static TResult SelectMany<TOuter, TInner, TResult>(
        this TOuter source,
        Func<TOuter, TInner> innerSelector,
        Func<TOuter, TInner, TResult> resultSelector)
        where TOuter : class
        where TInner : class
        where TResult : class
    {
        if (source == null) return null;
        TInner inner = innerSelector(source);
        if (inner == null) return null;
        return resultSelector(source, inner);
    }
}
dtb
fonte
Por que o método de extensão está aqui? Não está sendo usado.
Mladen Mihajlovic
1
@MladenMihajlovic: o SelectManymétodo de extensão é usado pela from ... in ... from ... in ...sintaxe.
dtb
5

Supondo que você tenha valores vazios de tipos, uma abordagem seria esta:

var x = (((objectA ?? A.Empty).PropertyOfB ?? B.Empty).PropertyOfC ?? C.Empty).PropertyOfString;

Sou um grande fã de C #, mas uma coisa muito boa no novo Java (1.7?) É o.? operador:

 var x = objectA.?PropertyOfB.?PropertyOfC.?PropertyOfString;
Apenas outro metaprogramador
fonte
1
Será mesmo em Java 1.7? É solicitado em C # há muito tempo, mas duvido que aconteça ...
Thomas Levesque
Infelizmente não tenho valores vazios. Essa sintaxe Java parece boa! Vou votar a favor disso, só porque quero essa sintaxe!
Jon Kragh
3
Thomas: A última vez que verifiquei tech.puredanger.com/java7, ele implicava que o Java o entenderia. Mas agora, quando eu volto a verificar, diz: Manuseio seguro nulo: NÃO. Portanto, revogo minha declaração e a substituo por uma nova: ela foi proposta para Java 1.7, mas não foi feita.
Apenas mais um metaprogrammer
Uma abordagem adicional é a usada por monad.net
apenas outro metaprogrammer
1
Parece que o?. operador está no Visual Studio 2015 https://msdn.microsoft.com/en-us/library/dn986595.aspx
Edward
4

Este código é "a menor quantidade de código", mas não a prática recomendada:

try
{
    return ObjectA.PropertyA.PropertyB.PropertyC;
}
catch(NullReferenceException)
{
     return null;
}
Boris Modylevsky
fonte
1
Já vi muito código como este e, desconsiderando a perda de desempenho, o maior problema é que complica a depuração porque a exceção real se afoga em milhões de exceções nulas ref inúteis.
Apenas mais um metaprogramador de
Às vezes é divertido ler minha própria resposta depois de 3 anos. Eu acho, eu responderia de forma diferente hoje. Eu diria que o código viola a lei de Demeter e recomendaria refatorá-lo para que não viesse.
Boris Modylevsky
1
A partir de hoje, 7 anos após a resposta original, eu me juntaria a @Phillip Ngan e usaria C # 6 com a seguinte sintaxe: int? value = objectA? .PropertyA? .PropertyB? .PropertyC;
Boris Modylevsky
4

Quando preciso encadear chamadas como essa, conto com um método auxiliar que criei, TryGet ():

    public static U TryGet<T, U>(this T obj, Func<T, U> func)
    {
        return obj.TryGet(func, default(U));
    }

    public static U TryGet<T, U>(this T obj, Func<T, U> func, U whenNull)
    {
        return obj == null ? whenNull : func(obj);
    }

No seu caso, você o usaria assim:

    int value = ObjectA
        .TryGet(p => p.PropertyA)
        .TryGet(p => p.PropertyB)
        .TryGet(p => p.PropertyC, defaultVal);
Emanuel
fonte
Não acho que esse código funcione. Qual é o tipo de defaultVal? var p = nova pessoa (); Assert.AreEqual (p.ExperimenteGet (x => x.FirstName) .ExperimenteGet (x => x.LastName) .ExperimenteGet (x => x.NickName, "foo"), "foo");
Keith
O exemplo que escrevi deve ser lido como tal: ObjectA.PropertyA.PropertyB.PropertyC. Seu código parece estar tentando carregar uma propriedade chamada "LastName" de "FirstName", que não é o uso pretendido. Um exemplo mais correto seria algo como: var postcode = person.ExperimenteGet (p => p.Address) .ExperimenteGet (p => p.Postcode); A propósito, meu método auxiliar TryGet () é muito semelhante a um novo recurso do C # 6.0 - o operador nulo-condicional. Seu uso será da seguinte maneira: var postcode = person? .Address? .Postcode; msdn.microsoft.com/en-us/magazine/dn802602.aspx
Emanuel
3

Eu vi algo no novo C # 6.0, isso é usando '?' em vez de verificar

por exemplo, em vez de usar

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)
{ 
  var city = person.contact.address.city;
}

você simplesmente usa

var city = person?.contact?.address?.city;

Espero que tenha ajudado alguém.


ATUALIZAR:

Você poderia fazer assim agora

 var city = (Person != null)? 
           ((Person.Contact!=null)? 
              ((Person.Contact.Address!= null)?
                      ((Person.Contact.Address.City!=null)? 
                                 Person.Contact.Address.City : null )
                       :null)
               :null)
            : null;
iYazee6
fonte
1

Você poderia fazer isso:

class ObjectAType
{
    public int PropertyC
    {
        get
        {
            if (PropertyA == null)
                return 0;
            if (PropertyA.PropertyB == null)
                return 0;
            return PropertyA.PropertyB.PropertyC;
        }
    }
}



if (ObjectA != null)
{
    int value = ObjectA.PropertyC;
    ...
}

Ou melhor ainda:

private static int GetPropertyC(ObjectAType objectA)
{
    if (objectA == null)
        return 0;
    if (objectA.PropertyA == null)
        return 0;
    if (objectA.PropertyA.PropertyB == null)
        return 0;
    return objectA.PropertyA.PropertyB.PropertyC;
}


int value = GetPropertyC(ObjectA);
Jeffrey L Whitledge
fonte
1

você pode usar a seguinte extensão e eu acho que é muito bom:

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<TF, TR>(TF t, Func<TF, TR> f)
    where TF : class
{
    return t != null ? f(t) : default(TR);
}

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<T1, T2, TR>(T1 p1, Func<T1, T2> p2, Func<T2, TR> p3)
    where T1 : class
    where T2 : class
{
    return Get(Get(p1, p2), p3);
}

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<T1, T2, T3, TR>(T1 p1, Func<T1, T2> p2, Func<T2, T3> p3, Func<T3, TR> p4)
    where T1 : class
    where T2 : class
    where T3 : class
{
    return Get(Get(Get(p1, p2), p3), p4);
}

E é usado assim:

int value = Nulify.Get(objectA, x=>x.PropertyA, x=>x.PropertyB, x=>x.PropertyC);
Tony
fonte
0

Eu escreveria seu próprio método no tipo PropertyA (ou um método de extensão, se não for o seu tipo) usando o padrão semelhante ao tipo Nullable.

class PropertyAType
{
   public PropertyBType PropertyB {get; set; }

   public PropertyBType GetPropertyBOrDefault()
   {
       return PropertyB != null ? PropertyB : defaultValue;
   }
}
Steve Danner
fonte
Bem, nesse caso, obviamente, PropertyB nunca pode ser nulo.
recursivo de
0

Apenas tropecei neste post.

Algum tempo atrás, fiz uma sugestão no Visual Studio Connect sobre a adição de um novo ???operador.

http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4104392-add-as-an-recursive-null-reference-check-opera

Isso exigiria algum trabalho da equipe do framework, mas não é necessário alterar a linguagem, apenas fazer a mágica do compilador. A ideia era que o compilador deveria alterar este código (sintaxe não permitida atm)

string product_name = Order.OrderDetails[0].Product.Name ??? "no product defined";

neste código

Func<string> _get_default = () => "no product defined"; 
string product_name = Order == null 
    ? _get_default.Invoke() 
    : Order.OrderDetails[0] == null 
        ? _get_default.Invoke() 
        : Order.OrderDetails[0].Product == null 
            ? _get_default.Invoke() 
            : Order.OrderDetails[0].Product.Name ?? _get_default.Invoke()

Para verificação nula, pode parecer

bool isNull = (Order.OrderDetails[0].Product ??? null) == null;
Jürgen Steinblock
fonte
0

Eu escrevi um método que aceita um valor padrão, aqui está como usá-lo:

var teacher = new Teacher();
return teacher.GetProperty(t => t.Name);
return teacher.GetProperty(t => t.Name, "Default name");

Aqui está o código:

public static class Helper
{
    /// <summary>
    /// Gets a property if the object is not null.
    /// var teacher = new Teacher();
    /// return teacher.GetProperty(t => t.Name);
    /// return teacher.GetProperty(t => t.Name, "Default name");
    /// </summary>
    public static TSecond GetProperty<TFirst, TSecond>(this TFirst item1,
        Func<TFirst, TSecond> getItem2, TSecond defaultValue = default(TSecond))
    {
        if (item1 == null)
        {
            return defaultValue;
        }

        return getItem2(item1);
    }
}
Akira Yamamoto
fonte
1
Esta solução já foi fornecida em outras respostas (repetidamente). Não há razão para postá-lo novamente .
Servy
Não vi nenhum que aceite um valor padrão.
Akira Yamamoto,
Eu conto 6 outros que utilizam um valor padrão definido. Aparentemente, você não parecia tão difícil.
Servy
0

Não é possível.
ObjectA.PropertyA.PropertyBirá falhar se ObjectAfor nulo devido à desreferenciação nula, o que é um erro.

if(ObjectA != null && ObjectA.PropertyA... funciona devido a curto-circuito, ou seja ObjectA.PropertyA, nunca será verificado se ObjectAestá null.

A primeira maneira que você propõe é a melhor e mais clara com a intenção. No mínimo, você poderia tentar redesenhar sem ter que depender de tantos nulos.

DanDan
fonte
-1

Essa abordagem é bastante direta, uma vez que você supera o lambda gobbly-gook:

public static TProperty GetPropertyOrDefault<TObject, TProperty>(this TObject model, Func<TObject, TProperty> valueFunc)  
                                                        where TObject : class
    {
        try
        {
            return valueFunc.Invoke(model);
        }
        catch (NullReferenceException nex)
        {
            return default(TProperty);
        }
    }

Com uso que pode ser semelhante a:

ObjectA objectA = null;

Assert.AreEqual(0,objectA.GetPropertyOrDefault(prop=>prop.ObjectB.ObjectB.ObjectC.ID));

Assert.IsNull(objectA.GetPropertyOrDefault(prop => prop.ObjectB));
BlackjacketMack
fonte
Só estou curioso para saber por que alguém faria downvote 8 anos depois de eu ter fornecido uma resposta (o que foi anos antes que o coalescimento nulo do C # 6 fosse uma coisa).
BlackjacketMack
-3
var result = nullableproperty ?? defaultvalue;

O ??(operador de coalescência nula) significa que se o primeiro argumento for null, retorne o segundo em vez disso.

Aridane Álamo
fonte
2
Essa resposta não resolve o problema do OP. Como você aplicaria a solução com ?? operador para ObjectA.PropertyA.PropertyB quando todas as partes da expressão (ObjectA, PropertyA e PropertyB) podem ser nulas?
Artemix de
É verdade, acho que nem li a pergunta. De qualquer forma, impossível é nada, apenas não faça: P static void Main (string [] args) {a ca = new a (); var default_value = new a () {b = new object ()}; var value = (ca ?? default_value) .b ?? default_value.b; } class a {public object b = null; }
Aridane Álamo
(ObjectA ?? DefaultMockedAtNull) .PropertyA! = Null? ObjectA.PropertyA.PropertyB: null
Aridane Álamo