Modo elegante in C # per verificare se la proprietà di una proprietà è nulla


97

In C #, si supponga di voler estrarre un valore da PropertyC in questo esempio e ObjectA, PropertyA e PropertyB possono essere tutti null.

ObjectA.PropertyA.PropertyB.PropertyC

Come posso ottenere PropertyC in modo sicuro con la minor quantità di codice?

In questo momento vorrei controllare:

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

Sarebbe bello fare qualcosa di più simile a questo (pseudo-codice).

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

Forse anche ulteriormente collassato con un operatore di coalescenza nullo.

EDIT Inizialmente ho detto che il mio secondo esempio era come js, ma l'ho cambiato in codice psuedo poiché è stato correttamente sottolineato che non avrebbe funzionato in js.


non vedo come funziona il tuo esempio js. dovresti ricevere un errore "oggetto atteso" ogni volta che ObjectAo PropertyAsono nulli.
Lincolnk

Risposte:


117

In C # 6 è possibile utilizzare l' operatore condizionale null . Quindi il test originale sarà:

int? value = objectA?.PropertyA?.PropertyB?.PropertyC;

2
potresti spiegare cosa fa? Cosa è valueuguale a se PropertyCè nullo? o se PropertyBè nullo? cosa succede se Object Aè nullo?
Kolob Canyon

1
Se QUALSIASI di queste proprietà è null, l'intera istruzione restituisce come null. Inizia da sinistra a destra. Senza zucchero sintattico ciò equivale ad una serie di istruzioni if in cui if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......con l'eventuale ultima espressione dell'essereif(propertyZ != null) { value = propertyZ }
DetectivePikachu

27

Metodo di estensione breve:

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

Utilizzando

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

Questo semplice metodo di estensione e molto altro puoi trovare su http://devtalk.net/csharp/chained-null-checks-and-the-maybe-monad/

MODIFICARE:

Dopo averlo usato per un momento, penso che il nome corretto per questo metodo dovrebbe essere IfNotNull () invece dell'originale With ().


15

Puoi aggiungere un metodo alla tua classe? In caso contrario, hai pensato di utilizzare metodi di estensione? È possibile creare un metodo di estensione per il tipo di oggetto chiamato GetPropC().

Esempio:

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

Utilizzo:

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

A proposito, questo presuppone che tu stia utilizzando .NET 3 o versioni successive.


11

Il modo in cui lo stai facendo è corretto.

Si potrebbe usare un trucco come quello descritto qui , utilizzando espressioni Linq:

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

Ma è molto più lento che controllare manualmente ogni proprietà ...


10

Rifattore per osservare la Legge di Demetra


Non considero un grafico a oggetti profondo solo tre livelli per necessitare di refactoring quando stai leggendo solo proprietà. Sarei d'accordo se l'OP volesse chiamare un metodo su un oggetto referenziato tramite PropertyC ma non quando si tratta di una proprietà che necessita solo di verificare la presenza di null prima della lettura. In questo esempio potrebbe essere semplice come Customer.Address.Country dove Country potrebbe essere un tipo di riferimento come KeyValuePair. Come lo rifattorereste per evitare la necessità di controlli di riferimento null?
Darren Lewis

L'esempio OP è in realtà di 4 profondità. Il mio suggerimento non è di rimuovere i controlli null ref ma di individuarli negli oggetti che più probabilmente saranno in grado di gestirli correttamente. Come la maggior parte delle "regole pratiche" ci sono delle eccezioni, ma non sono convinto che questa sia una. Possiamo accettare di non essere d'accordo?
rtalbot

3
Sono d'accordo con @rtalbot (sebbene, in tutta onestà, @Daz Lewis sta proponendo un esempio di 4 profondità, poiché l'ultimo elemento è un KeyValuePair). Se qualcosa sta incasinando un oggetto Cliente, allora non vedo quale attività ha guardando attraverso la gerarchia dell'oggetto Indirizzo. Supponiamo che in seguito decidiate che KeyValuePair non era un'idea così eccezionale per la proprietà Country. In tal caso, il codice di tutti deve cambiare. Non è un buon design.
Jeffrey L Whitledge,

