Spis treści:

Kategoria:C#


Operator warunkowy wartości null ?.

Operator warunkowy wartości null ?.

Operator bezpiecznej nawigacji

Programowanie obiektowe ma swoje zalety. Na tyle wyraziste, że na dobre zadomowiło się w małych, średnich i dużych projektach. Każdy byt, każdą strukturę, reprezentuje się w postaci obiektów. Te różne obiekty mają swoje klasy. I tak, pracownik reprezentowany jest przez obiekty klasy Pracownik, samochody przez obiekty klasy Samochod. Nawet te bardziej abstrakcyjne pojęcia mają swoje odpowiedniki w świecie obiektowym: punkt reprezentowany jest przez klasę Punkt, położenie przez klasę Polozenie. Klasa definiuje stan i zachowanie obiektu poprzez składowe. Klasa punkt może wobec tego definiować współrzędne x i y, klasa Pracownik może definiować imię, nazwisko, wiek, płeć i, co ważniejsze z punktu widzenia dzisiejszego zagadnienia, wartość, która również jest obiektem. Pracownik może mieć przypisana firmę, która jest obiektem, firma może mieć nazwę i adres. Adres również może być obiektem zawierającym miasto, ulicę i numer. Takie podejście obiektowe pozwala pogrupować zmienne w odpowiednie konteksty. W obiektowych językach programowania musi być jakiś sposób odwołania się do konkretnej wartości z konkretnego kontekstu. W większości jest to kropka. Jeżeli mamy pracownika pracownik1, pracującego w firmie zapamiętanej w polu Firma, firma ma adres zapamiętany w polu Adres, a adres zawiera ulicę w polu Ulica, wtedy moglibyśmy tę ulicę pobrać tak:

string ulica = pracownik1.Firma.Adres.Ulica;

Zapis jest bardzo czytelny. Nie zmienia to jednak faktu, że w tej niepozornej linijce może się wiele wydarzyć. Wiemy przecież, że obiekty nie muszą być uzupełnione i nie są to jednostkowe przypadki. A jeżeli pracownik w formularzu zgłoszeniowym nie podał firmy, jeżeli firma ze względu na ochronę danych nie pozwala przetwarzać danych, jeżeli ktoś nie uzupełnił adresu bo mu się nie chciało, i w końcu, jeżeli firma jest na wsi i tam nie ma ulic? jeżeli w chociaż jednym miejscu będzie wartość pusta, null, dostaniemy znany wszystkim wyjątek.

NullReferenceException

Nie trzeba dużego doświadczenia programistycznego w C# i innych językach platformy .NET aby doskonale poznać tego niezwykle przebiegłego przedstawiciela gatunku wyjątków. Ten wyjątek pojawia się głownie wtedy, gdy programista popełnił błąd nie zabezpieczając kodu odpowiednimi instrukcjami warunkowymi. Każde odwołanie się do składowej obiektu pustego (null) powoduje wyjątek NullReferenceException. Gdyby zatem nasz system dopuszczał możliwość pustych obiektów na każdym z poziomów zagnieżdżenia, musielibyśmy zapisać powyższą instrukcję następująco:

string ulica = pracownik1.Firma.Adres.Ulica;
if (pracownik1 != null)
{
    if (pracownik1.Firma != null)
    {
        if (pracownik1.Firma.Adres != null)
        {
            ulica = pracownik1.Firma.Adres.Ulica;
        }
    }
}

Kod staje się zdecydowanie mniej przejrzysty i czytelny. Można wprawdzie skorzystać z pewnej właściwości kompilatorów, które przerywają sprawdzanie kolejnych warunków koniunkcji po napotkaniu pierwszego fałszywego zdania, ale dalej kod nie będzie tak ładny jak na początku:

string ulica = pracownik1.Firma.Adres.Ulica;
if (pracownik1 != null &&
    pracownik1.Firma != null &&
    pracownik1.Firma.Adres != null)
{
        ulica = pracownik1.Firma.Adres.Ulica;
}

Wielu zapewne pamięta dziwną składnię wyzwalania zdarzeń:

if (MyEvent != null)
    MyEvent(...);

Te i inne problemy sprawiły, że społeczność programistów .NET zjednoczyła się, wołając jednym głosem do twórców języka o jakieś rozwiązanie.

Operator warunkowy wartości null w C# 6.0

Operator warunkowy wartości null był jedną z najwyżej ocenianych sugestii w obszarze Visual Studio (Ja najbardziej czekam chyba na możliwość wykonywania wyrażeń lambda w oknach debuggera). Być może przyspieszyło to pojawienie się rozwiązania. To, co w poprzednim podpunkcie, można teraz wykonać w następujący sposób:

string ulica = pracownik1?.Firma?.Adres?.Ulica;

Jeżeli porównamy zapis z operatorem ?. i poprzednie rozwiązania, ten wyda nam się wyjątkowo czytelny. Operator działa następująco: jeżeli obiekt, do którego pól odwołujemy się operatorem ?. jest pusty, zwracana jest wartość null. Dalsza część wyrażenia nie jest sprawdzana.

Podobnie upraszcza się pokazany wcześniej przykład wywoływania zdarzeń. Można teraz napisać tak:

MyEvent?.Invoke(...);

Operator warunkowy wartości null występuje pod postacią symboli ?. (pytajnik kropka) dla metod, właściwości i pól, ale może być też stosowany jako warunkowy operator indeksowania. Wtedy składnia przybiera postać ?[...]. Zostało to pokazane na poniższym listingu:

var item = person.Cars?[0]?.Brand;

