Controllo null profondo, esiste un modo migliore?


130

Nota: Questa domanda è stato chiesto prima dell'introduzione del .?operatore in C # 6 / Visual Studio 2015 .

Siamo stati tutti lì, abbiamo alcune proprietà profonde come cake.frosting.berries.loader che dobbiamo controllare se è nullo, quindi non c'è eccezione. Il modo di fare è usare un'istruzione if in corto circuito

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

Questo non è esattamente elegante, e forse dovrebbe esserci un modo più semplice per controllare l'intera catena e vedere se si trova su una variabile / proprietà nulla.

È possibile utilizzare un metodo di estensione o sarebbe una funzionalità linguistica o è solo una cattiva idea?


3
L'ho desiderato abbastanza spesso, ma tutte le idee che ho avuto erano peggiori del problema reale.
Peter

Grazie per tutte le risposte e interessante vedere che altre persone hanno avuto gli stessi pensieri. Ho avuto modo di pensare a come mi piacerebbe vederlo risolto da solo e sebbene le soluzioni di Eric siano carine, penso che avrei semplicemente scritto qualcosa del genere se (IsNull (abc)), o if (IsNotNull (abc)) ma forse è solo per i miei gusti :)
Homde,

Quando installi la glassa, ha una proprietà di bacche, quindi a quel punto nel tuo costruttore, puoi semplicemente dire glassare che ogni volta che viene istigato a creare bacche vuote (non nulle)? e ogni volta che le bacche vengono modificate la glassa fa il controllo del valore ????
Doug Chamberlain,

In qualche modo vagamente correlato, alcune delle tecniche qui che ho trovato preferibili per il problema dei "nullità profonde" che stavo cercando di aggirare. stackoverflow.com/questions/818642/…
AaronLS il

Risposte:


223

Abbiamo considerato l'aggiunta di una nuova operazione "?." nella lingua che ha la semantica desiderata. (Ed è stato aggiunto ora; vedi sotto.) Cioè, diresti

cake?.frosting?.berries?.loader

e il compilatore genererebbe per te tutti i controlli di corto circuito.

Non ha fatto la barra per C # 4. Forse per un'ipotetica versione futura della lingua.

Aggiornamento (2014): l' ?.operatore è ora pianificato per la prossima versione del compilatore Roslyn. Si noti che c'è ancora qualche dibattito sull'esatta analisi sintattica e semantica dell'operatore.

Aggiornamento (luglio 2015): Visual Studio 2015 è stato rilasciato e distribuito con un compilatore C # che supporta gli operatori null-condizionali ?.e?[] .


10
Senza il punto diventa sintatticamente ambiguo con l'operatore condizionale (A? B: C). Cerchiamo di evitare costrutti lessicali che ci richiedono di "guardare avanti" arbitrariamente lontano nel flusso di token. (Anche se, sfortunatamente, ci sono già tali costrutti in C #; preferiremmo non aggiungerne altro.)
Eric Lippert,

33
@Ian: questo problema è estremamente comune. Questa è una delle richieste più frequenti che riceviamo.
Eric Lippert,

7
@Ian: Preferisco anche usare il modello di oggetti null quando possibile, ma la maggior parte delle persone non ha il lusso di lavorare con modelli di oggetti che loro stessi hanno progettato. Molti modelli di oggetti esistenti usano valori null e quindi è il mondo con cui dobbiamo convivere.
Eric Lippert,

12
@John: riceviamo questa richiesta di funzionalità quasi interamente dai nostri programmatori più esperti. Gli MVP lo chiedono sempre . Ma capisco che le opinioni variano; se vuoi dare un suggerimento costruttivo sulla progettazione del linguaggio oltre alle tue critiche, sono felice di considerarlo.
Eric Lippert,

28
@lazyberezovsky: non ho mai capito la cosiddetta "legge" di Demetra; prima di tutto, sembra essere chiamato con più precisione "Il suggerimento di Demetra". E in secondo luogo, il risultato dell'accesso "a un solo membro" alla sua logica conclusione è "oggetti di Dio" in cui ogni oggetto è tenuto a fare tutto per ogni cliente, piuttosto che essere in grado di distribuire oggetti che sanno come fare ciò che il cliente vuole. Preferisco l'esatto contrario della legge di demeter: ogni oggetto risolve bene un piccolo numero di problemi, e una di quelle soluzioni può essere "ecco un altro oggetto che risolve meglio il tuo problema"
Eric Lippert,

27

Mi sono ispirato a questa domanda per provare a scoprire come questo tipo di controllo null profondo può essere fatto con una sintassi più semplice / più carina usando gli alberi delle espressioni. Anche se concordo con le risposte affermando che potrebbe essere una cattiva progettazione se spesso è necessario accedere alle istanze in profondità nella gerarchia, penso anche che in alcuni casi, come la presentazione dei dati, possa essere molto utile.

Quindi ho creato un metodo di estensione, che ti permetterà di scrivere:

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

