Głębokie sprawdzanie zerowe, czy jest lepszy sposób?

132

Uwaga: to pytanie został poproszony przed wprowadzeniem do .?operatora w C # 6 / Visual Studio 2015 .

Wszyscy tam byliśmy, mamy pewne głębokie właściwości, takie jak cake.frosting.berries.loader, które musimy sprawdzić, czy jest zerowe, więc nie ma wyjątku. Aby to zrobić, użyj skróconej instrukcji if

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

Nie jest to zbyt eleganckie i być może powinien istnieć łatwiejszy sposób sprawdzenia całego łańcucha i zobaczenia, czy napotyka on zmienną / właściwość o wartości null.

Czy jest możliwe użycie jakiejś metody rozszerzenia, czy byłaby to funkcja języka, czy to po prostu zły pomysł?

Homde
źródło
3
Często tego pragnąłem - ale wszystkie pomysły, które wpadłem, były gorsze niż rzeczywisty problem.
peterchen
Dziękuję za wszystkie odpowiedzi i ciekawe, że inni ludzie mają te same myśli. Zastanawiałem się, jak sam chciałbym to rozwiązać i chociaż rozwiązania Erica są fajne, myślę, że po prostu napisałbym coś takiego, jeśli (IsNull (abc)), lub jeśli (IsNotNull (abc)), ale może to jest po prostu w moim guście :)
Homde
Kiedy tworzysz lukrowanie, ma on właściwość jagód, więc w tym momencie w konstruktorze możesz po prostu powiedzieć lukierowi, że za każdym razem, gdy zostanie utworzony pusty (nie zerowy), jagody? a ilekroć jagody modyfikuje się lukier czy sprawdzamy wartość ????
Doug Chamberlain
Nieco luźno powiązane, niektóre techniki tutaj uważałem za preferowane w przypadku problemu „głębokich zer”, który próbowałem obejść. stackoverflow.com/questions/818642/…
AaronLS,

Odpowiedzi:

224

Rozważaliśmy dodanie nowej operacji „?”. do języka, który ma żądaną semantykę. (I został dodany teraz; patrz poniżej.) To znaczy, można powiedzieć

cake?.frosting?.berries?.loader

a kompilator wygeneruje dla ciebie wszystkie testy zwarciowe.

To nie sprawiło, że poprzeczka dla C # 4. Być może dla hipotetycznej przyszłej wersji języka.

Update (2014):?. operator jest teraz planowane dla następnej wersji kompilatora Roslyn. Zwróć uwagę, że wciąż toczy się dyskusja na temat dokładnej analizy składniowej i semantycznej operatora.

Aktualizacja (lipiec 2015 r.): Visual Studio 2015 został wydany i jest dostarczany z kompilatorem C #, który obsługuje operatory warunkowe zerowe ?.i?[] .

Eric Lippert
źródło
10
Bez kropki staje się niejednoznaczny składniowo z operatorem warunkowym (A? B: C). Staramy się unikać konstrukcji leksykalnych, które wymagają od nas „patrzenia w przyszłość” arbitralnie daleko w strumieniu tokenów. (Chociaż, niestety, są już takie konstrukcje w C #; wolelibyśmy więcej nie dodawać.)
Eric Lippert
33
@Ian: ten problem jest niezwykle powszechny. To jedna z najczęściej otrzymywanych próśb.
Eric Lippert
7
@Ian: Wolę również używać wzorca obiektu zerowego, jeśli jest to możliwe, ale większość ludzi nie ma luksusu pracy z modelami obiektów, które sami zaprojektowali. Wiele istniejących modeli obiektów używa wartości null, więc to jest świat, z którym musimy żyć.
Eric Lippert
12
@John: Prośbę o tę funkcję otrzymujemy prawie w całości od naszych najbardziej doświadczonych programistów. MVP proszą o to cały czas . Ale rozumiem, że opinie są różne; jeśli oprócz krytyki chciałbyś podać konstruktywną sugestię dotyczącą projektu języka, z przyjemnością ją rozważę.
Eric Lippert
28
@lazyberezovsky: Nigdy nie rozumiałem tak zwanego „prawa” Demeter; przede wszystkim wydaje się, że trafniej nazywa się ją „Sugestią Demeter”. Po drugie, rezultatem przyjęcia „dostępu tylko dla jednego członka” do jego logicznej konkluzji jest „Bóg przedmioty”, w których każdy obiekt jest zobowiązany do zrobienia wszystkiego dla każdego klienta, a nie do rozdawania obiektów, które wiedzą, jak zrobić to, co klient chce. Wolę dokładne przeciwieństwo prawa demeter: każdy obiekt dobrze rozwiązuje niewielką liczbę problemów, a jednym z tych rozwiązań może być „oto inny przedmiot, który lepiej rozwiązuje twój problem”
Eric Lippert
27

Zainspirowało mnie to pytanie, aby spróbować dowiedzieć się, jak tego rodzaju głębokie sprawdzanie wartości null można wykonać za pomocą łatwiejszej / ładniejszej składni przy użyciu drzew wyrażeń. Chociaż zgadzam się z odpowiedziami stwierdzającymi, że może to być zły projekt, jeśli często potrzebujesz dostępu do instancji głęboko w hierarchii, myślę również, że w niektórych przypadkach, takich jak prezentacja danych, może to być bardzo przydatne.

Stworzyłem więc metodę rozszerzającą, która pozwoli Ci napisać:

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

To zwróci jagody, jeśli żadna część wyrażenia nie jest pusta. Jeśli napotkano wartość null, zwracana jest wartość null. Istnieją jednak pewne zastrzeżenia, w obecnej wersji będzie działać tylko z prostym dostępem do elementów członkowskich i działa tylko w .NET Framework 4, ponieważ używa metody MemberExpression.Update, która jest nowa w wersji 4. Oto kod metody rozszerzenia 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;            
        }
    }
}