10

Aggiornamento 2014: C # 6 ha un nuovo operatore ?.chiamato "navigazione sicura" o "propagazione null"

parent?.child

Leggi http://blogs.msdn.com/b/jerrynixon/archive/2014/02/26/at-last-c-is-getting-sometimes-called-the-safe-navigation-operator.aspx per i dettagli

Questa è stata a lungo una richiesta estremamente popolare https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3990187-add-operator-to-c-?tracking_code=594c10a522f8e9bc987ee4a5e2c0b38d


8

Stai ovviamente cercando la Monade nullable :

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

diventa

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

Restituisce null, se una qualsiasi delle proprietà nullable è null; in caso contrario, il valore di Value.

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

Metodi di estensione 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);
    }
}

Perché il metodo di estensione è qui? Non viene utilizzato.
Mladen Mihajlovic

1
@MladenMihajlovic: il SelectManymetodo di estensione è utilizzato dalla from ... in ... from ... in ...sintassi.
dtb

5

Supponendo che tu abbia valori vuoti di tipi, un approccio sarebbe questo:

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

Sono un grande fan di C # ma una cosa molto carina nel nuovo Java (1.7?) È il.? operatore:

 var x = objectA.?PropertyOfB.?PropertyOfC.?PropertyOfString;

1
Sarà davvero in Java 1.7? È stato richiesto in C # da molto tempo, ma dubito che accadrà mai ...
Thomas Levesque

Purtroppo non ho valori vuoti. Quella sintassi Java però sembra dolce! Lo voterò a favore, solo perché voglio quella sintassi!
Jon Kragh

3
Thomas: L'ultima volta che ho controllato tech.puredanger.com/java7 implicava che Java l'avrebbe capito. Ma ora, quando ricontrollo, dice: Null safe handling: NO. Quindi revoco la mia dichiarazione e la sostituisco con una nuova: era stata proposta per Java 1.7 ma non ce l'aveva fatta.
Solo un altro metaprogrammatore il

Un approccio aggiuntivo è quello utilizzato da monad.net
Solo un altro metaprogrammatore il

1
Sembra che il?. operator è in Visual Studio 2015 https://msdn.microsoft.com/en-us/library/dn986595.aspx
Edward

4

Questo codice è "la quantità minima di codice", ma non la best practice:

try
{
    return ObjectA.PropertyA.PropertyB.PropertyC;
}
catch(NullReferenceException)
{
     return null;
}

1
Ho visto un codice come questo molto e ignorando la perdita di prestazioni il problema più grande è che complica il debug perché la vera eccezione annega in milioni di eccezioni null ref inutili.
Solo un altro metaprogrammatore il

A volte è divertente leggere la mia risposta dopo 3 anni. Penso, risponderei diversamente oggi. Direi che il codice viola la legge di Demetra e consiglierei di refactoring in modo che non lo faccia.
Boris Modylevsky

1
Ad oggi, 7 anni dopo la risposta originale, mi unirei a @Phillip Ngan e userei C # 6 con la seguente sintassi: int? valore = oggettoA? .PropertyA? .PropertyB? .PropertyC;
Boris Modylevsky

4

Quando ho bisogno di concatenare chiamate in questo modo, mi affido a un metodo di supporto che ho creato, 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);
    }

Nel tuo caso, lo useresti così:

    int value = ObjectA
        .TryGet(p => p.PropertyA)
        .TryGet(p => p.PropertyB)
        .TryGet(p => p.PropertyC, defaultVal);

Non credo che questo codice funzioni. Qual è il tipo di defaultVal? var p = new Person (); Assert.AreEqual (p.TryGet (x => x.FirstName) .TryGet (x => x.LastName) .TryGet (x => x.NickName, "foo"), "foo");
Keith