Questo restituirà le bacche se nessuna parte dell'espressione è nulla. Se viene rilevato null, viene restituito null. Ci sono alcuni avvertimenti, nella versione corrente funzionerà solo con un semplice accesso ai membri e funziona solo su .NET Framework 4, perché usa il metodo MemberExpression.Update, che è nuovo in v4. Questo è il codice per il metodo di estensione 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;            
        }
    }
}

Funziona esaminando l'albero delle espressioni che rappresenta la tua espressione e valutando le parti una dopo l'altra; ogni volta verificando che il risultato non sia nullo.

Sono sicuro che questo potrebbe essere esteso in modo da supportare espressioni diverse da MemberExpression. Considera questo come un codice di prova di concetto e tieni presente che ci sarà una penalità per le prestazioni usandolo (che probabilmente non importerà in molti casi, ma non usarlo in un ciclo stretto :-))


Sono impressionato dalle tue abilità lambda :) la sintassi sembra tuttavia essere un po 'più complessa di quanto si vorrebbe, almeno per lo scenario if-statement
Homde,

Fantastico, ma funziona come 100 volte più codice di un if .. &&. Vale la pena solo se si compila ancora in un if .. &&.
Monstieur,

1
Ah e poi ho visto DynamicInvokelì. Lo evito religiosamente :)
nawfal l'

24

Ho trovato questa estensione molto utile per gli scenari di nidificazione profonda.

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

È un'idea che ho tratto dall'operatore null coalescente in C # e T-SQL. La cosa bella è che il tipo restituito è sempre il tipo restituito della proprietà interna.

In questo modo puoi farlo:

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

... o una leggera variazione di quanto sopra:

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

Non è la migliore sintassi che conosca, ma funziona.


Perché "Carbone", che sembra estremamente inquietante. ;) Tuttavia, il campione fallirebbe se la glassa fosse nulla. Avrei dovuto apparire così: var bacche = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke,

Oh, ma stai insinuando che il secondo argomento non è una chiamata al carbone, che ovviamente deve essere. È solo una comoda modifica. Il selettore (x => x.berries) viene passato a una chiamata Coal all'interno del metodo Coal che accetta due argomenti.
John Leidegren,

Il nome coalescing o coalesce è stato preso da T-SQL, è lì che ho avuto l'idea per la prima volta. IfNotNull implica che qualcosa ha luogo se non nullo, tuttavia ciò che è, non è spiegato dalla chiamata del metodo IfNotNull. Il carbone è davvero un nome strano, ma questo è in realtà un metodo strano che vale la pena notare.
John Leidegren,

Il miglior nome letterale per questo sarebbe qualcosa come "ReturnIfNotNull" o "ReturnOrDefault"
John Leidegren,

@flq +1 ... nel nostro progetto si chiama anche IfNotNull :)
Marc Sigrist,

16

Oltre a violare la Legge di Demetra, come ha già sottolineato Mehrdad Afshari, mi sembra che sia necessario un "controllo nullo profondo" per la logica decisionale.

Questo è spesso il caso in cui si desidera sostituire oggetti vuoti con valori predefiniti. In questo caso dovresti prendere in considerazione l'implementazione del Null Object Pattern . Funge da supporto per un oggetto reale, fornendo valori predefiniti e metodi "non-action".


no, goal-c consente l'invio di messaggi a oggetti null e restituisce il valore predefinito appropriato, se necessario. Nessun problema lì.
Johannes Rudolph,

2
Si. Questo è il punto. Fondamentalmente, emulerai il comportamento di ObjC con Null Object Pattern.
Mehrdad Afshari,

10

Aggiornamento: a partire da Visual Studio 2015, il compilatore C # (lingua versione 6) ora riconosce l' ?.operatore, il che rende un "controllo null profondo" un gioco da ragazzi. Vedi questa risposta per i dettagli.

Oltre a riprogettare il tuo codice, come suggerito da questa risposta cancellata , un'altra (anche se terribile) opzione sarebbe quella di utilizzare un try…catchblocco per vedere se si NullReferenceExceptionverifica a volte durante quella ricerca approfondita della proprietà.

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

Personalmente non lo farei per i seguenti motivi:

  • Non sembra carino.
  • Utilizza la gestione delle eccezioni, che dovrebbe riguardare situazioni eccezionali e non qualcosa che ti aspetti che accada spesso durante il normale corso dell'operazione.
  • NullReferenceExceptionprobabilmente non dovrebbero mai essere catturati in modo esplicito. (Vedi questa domanda .)

Quindi è possibile usare un metodo di estensione o sarebbe una funzione linguistica, [...]