Działa poprzez badanie drzewa wyrażeń reprezentujących twoje wyrażenie i ocenianie części jedna po drugiej; za każdym razem sprawdzając, czy wynik nie jest zerowy.

Jestem pewien, że można to rozszerzyć, aby obsługiwane były inne wyrażenia niż MemberExpression. Potraktuj to jako kod weryfikujący koncepcję i pamiętaj, że użycie go spowoduje spadek wydajności (co prawdopodobnie nie będzie miało znaczenia w wielu przypadkach, ale nie używaj go w ciasnej pętli :-))

driis
źródło
Jestem pod wrażeniem twoich umiejętności lambda :) składnia wydaje się jednak być odrobinę bardziej złożona niż by się chciało, przynajmniej w scenariuszu z instrukcją „
if”
Fajnie, ale działa jak 100 razy więcej kodu niż if .. &&. Opłaca się tylko wtedy, gdy nadal kompiluje się do wartości if .. &&.
Monstieur
1
Ach i wtedy zobaczyłem DynamicInvoketam. Religijnie tego
unikam
24

Odkryłem, że to rozszerzenie jest bardzo przydatne w scenariuszach głębokiego zagnieżdżania.

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

To pomysł, który zacząłem od null coalescing operator w C # i T-SQL. Fajną rzeczą jest to, że typ zwracany jest zawsze typem zwracanym właściwości wewnętrznej.

W ten sposób możesz to zrobić:

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

... lub niewielka zmiana powyższego:

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

Nie jest to najlepsza składnia, jaką znam, ale działa.

John Leidegren
źródło
Dlaczego „Coal”, to wygląda wyjątkowo przerażająco. ;) Jednak próbka nie powiodłaby się, gdyby lukier był zerowy. Powinien tak wyglądać: var jagody = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke
Och, ale sugerujesz, że drugi argument nie jest wezwaniem do Coala, którym oczywiście musi być. To tylko wygodna zmiana. Selektor (x => x.berries) jest przekazywany do wywołania Coal wewnątrz metody Coal, która przyjmuje dwa argumenty.
John Leidegren
Nazwa coalescing lub coalesce pochodzi z T-SQL, stąd pierwszy pomysł. IfNotNull sugeruje, że coś ma miejsce, jeśli nie jest null, jednak to, co to jest, nie jest wyjaśnione przez wywołanie metody IfNotNull. Węgiel to rzeczywiście dziwna nazwa, ale w rzeczywistości jest to dziwna metoda, na którą warto zwrócić uwagę.
John Leidegren,
Najlepszą dosłowną nazwą byłoby coś w rodzaju „ReturnIfNotNull” lub „ReturnOrDefault”
John Leidegren,
@flq +1 ... w naszym projekcie nazywa się też IfNotNull :)
Marc Sigrist
16