L'esempio che ho scritto dovrebbe essere letto come tale: ObjectA.PropertyA.PropertyB.PropertyC. Sembra che il tuo codice stia tentando di caricare una proprietà chiamata "LastName" da "FirstName", che non è l'utilizzo previsto. Un esempio più corretto potrebbe essere qualcosa del tipo: var postcode = person.TryGet (p => p.Address) .TryGet (p => p.Postcode); A proposito, il mio metodo helper TryGet () è molto simile a una nuova funzionalità in C # 6.0: l'operatore condizionale null. Il suo utilizzo sarà così: var postcode = person? .Address? .Postcode; msdn.microsoft.com/en-us/magazine/dn802602.aspx
Emanuel

3

Ho visto qualcosa nel nuovo C # 6.0, utilizzando "?" invece di controllare

per esempio invece di usare

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

usi semplicemente

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

Spero abbia aiutato qualcuno.


AGGIORNARE:

Potresti fare così adesso

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

1

Potresti farlo:

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

O ancora meglio potrebbe essere questo:

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

1

puoi usare la seguente estensione e penso che sia davvero buono:

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

Ed è usato in questo modo:

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

0

Vorrei scrivere il tuo metodo nel tipo di PropertyA (o un metodo di estensione se non è il tuo tipo) usando il modello simile al tipo Nullable.

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

   public PropertyBType GetPropertyBOrDefault()
   {
       return PropertyB != null ? PropertyB : defaultValue;
   }
}

Bene, in quel caso, ovviamente PropertyB non può mai essere nullo.
ricorsivo

0

Sono appena inciampato in questo post.

Qualche tempo fa ho suggerito a Visual Studio Connect di aggiungere un nuovo ???operatore.

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

Ciò richiederebbe un po 'di lavoro da parte del team del framework, ma non è necessario modificare il linguaggio, ma solo fare un po' di magia del compilatore. L'idea era che il compilatore dovesse modificare questo codice (sintassi non consentita atm)

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

in questo codice

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

Per il controllo nullo questo potrebbe apparire come

bool isNull = (Order.OrderDetails[0].Product ??? null) == null;

0

Ho scritto un metodo che accetta un valore predefinito, ecco come usarlo:

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

Ecco il codice:

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

1
Questa soluzione è già stata fornita in altre risposte (più volte). Non c'è alcun motivo per pubblicarlo di nuovo .
Servizio

Non ho visto nessuno che accetti un valore predefinito.
Akira Yamamoto

Ne conto altri 6 che utilizzano un valore predefinito definito. A quanto pare non hai guardato così bene.
Servizio

0

Non è possibile.
ObjectA.PropertyA.PropertyBfallirà se ObjectAè nullo a causa della dereferenziazione nullo, che è un errore.

if(ObjectA != null && ObjectA.PropertyA... funziona a causa di cortocircuito, ovvero ObjectA.PropertyAnon verrà mai verificato se lo ObjectAè null.

Il primo modo in cui proponi è il migliore e il più chiaro con l'intento. Semmai potresti provare a riprogettare senza dover fare affidamento su così tanti null.


-1

Questo approccio è abbastanza semplice una volta superato il 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);
        }
    }

Con un utilizzo che potrebbe assomigliare a:

ObjectA objectA = null;

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

Assert.IsNull(objectA.GetPropertyOrDefault(prop => prop.ObjectB));

Sono solo curioso del motivo per cui qualcuno avrebbe downvote 8 anni dopo che ho fornito una risposta (che era anni prima che la fusione nullo di C # 6 fosse una cosa).
BlackjacketMack

-3
var result = nullableproperty ?? defaultvalue;

L ' ??(operatore di coalescenza null) significa che se il primo argomento è null, restituisci invece il secondo.


2
Questa risposta non risolve il problema dell'OP. Come applicheresti la soluzione con ?? operatore a ObjectA.PropertyA.PropertyB quando tutte le parti dell'espressione (ObjectA, PropertyA e PropertyB) possono essere nulle?
Artemix

È vero, penso di non aver nemmeno letto la domanda. Comunque, impossibile non è niente, semplicemente non farlo: 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; } classe a {oggetto pubblico b = null; }
Aridane Álamo

(ObjectA ?? DefaultMockedAtNull) .PropertyA! = Null? ObjectA.PropertyA.PropertyB: null
Aridane Álamo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.