Questo dovrebbe quasi certamente essere una caratteristica del linguaggio (che è disponibile in C # 6 sotto forma di .?e ?[]operatori), a meno che C # non abbia già avuto una valutazione pigra più sofisticata, o a meno che tu non voglia usare la riflessione (che probabilmente non è nemmeno una buona idea per motivi di prestazioni e sicurezza del tipo).

Poiché non è possibile passare semplicemente cake.frosting.berries.loadera una funzione (verrebbe valutata e generata un'eccezione di riferimento null), è necessario implementare un metodo di ricerca generale nel modo seguente: accetta un oggetto e i nomi delle proprietà per consultare:

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: codice modificato.)

Si vedono rapidamente diversi problemi con un simile approccio. Innanzitutto, non si ottiene alcuna sicurezza di tipo e possibili inscatolamento di valori di proprietà di un tipo semplice. In secondo luogo, puoi tornare nullse qualcosa va storto e dovrai verificarlo nella tua funzione di chiamata, oppure lanciare un'eccezione e tornare al punto di partenza. Terzo, potrebbe essere lento. In quarto luogo, sembra più brutto di quello con cui hai iniziato.

[...] o è solo una cattiva idea?

O starei con:

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

o vai con la risposta sopra di Mehrdad Afshari.


PS: Quando ho scritto questa risposta, ovviamente non ho considerato gli alberi delle espressioni per le funzioni lambda; vedi ad esempio la risposta di @driis per una soluzione in questa direzione. È anche basato su un tipo di riflessione e quindi potrebbe non funzionare altrettanto bene come una soluzione più semplice ( if (… != null & … != null) …), ma può essere giudicato più bello dal punto di vista della sintassi.


2
Non so perché questo sia stato sottoposto a downgrade, ho fatto un voto positivo: la risposta è corretta e introduce un nuovo aspetto (e menziona esplicitamente gli svantaggi di questa soluzione ...)
MartinStettner

dov'è "la risposta sopra di Mehrdad Afshari"?
Marson Mao,

1
@MarsonMao: quella risposta è stata cancellata nel frattempo. (Sei ancora in grado di leggerlo se il tuo grado SO è sufficientemente alto.) Grazie per aver sottolineato il mio errore: dovrei fare riferimento ad altre risposte usando un collegamento ipertestuale, non usando parole come "vedi sopra" / "vedi sotto" (poiché le risposte non compaiono in un ordine fisso). Ho aggiornato la mia risposta.
stakx - non contribuisce più al

5

Mentre la risposta di driis è interessante, penso che sia un po 'troppo costoso dal punto di vista delle prestazioni. Piuttosto che compilare molti delegati, preferirei compilare un lambda per percorso di proprietà, memorizzarlo nella cache e quindi ripristinarlo in molti tipi.

NullCoalesce di seguito fa proprio questo, restituisce una nuova espressione lambda con controlli null e un ritorno di default (TResult) nel caso in cui qualsiasi percorso sia null.

Esempio:

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

Restituirà un'espressione

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

Codice:

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


3

Anch'io ho spesso desiderato una sintassi più semplice! Diventa particolarmente brutto quando hai metodi-return-valori che potrebbero essere nulli, perché allora hai bisogno di variabili extra (ad esempio cake.frosting.flavors.FirstOrDefault().loader:)

Tuttavia, ecco un'alternativa abbastanza decente che uso: creare un metodo di supporto Null-Safe-Chain. Mi rendo conto che questo è abbastanza simile alla risposta di @ John sopra (con il Coalmetodo di estensione) ma trovo che sia più semplice e meno digitante. Ecco come appare:

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

Ecco l'implementazione:

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

Ho anche creato diversi sovraccarichi (con parametri da 2 a 6), nonché sovraccarichi che consentono alla catena di terminare con un tipo di valore o un valore predefinito. Questo funziona davvero bene per me!



1

Come suggerito in John Leidegren 's risposta , un approccio al lavoro intorno a questo è quello di utilizzare metodi di estensione e delegati. Usarli potrebbe assomigliare a questo:

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

L'implementazione è disordinata perché è necessario che funzioni per tipi di valore, tipi di riferimento e tipi di valore nullable. È possibile trovare una completa implementazione in Timwi 's risposta alla Qual è il modo corretto di verificare la presenza di valori nulli? .


1

Oppure puoi usare la riflessione :)

Funzione di riflessione:

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

Il mio caso (restituisce DBNull.Value anziché null nella funzione di riflessione):

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

1

Prova questo codice:

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

0

L'ho pubblicato ieri sera e poi un amico mi ha indicato questa domanda. Spero che sia d'aiuto. È quindi possibile fare qualcosa del genere:

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

Leggi il post completo del blog qui .

Lo stesso amico ha anche suggerito di guardarlo .


3
Perché preoccuparsi di un Expressionse hai intenzione di compilare e catturare? Basta usare a Func<T>.
Scott Rippey,

0

Ho leggermente modificato il codice da qui per farlo funzionare per la domanda posta:

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 sì, questa probabilmente non è la soluzione ottimale a causa di tentativi di rilevazione delle prestazioni ma funziona:>

Uso:

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

0

Dove è necessario raggiungere questo obiettivo, procedere come segue:

uso

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

o

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

Implementazione della classe di supporto

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

-3

Mi piace l'approccio adottato da Objective-C:

"Il linguaggio Objective-C adotta un altro approccio a questo problema e non invoca metodi su zero ma restituisce invece zero per tutte queste invocazioni."

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

1
ciò che fa un'altra lingua (e la tua opinione su di essa) è quasi del tutto irrilevante per farlo funzionare in C #. Non aiuta nessuno a risolvere il problema C #
ADyson
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.