C # elegancki sposób sprawdzania, czy właściwość właściwości jest null

98

W C # powiedz, że chcesz pobrać wartość z PropertyC w tym przykładzie, a ObjectA, PropertyA i PropertyB mogą mieć wartość null.

ObjectA.PropertyA.PropertyB.PropertyC

Jak mogę bezpiecznie uzyskać PropertyC przy najmniejszej ilości kodu?

Teraz sprawdziłbym:

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

Byłoby miło zrobić coś więcej takiego (pseudokod).

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

Prawdopodobnie jeszcze bardziej upadł z operatorem koalescencji zerowej.

EDYCJA Pierwotnie powiedziałem, że mój drugi przykład był podobny do js, ​​ale zmieniłem go na psuedo-code, ponieważ zostało prawidłowo wskazane, że nie będzie działać w js.

Jon Kragh
źródło
nie widzę, jak działa Twój przykład js. powinieneś otrzymać błąd „oczekiwano obiektu” zawsze, gdy ObjectAlub PropertyAsą puste.
Lincolnk

Odpowiedzi:

120

W C # 6 możesz użyć operatora warunkowego o wartości Null . Więc oryginalny test będzie wyglądał następująco:

int? value = objectA?.PropertyA?.PropertyB?.PropertyC;
Phillip Ngan
źródło
2
czy możesz wyjaśnić, co to robi? Co jest valuerówne, jeśli PropertyCjest null? lub jeśli PropertyBjest null? co jeśli Object Ajest zerowe?
Kanion Kolob
1
Jeśli DOWOLNA z tych właściwości ma wartość null, cała instrukcja zwraca jako null. Zaczyna się od lewej do prawej. Bez cukru syntaktycznego jest to równoznaczne z serią stwierdzeń if, w których if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......ostateczne ostatnie wyrażenie toif(propertyZ != null) { value = propertyZ }
DetectivePikachu
27

Metoda krótkiego przedłużenia:

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

Za pomocą

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

Ta prosta metoda rozszerzenia i wiele więcej można znaleźć na http://devtalk.net/csharp/chained-null-checks-and-the-maybe-monad/

EDYTOWAĆ:

Po chwilowym użyciu, myślę, że właściwą nazwą tej metody powinno być IfNotNull () zamiast oryginalnego With ().

Krzysztof Morcinek
źródło
15

Czy możesz dodać metodę do swojej klasy? Jeśli nie, czy myślałeś o użyciu metod rozszerzających? Możesz utworzyć metodę rozszerzenia dla swojego typu obiektu o nazwie GetPropC().

Przykład:

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

Stosowanie:

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

Przy okazji zakłada się, że używasz platformy .NET 3 lub nowszej.

Sam
źródło
11

Sposób, w jaki to robisz, jest poprawny.

Państwo mogli korzystać trick jak opisany tutaj , używając wyrażeń Linq:

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

Jednak ręczne sprawdzanie każdej właściwości jest znacznie wolniejsze ...

Thomas Levesque
źródło
10

Refaktoryzuj, aby przestrzegać prawa Demeter

rtalbot
źródło
Nie uważam, aby wykres obiektów o głębokości zaledwie trzech poziomów wymagał refaktoryzacji, gdy czytasz tylko właściwości. Zgodziłbym się, gdyby OP chciał wywołać metodę na obiekcie, do którego odwołuje się PropertyC, ale nie wtedy, gdy jest to właściwość, która wymaga tylko sprawdzenia wartości null przed odczytaniem. W tym przykładzie może to być tak proste, jak Customer.Address.Country, gdzie Country może być typem referencyjnym, takim jak KeyValuePair. Jak można to zmienić, aby zapobiec potrzebie zerowych sprawdzeń referencyjnych?
Darren Lewis,
Przykład OP jest tak naprawdę 4 głęboki. Moja sugestia nie polega na usuwaniu zerowych sprawdzeń referencyjnych, ale na umieszczaniu ich w obiektach, które najprawdopodobniej będą w stanie je prawidłowo obsłużyć. Jak większość praktycznych reguł, są wyjątki, ale nie jestem przekonany, że to jeden z nich. Czy możemy się nie zgodzić?
rtalbot
4
Zgadzam się z @rtalbot (choć uczciwie @Daz Lewis proponuje 4-głęboki przykład, ponieważ ostatnia pozycja to KeyValuePair). Jeśli coś miesza z obiektem klienta, nie widzę, jaki ma biznes, przeglądając hierarchię obiektów Adres. Załóżmy, że później zdecydujesz, że KeyValuePair nie jest tak świetnym pomysłem dla właściwości Country. W takim przypadku kod każdego musi się zmienić. To nie jest dobry projekt.
Jeffrey L Whitledge,
10

