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ł?
Odpowiedzi:
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ć
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?[]
.źródło
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 :-))
źródło
DynamicInvoke
tam. Religijnie tegoOdkrył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.
źródło
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”.
źródło
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…catch
bloku, aby sprawdzić, czyNullReferenceException
wystą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:
NullReferenceException
s prawdopodobnie nigdy nie powinny być wyraźnie przechwytywane. (Zobacz to pytanie .)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.loader
do 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ć,
null
jeś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ś.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.źródło
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:
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; }
źródło
Jedną z opcji jest użycie Null Object Patten, więc zamiast null, gdy nie masz ciasta, masz NullCake, które zwraca NullFosting itp. Przepraszam, nie jestem zbyt dobry w wyjaśnianiu tego, ale inni ludzie są.
źródło
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
Coal
metodą 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!
źródło
Istnieje projekt Maybe Codeplex, który implementuje może lub IfNotNull przy użyciu lambd dla głębokich wyrażeń w języku C #
Przykład użycia:
int? CityId= employee.Maybe(e=>e.Person.Address.City);
Link został zasugerowany w podobnym pytaniu Jak sprawdzić wartości zerowe w głębokim wyrażeniu lambda?
źródło
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? .
źródło
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));
źródło
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; }
źródło
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ł .
źródło
Expression
jeśli zamierzasz tylko skompilować i złapać? Po prostu użyjFunc<T>
.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);
źródło
Tam, gdzie chcesz to osiągnąć, zrób to:
Stosowanie
lub
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); } } }
źródło
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...; }
źródło