Wydaje mi się, że oprócz naruszenia prawa Demeter, jak już zauważył Mehrdad Afshari, dla logiki decyzyjnej potrzebne jest „głębokie sprawdzanie zerowe”.

Dzieje się tak najczęściej, gdy chcesz zastąpić puste obiekty wartościami domyślnymi. W takim przypadku należy rozważyć implementację wzorca obiektu zerowego . Działa jako zastępstwo dla rzeczywistego obiektu, dostarczając wartości domyślne i metody „niedziałające”.

Johannes Rudolph
źródło
nie, cel-c umożliwia wysyłanie wiadomości do obiektów zerowych i zwraca odpowiednią wartość domyślną, jeśli to konieczne. Żadnych problemów.
Johannes Rudolph
2
Tak. O to chodzi. Zasadniczo będziesz emulować zachowanie ObjC z Null Object Pattern.
Mehrdad Afshari
10

Aktualizacja: Począwszy od programu Visual Studio 2015, kompilator C # (wersja językowa 6) rozpoznaje teraz ?.operatora, dzięki czemu „głębokie sprawdzanie wartości null” jest dziecinnie proste. Zobacz tę odpowiedź, aby uzyskać szczegółowe informacje.

Oprócz przeprojektowania kodu, jak sugerowała ta usunięta odpowiedź , inną (choć okropną) opcją byłoby użycie try…catchbloku, aby sprawdzić, czy NullReferenceExceptionwystąpi jakiś czas podczas tego głębokiego wyszukiwania właściwości.

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

Osobiście nie zrobiłbym tego z następujących powodów:

  • Nie wygląda to ładnie.
  • Używa obsługi wyjątków, która powinna być ukierunkowana na wyjątkowe sytuacje, a nie coś, czego można się spodziewać często podczas normalnego przebiegu operacji.
  • NullReferenceExceptions prawdopodobnie nigdy nie powinny być wyraźnie przechwytywane. (Zobacz to pytanie .)

Czy jest więc możliwe użycie jakiejś metody rozszerzenia, czy też byłaby to funkcja języka, [...]

Prawie na pewno musiałaby to być funkcja języka (która jest dostępna w C # 6 w postaci .? i?[] operatorów ), chyba że C # ma już bardziej wyrafinowaną leniwą ocenę lub jeśli nie chcesz użyć odbicia (co prawdopodobnie również nie jest dobry pomysł ze względu na wydajność i bezpieczeństwo typu).

Ponieważ nie ma sposobu, aby po prostu przekazać cake.frosting.berries.loaderdo funkcji (zostanie ona oceniona i zgłosi zerowy wyjątek odniesienia), musiałbyś zaimplementować ogólną metodę wyszukiwania w następujący sposób: Pobiera obiekty i nazwy właściwości do sprawdzać:

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

(Uwaga: edytowano kod).

Szybko widzisz kilka problemów z takim podejściem. Po pierwsze, nie uzyskujesz żadnego bezpieczeństwa typu i możliwego pakowania wartości właściwości prostego typu. Po drugie, możesz albo powrócić, nulljeśli coś pójdzie nie tak i będziesz musiał sprawdzić to w swojej funkcji wywołującej, albo zgłosisz wyjątek i wrócisz do miejsca, w którym zacząłeś. Po trzecie, może być powolne. Po czwarte, wygląda brzydiej niż to, od czego zacząłeś.

[...], czy to po prostu zły pomysł?

Zostałbym z:

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

lub skorzystaj z powyższej odpowiedzi Mehrdada Afshariego.


PS: Kiedy pisałem tę odpowiedź, oczywiście nie brałem pod uwagę drzew wyrażeń dla funkcji lambda; zobacz np. odpowiedź @driis, aby znaleźć rozwiązanie w tym kierunku. Opiera się również na pewnego rodzaju refleksji i dlatego może nie działać tak dobrze, jak prostsze rozwiązanie ( if (… != null & … != null) …), ale może być lepiej ocenione z punktu widzenia składni.

stakx - już nie wnoszący wkładu
źródło
2
Nie wiem, dlaczego to zostało odrzucone, zagłosowałem za równowagą: odpowiedź jest poprawna i wnosi nowy aspekt (i wyraźnie wspomina o wadach tego rozwiązania ...)
MartinStettner
gdzie jest „powyższa odpowiedź Mehrdada Afshariego”?
Marson Mao
1
@MarsonMao: Ta odpowiedź została w międzyczasie usunięta. (Nadal możesz go przeczytać, jeśli twoja pozycja SO jest wystarczająco wysoka.) Dziękuję za wskazanie mojego błędu: powinienem odnosić się do innych odpowiedzi za pomocą hiperłącza, nie używając słów typu „patrz wyżej” / „patrz poniżej” (ponieważ odpowiedzi nie pojawiają się w ustalonej kolejności). Zaktualizowałem odpowiedź.
stakx - nie publikuje już
5

Chociaż odpowiedź driis jest interesująca, myślę, że jest to trochę zbyt kosztowne, jeśli chodzi o wydajność. Zamiast kompilować wiele delegatów, wolałbym skompilować jedną lambdę na ścieżkę właściwości, buforować ją, a następnie ponownie wywołać wiele typów.

NullCoalesce poniżej właśnie to robi, zwraca nowe wyrażenie lambda z kontrolami null i zwracaniem wartości domyślnej (TResult) w przypadku, gdy jakakolwiek ścieżka ma wartość null.

Przykład:

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

Zwróci wyrażenie

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

Kod:

    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;
    }