Aktualizacja 2014: C # 6 ma nowego operatora o ?.nazwie „bezpieczna nawigacja” lub „propagacja zerowa”

parent?.child

Przeczytaj http://blogs.msdn.com/b/jerrynixon/archive/2014/02/26/at-last-c-is-getting-sometimes-called-the-safe-navigation-operator.aspx, aby uzyskać szczegółowe informacje

To od dawna bardzo popularne żądanie https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3990187-add-operator-to-c-?tracking_code=594c10a522f8e9bc987ee4a5e2c0b38d

Colonel Panic
źródło
8

Oczywiście szukasz monady Nullable :

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

staje się

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

Zwraca to null, jeśli którakolwiek z właściwości dopuszczających wartość null ma wartość null; w przeciwnym razie wartość Value.

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

Metody rozszerzenia 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
źródło
Dlaczego jest tu metoda rozszerzenia? Nie jest używany.
Mladen Mihajlovic
1
@MladenMihajlovic: SelectManymetoda rozszerzenia jest używana przez from ... in ... from ... in ...składnię.
dtb
5

Zakładając, że masz puste wartości typów, jedno podejście byłoby takie:

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

Jestem wielkim fanem C #, ale bardzo fajną rzeczą w nowej Javie (1.7?) Jest.? operator:

 var x = objectA.?PropertyOfB.?PropertyOfC.?PropertyOfString;
Po prostu kolejny metaprogramator
źródło
1
Czy to naprawdę będzie w Javie 1.7? To zwrócono w C # przez długi czas, ale wątpię, czy kiedykolwiek zdarzyć ...
Thomas Levesque
Niestety nie mam pustych wartości. Jednak ta składnia Java wygląda słodko! Mam zamiar głosować za tym, tylko dlatego, że chcę mieć taką składnię!
Jon Kragh
3
Thomas: Ostatnim razem, gdy sprawdzałem tech.puredanger.com/java7 , sugerowało się, że Java go dostanie. Ale teraz, kiedy ponownie sprawdzam, pojawia się komunikat: Brak bezpiecznej obsługi: NIE. Dlatego wycofuję swoje oświadczenie i zastępuję je nowym: zaproponowano je dla Javy 1.7, ale się nie udało.
Kolejny metaprogramator,
Dodatkowym podejściem jest to stosowane przez monad.net
Just another metaprogrammer
1
Wydaje się, że? operator znajduje się w programie Visual Studio 2015 https://msdn.microsoft.com/en-us/library/dn986595.aspx
Edward
4

Ten kod jest „najmniejszą ilością kodu”, ale nie jest najlepszym rozwiązaniem:

try
{
    return ObjectA.PropertyA.PropertyB.PropertyC;
}
catch(NullReferenceException)
{
     return null;
}
Boris Modylevsky
źródło
1
Widziałem wiele takich kodów i pomijając utratę wydajności, największym problemem jest to, że komplikuje debugowanie, ponieważ prawdziwy wyjątek tonie w milionach bezużytecznych wyjątków zerowych ref.
Kolejny metaprogramator z
Czasami zabawnie jest przeczytać własną odpowiedź po 3 latach. Myślę, że dzisiaj odpowiedziałbym inaczej. Powiedziałbym, że kod narusza prawo Demeter i poleciłbym jego refaktoryzację, aby nie było.
Boris Modylevsky
1
Od dzisiaj, 7 lat po pierwotnej odpowiedzi, dołączyłbym do @Phillip Ngan i użyłbym C # 6 z następującą składnią: int? wartość = obiektA? .PropertyA? .PropertyB? .PropertyC;
Boris Modylevsky
4

Kiedy muszę łączyć wywołania w ten sposób, polegam na utworzonej przeze mnie metodzie pomocniczej 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);
    }

W twoim przypadku użyłbyś go tak:

    int value = ObjectA
        .TryGet(p => p.PropertyA)
        .TryGet(p => p.PropertyB)
        .TryGet(p => p.PropertyC, defaultVal);
Emanuel
źródło
Myślę, że ten kod nie działa. Jaki jest typ defaultVal? var p = new Person (); Assert.AreEqual (p.TryGet (x => x.FirstName) .TryGet (x => x.LastName) .TryGet (x => x.NickName, "foo"), "foo");
Keith,
Przykład, który napisałem, należy czytać jako taki: ObjectA.PropertyA.PropertyB.PropertyC. Wygląda na to, że Twój kod próbuje załadować właściwość o nazwie „LastName” z „FirstName”, co nie jest zamierzone. Bardziej poprawnym przykładem byłoby coś takiego: var postcode = person.TryGet (p => p.Address) .TryGet (p => p.Postcode); Nawiasem mówiąc, moja metoda pomocnicza TryGet () jest bardzo podobna do nowej funkcji w C # 6,0 - operatora warunkowego zerowego. Jego użycie będzie wyglądało następująco: var postcode = person? .Address? .Postcode; msdn.microsoft.com/en-us/magazine/dn802602.aspx
Emanuel
3

