Wielokrotne zabezpieczanie się przed nullem

Powiedzmy, że potrzebujemy wyciągnąć PostCode użytkownika (a po drodze mamy jeszcze Address):

string postCode = person.Address.PostCode.ToString();

Nie mamy pewności czy person nie jest nullem, a następnie czy person.Address nie jest nullem, a następnie person.Address.PostCode … . Aby się zabezpieczyć przed NullReferenceException musimy napisać trochę dodatkowego kodu, jak np:

string postCode = null;
if (person != null && person.Address != null && person.Address.PostCode != null)
{
  postCode = person.Address.PostCode.ToString();
}

W powyższym przypadku i tak mamy szczęście, że wszystko mieści się w jednym if’ie. Sytuacja wygląda dużo gorzej, gdy w te if’y wpląta się jakaś akcja do wykonania:

string postCode;
if (person != null)
{
  if (HasMedicalRecord(person) && person.Address != null)
  {
    CheckAddress(person.Address);
    if (person.Address.PostCode != null)
      postCode = person.Address.PostCode.ToString();
    else
      postCode = "UNKNOWN";
  }
}

Rozwiązaniem jest prosta extension method (widziałem masę różnych rozwiązań tego problemu, wszystkie ogólne rozwiazania nawet nie zbliżały się do tych prostych kilku linijek):

public static TResult With<TInput, TResult>
  (this TInput o, Func<TInput, TResult> evaluator)
  where TResult : class where TInput : class
{
  if (o == null) return null;
  return evaluator(o);
}

z których korzystamy w następujący sposób:

string postCode = person
                  .With(x => x.Address)
                  .With(x => x.PostCode);

(Coś takiego nazywa się Monadic null checking i wejdzie C# 6.0, ale nie każdy od razu migruje na najnowsze frameworki.

person?.Address?.PostCode

)

Tą prostą metodkę jak i kilka innych można znaleźć na Chained null checks and the Maybe monad wraz z opisem w jakich warunkach warto tak pisać kod. Autor Dmitri Nеstеruk nie przypadkowo pracuje nad R#.

Gdy komuś spodobało się to podejście to może podbić moją odpowiedź na StackOverflow: C# elegant way to check if a property’s property is null

Stworzyłem Gista z tymi wszystkim extension methods: https://gist.github.com/kmorcinek/9931393

Reklamy
Ten wpis został opublikowany w kategorii Programowanie i oznaczony tagami , . Dodaj zakładkę do bezpośredniego odnośnika.

9 odpowiedzi na „Wielokrotne zabezpieczanie się przed nullem

  1. Pingback: dotnetomaniak.pl

  2. string postCode = person.Address.PostCode.ToString();
    http://pl.wikipedia.org/wiki/Prawo_Demeter

  3. chrisseroka pisze:

    Ogólnie super, że wspomniałeś o Monadic null checking, pewnie trochę to uprości 🙂 Ale zgadzam się z Pawłem, takie łańcuszki przeczą regule Demeter.
    Można to uprościć i zgodzić się, że „person.Address.ToString()” już tak bardzo nie przeczy regule ograniczonej wiedzy i tutaj przydałoby się takie extension, ale żeby sobie z tym poradzić można stosować programowanie kontraktowe (albo po prostu konsekwentnie używać CodeContracts).

  4. Bartek Soter pisze:

    Można sprawić by użycie metody With zamiast:

    string postCode = person
    .With(x => x.Address)
    .With(x => x.PostCode);

    wyglądało tak:

    string postCode = person.With(p => p.Address.PostCode);

    A oto rozwiązanie na kolanie:

    public static TValue With<TObject, TValue>(this TObject obj, Expression<Func<TObject, TValue>> exp)
    {
        if (!(exp.Body is MemberExpression))
            throw new ArgumentException();
    
        MemberExpression currentMemberExp = null;
    
        Stack<MemberExpression> membersChain = new Stack<MemberExpression>();
        Expression currentExp = exp.Body;
        while (currentExp.NodeType == ExpressionType.MemberAccess)
        {
            currentMemberExp = (MemberExpression)currentExp;
            membersChain.Push(currentMemberExp);
            currentExp = currentMemberExp.Expression;
        }
    
        currentMemberExp = null;
        object currentObject = obj;
        while (membersChain.Count > 0)
        {
            currentMemberExp = membersChain.Pop();
            if (currentObject == null)
                return default(TValue);
    
            MemberInfo member = currentMemberExp.Member;
            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    FieldInfo field = (FieldInfo)member;
                    currentObject = field.GetValue(currentObject);
                    break;
                case MemberTypes.Property:
                    PropertyInfo property = (PropertyInfo)member;
                    currentObject = property.GetValue(currentObject);
                    break;
                default:
                    throw new NotImplementedException();
            }
        }
    
        return (TValue)currentObject;
    }
    

    Rozbudowując switcha można rozszerzyć działanie metody na przypadki zawarcia w łańcuchu wywołań takich pól jak np. konstruktory czy wywołania metod.

  5. @Bartek mimo, że na kolanie to działa 😉

    • U mnie się nie skompilowało, ale poprawienie tych drobnostek to była chwila 🙂 Koncepcja jest bardzo fajna, bo faktycznie dużo upraszcza.. można pójść dalej i obsłużyć też kolekcje, żeby można było zrobić coś takiego: p.Address[0].PostCode.

      Niestety rozwiązanie ma jedną wadę… jest prawie 1000x wolniejsze niż zwykłe if’y 🙂 To pierwsze rozwiązanie (fluent) prawie nie ma straty na wydajności. Muszę to przeanalizować profilerem, bo czuje w kościach, że da się tam coś ugrać.

    • Bartek Soter pisze:

      @Ireneusz Patalas:
      Nie sprawdzałem wydajności, ale gdyby ktoś zapytał jak się ona ma do wydajności rozwiązania „standardowego” (zwykłe ify), to pewnie bym przyznał, że musi być o kilka rzędów wielkości gorsza. Można pewnie tę wydajność nieco poprawić, ale od razu mówię, że szału nie będzie. Wystarczy sobie porównać te koncepcje:
      1) Standard: najzwyklejsze instrukcje warunkowe;
      2) Rozwiązanie by @Krzysztof Morcinek: kolejne wywołanie kilku delegatów;
      3) Rozwiązanie by @me: mozolna wędrówka po drzewie wyrażeń, wrzucanie ich na stosik, potem zbieranie ze stosu i wywoływanie. Już samo traktowanie wyrażenia lambda jako drzewa wyrażeń musi znacząco spowolnić działanie kodu w porównaniu z wywołaniem tego wyrażenia w postaci delegata 🙂

    • Wiadomo, to widać na czuja, choć szczerze mówiąc spodziewałem się trochę mniejszej różnicy… Do wołania „detalicznego” czy nawet w stosunkowo małych pętlach nie będzie to miało znaczenia, bo dla 100k powtórzeń najwolniejsze rozwiązanie trwało ok pół sekundy przy zagnieżdżeniu do 4 poziomu. Dla 1000 będzie to już tylko 5ms, więc nie ma co się spinać jeśli to nie jakiś krytyczny kod.

  6. wozni pisze:

    Akurat elementy Address i PostalCode to idealni kandydaci na ValueObject. Wtedy zamiast nulla możemy mieć statyczne instancje Address.Unknown, PostalCode.Empty i problem z głowy.
    Znalazłem nawet dyskusję na ten temat: http://stackoverflow.com/a/75769.
    Generalnie widzę tutaj próbę rozwiązania rezultatu a nie przyczyny problemu.

Możliwość komentowania jest wyłączona.