Zagiąć
źródło
3

Ja też często marzyłem o prostszej składni! Jest to szczególnie brzydkie, gdy masz wartości zwracane przez metodę, które mogą być zerowe, ponieważ wtedy potrzebujesz dodatkowych zmiennych (na przykład cake.frosting.flavors.FirstOrDefault().loader:)

Jednak tutaj jest całkiem przyzwoita alternatywa, której używam: utwórz metodę pomocniczą Null-Safe-Chain. Zdaję sobie sprawę, że jest to bardzo podobne do odpowiedzi @ Johna powyżej (z Coalmetodą rozszerzenia), ale uważam, że jest to prostsze i mniej wpisywane. Oto jak to wygląda:

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

Oto implementacja:

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

Utworzyłem również kilka przeciążeń (z 2 do 6 parametrami), a także przeciążenia, które pozwalają na zakończenie łańcucha typem wartości lub wartością domyślną. U mnie to działa naprawdę dobrze!

Scott Rippey
źródło
1

Jak sugeruje John Leidegren jest odpowiedź , jedno podejście do pracy wokół tego jest użycie metody rozszerzenie i delegatów. Korzystanie z nich może wyglądać mniej więcej tak:

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

Implementacja jest niechlujna, ponieważ musisz sprawić, aby działała dla typów wartości, typów odwołań i typów wartości dopuszczających wartość null. Można znaleźć pełną realizację w Timwi „s odpowiedzi do Co to jest właściwy sposób, aby sprawdzić wartości null? .

Sam
źródło
1

Lub możesz użyć refleksji :)

Funkcja odbicia:

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

Stosowanie:

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

My Case (zwraca DBNull.Value zamiast null w funkcji odbicia):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));
heybeliman
źródło
1

Wypróbuj ten kod:

    /// <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
źródło
0

Opublikowałem to wczoraj wieczorem, a potem znajomy wskazał mi to pytanie. Mam nadzieję, że to pomoże. Możesz wtedy zrobić coś takiego:

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

Przeczytaj cały wpis na blogu tutaj .

Ten sam przyjaciel również zasugerował, żebyś to obejrzał .

Tyler Jensen
źródło
3
Po co zawracać sobie głowę, Expressionjeśli zamierzasz tylko skompilować i złapać? Po prostu użyj Func<T>.
Scott Rippey
0

Lekko zmodyfikowałem kod stąd, aby działał na zadane pytanie:

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

I tak, to chyba nie jest optymalne rozwiązanie ze względu na implikacje wydajności try / catch, ale działa:>

Stosowanie:

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);
kaptan
źródło
0

Tam, gdzie chcesz to osiągnąć, zrób to:

Stosowanie

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

lub

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

Implementacja klasy pomocniczej

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
źródło
-3

Podoba mi się podejście Objective-C:

„Język Objective-C ma inne podejście do tego problemu i nie wywołuje metod na nil, ale zamiast tego zwraca nil dla wszystkich takich wywołań”.

if (cake.frosting.berries != null) 
{
    var str = cake.frosting.berries...;
}
Shyam vemuri
źródło
1
to, co robi inny język (i twoja opinia na jego temat), jest prawie całkowicie bez znaczenia, aby działał w C #. To nie pomaga nikomu w rozwiązaniu ich problemu z C #
ADyson