Widziałem coś w nowym C # 6.0, to jest za pomocą '?' zamiast sprawdzać

na przykład zamiast używać

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

po prostu używasz

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

Mam nadzieję, że komuś pomogło.


AKTUALIZACJA:

Możesz to teraz zrobić

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

Możesz to zrobić:

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

A może nawet lepiej:

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

możesz użyć następującego rozszerzenia i myślę, że jest naprawdę dobre:

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

I jest używany w ten sposób:

int value = Nulify.Get(objectA, x=>x.PropertyA, x=>x.PropertyB, x=>x.PropertyC);
Tony
źródło
0

Napisałbym własną metodę w typie PropertyA (lub metodę rozszerzenia, jeśli nie jest to twój typ), używając podobnego wzorca do typu Nullable.

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

   public PropertyBType GetPropertyBOrDefault()
   {
       return PropertyB != null ? PropertyB : defaultValue;
   }
}
Steve Danner
źródło
Cóż, w takim przypadku oczywiście PropertyB nigdy nie może być zerowe.
rekurencyjny
0

Właśnie natknąłem się na ten post.

Jakiś czas temu w Visual Studio Connect zasugerowałem dodanie nowego ???operatora.

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

Wymagałoby to trochę pracy zespołu ds. Frameworka, ale nie trzeba zmieniać języka, ale po prostu zrobić trochę magii kompilatora. Pomysł był taki, że kompilator powinien zmienić ten kod (składnia nie jest dozwolona w atm)

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

do tego kodu

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

Dla sprawdzenia zerowego może to wyglądać

bool isNull = (Order.OrderDetails[0].Product ??? null) == null;
Jürgen Steinblock
źródło
0

Napisałem metodę, która akceptuje wartość domyślną, oto jak jej używać:

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

Oto kod:

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
źródło
1
To rozwiązanie zostało już podane w innych odpowiedziach (wielokrotnie). Nie ma żadnego powodu, aby publikować go ponownie .
Servy
Nie widziałem niczego, co akceptują wartość domyślną.
Akira Yamamoto
Liczę 6 innych, które wykorzystują zdefiniowaną wartość domyślną. Najwyraźniej nie wyglądałeś aż tak mocno.
Servy
0

To niemożliwe.
ObjectA.PropertyA.PropertyBzakończy się niepowodzeniem, jeśli ObjectAma wartość null z powodu wyłuskiwania odwołań o wartości zerowej, co jest błędem.

if(ObjectA != null && ObjectA.PropertyA... działa z powodu zwarcia, tzn. ObjectA.PropertyAnigdy nie będzie sprawdzane, czy ObjectAjest null.

Pierwsza propozycja jest najlepsza i najbardziej przejrzysta z zamiarem. Jeśli cokolwiek, możesz spróbować przeprojektować bez konieczności polegania na tak wielu zerach.

DanDan
źródło
-1

To podejście jest dość proste, gdy przejdziesz przez 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);
        }
    }

Przy użyciu, które może wyglądać następująco:

ObjectA objectA = null;

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

Assert.IsNull(objectA.GetPropertyOrDefault(prop => prop.ObjectB));
BlackjacketMack
źródło
Ciekawe, dlaczego ktoś miałby głosować negatywnie 8 lat po tym, jak dostarczyłem odpowiedź (czyli lata przed zerowym połączeniem C # 6).
BlackjacketMack
-3
var result = nullableproperty ?? defaultvalue;

W ??(null koalescencji operatorów) oznacza, że jeśli pierwszy argument jest null, należy zwrócić drugi zamian.

Aridane Álamo
źródło
2
Ta odpowiedź nie rozwiązuje problemu PO. Jak zastosowałbyś rozwiązanie z ?? operator na ObjectA.PropertyA.PropertyB, gdy wszystkie części wyrażenia (ObjectA, PropertyA i PropertyB) mogą mieć wartość null?
Artemix
To prawda, myślę, że w ogóle nie przeczytałem tego pytania. W każdym razie niemożliwe jest niczym, po prostu tego nie rób: 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; } klasa a {obiekt publiczny b = null; }
Aridane Álamo
(ObjectA ?? DefaultMockedAtNull) .PropertyA! = Null? ObjectA.PropertyA.PropertyB: null
Aridane Álamo