Operator warunkowy występuje tutaj dwa razy: pierwszy sprawdza, czy tablica nie jest pusta, drugi, czy element spod indeksu 0 nie jest pustyOperator warunkowy wartości null nie zapewni obsługi nieprawidłowego indeksu. Jeżeli zatem tablica nie jest pusta a indeks będzie wykraczał poza zakres (np. person.Cars?[4] dla dwuelementowej tablicy), dostaniemy wyjątek System.IndexOutOfRangeException..

Operator przydaje się też w zakresie LINQ oraz w przypadku obsługi zewnętrznych usług, które mogą zwracać null. Popatrzmy na poniższy przykład:

var customersList = proxy.GetCustomers();
var supervisors = customersList?.Where(a=>a.IsSupervisor);

Jeżeli usługa zwróci null, w polu supervisors również będzie null. Wyjątek nie zostanie wyrzucony.

Wartości domyślne dla null

To, że w którymś z pól jest null to nie koniec świata. W wielu przypadkach jest to pożądane. Ktoś może nie mieć drugiego imienia, jakiś parametr konfiguracji może nie być ustawiony. W relacyjnych bazach danych NULL jest tak powszechny, że pewnie każdy się już do niego przyzwyczaił. Z tym, że w bazach są inne reguły i nie grasuje tam NullReferenceException. Dalsze przetwarzanie wartości, które mogą zawierać null może czasami wymagać odpowiednich modyfikacji. Jeżeli zatem chcielibyśmy pobrać nazwisko panieńskie matki użytkownika i zechcieli je jakoś przetwarzać, wygodniej byłoby czasami zastąpić null pustym ciągiem:

var maidenName = person.Mother?.MaidenName ?? string.Empty;

Zdarza się, że program posiada złożoną konfigurację. Mało rozsądne jest zmuszanie użytkownika do wpisywania wszystkich wartości konfiguracyjnych. A praktyka pokazuje, że nawet średnio złożony system pisany dla kilku klientów potrafi zawierać kilkadziesiąt parametrów. Żeby nie wpływać na istniejących użytkowników, wprowadza się parametr. Popatrzmy na poniższy listing:

var service = (ConfigurationManager.GetSection("IntegrationService") as Service)?.Timeout ?? 30;

Przykład nieco bardziej złożony, ale koncepcyjnie prosty. Jeżeli sekcja konfiguracyjna usług integracyjnych jest zdefiniowana, wtedy limit czasu połączenia jest umieszczony w polu Timeout. Jeżeli nie, przyjmujemy domyślny limit 30 sekund. Zapis bez operatora warunkowego wartości null byłby z pewnością bardziej złożony.

Operator warunkowy wartości null wcześniej

Opisywany operator jest tak przyjemny w użyciu, że czasami trudno będzie wrócić do rozwiązań stosowanych wcześniej. A były to wielokrotne zagnieżdżenia zwykłych instrukcji warunkowych, szeregowe porównania z wartością null i inne, równie złożone techniki. To tak jak z LINQ i wyrażeniami Lambda. Powrót do starych technik, to jest do ręcznie pisanych pętli i tworzenia delegacji (ang. delegate) nie jest przyjemny. Wszystko wydaje się skomplikowane. Operatorem warunkowy wartości null należy do tej grupy zmian .NET, które są mało inwazyjne i nie wpływają negatywnie na przejrzystość kodu (niedawno pisałem również o nowym operatorze nameof, wpis znajduje się tutaj: Słowo kluczowe nameof w C# 6.0). Ta przepaść składniowa pomiędzy C# 6.0 a wcześniejszymi wersjami sprawiła, że postanowiłem napisać program z gatunku eksperymentalnych. Nie ma on na celu zastąpić składni języka, ale pokazać, że przy odrobinie kombinacji da się bardzo mocno uprościć zapis. Metoda korzysta z techniki przetwarzania drzew wyrażeń, a całą implementację mozna znaleźć tutaj: NullConditionalOperator.zip. Można wprawdzie tak otoczyć metodę, aby wyłapywała wyjątek NullReferenceException i wyciszała go zwracając null, ale zaprezentowane rozwiązanie wydaje się bardziej edukacyjne. Poniżej pokazuję kilka przykładów wykorzystania rozwiązania, którego dokładniejszy opis pojawi się wkrótce:

var obj = new A()
{
    B = new B()
    {
        Value1 = null,
        Value2 = 7
    }
};

var nullableNullA = obj.Get(a => a.B.Value1);
var nullableNullB = obj?.B?.Value1;
var nullableA = obj.Get(a => a.B.Value2);
var nullableB = obj?.B?.Value2;
var nullClassA = obj.Get(a => a.B.Value1);
var nullClassB = obj?.B?.Value1;
var emptyTableA = obj.Get(a => a.emptyTable[1].Length);
var emptyTableB = obj?.emptyTable?[1]?.Length;
var tableA = obj.Get(a => a.table[1].Length);
var tableB = obj?.table?[1]?.Length;

Rozszerzenie Get to właśnie wspomniana metoda. Poniżej każdej z linijek z Get znajduje się odpowiednik z C# 6.0. Rozwiązanie jest wprawdzie mało optymalne, ale w wielu przypadkach czytelność jest ważniejsza niż oszczędność 1 milisekundy. Szczegóły wkrótce.

Kategoria:C#

, 2015-06-17

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?
Ciekawy artykuł.
Czy można za pomocą EF wysłać swoje zapytanie?
Czy lepiej do tego użyć ADO.net i DataTable?