Paweł Łukasiewicz
2016-12-05
Paweł Łukasiewicz
2016-12-05
Udostępnij Udostępnij Kontakt
Wprowadzenie

Jeżeli czytałeś posty lub artykuły na MSDN poświecone nowym funkcjonalnością języka C# 6.0 napewno natrafiłeś na informację, że operator warunkowy null może Ci pomóc znacznie zmniejszyć liczbę trudnych do zdebugowania oraz zreprodukowania błędów typu: NullReferenceException.

Po pierwszym przeczytaniu wspomnianego wyżej posta udało mi się jedynie zarejestrować, że operator ten pomaga w łańcuchowym sprawdzaniu wartości null zwłaszcza w zagnieżdzonych strukturach danych oraz, że zwraca wartość null jak tylko znajdzie takie wystąpienie w całym łańcuchu danych. Myślałem, że jest to jedynie uproszczenie składniowe.

Nie zrozumiałem jednak prawdziwej mocy tego operatora. Zwykle preferuje drobiazgowe sprawdzenia, które przy użyciu C# 5.0 mogłby być przedstawione w poniższy sposób:

private static int GetCurrentCarSpped(Car car)
{
	if(car!=null && car.Engine!=null && car.Engine.ControlUnit!=null)
	{
		return car.Engine.ControlUnit.CurrentCarSpeed;
	}
	return 0;
}

W celu łatwiejszej wizualizacji struktury danych poniżej przygotowane klasy:

class Car
{
	public Engine Engine { get; set; }
}
class Engine
{
	public ControlUnit ControlUnit { get; set; }
}
class ControlUnit
{
	public int CurrentCarSpeed { get; set; }
}

Pojawa się pytanie. Czy takie sprawdzenie jest koniecznie, czy musimy przechodzić przez każdy element z osobna? Czy możemy zastosować tutaj operator warunkowy null? Spójrzmy na poniższą konstrukcję:

private static int GetCurrentCarSpeed(Car car)
{
	return car?.Engine?.ControlUnit?.CurrentCarSpeed ?? 0;
}

Pozbyliśmy się 5 linijek kodu, było warto?

Było warto ponieważ taka konstrukcja jest bezpieczna dla wątku – kompilator generuje kodu do oceny właściwości tylko jeden raz przechowując ten wynik w zmiennej tymczasowej.

Mam nadzieje, że po takiej definicji wszystko stanie się jasne. Żeby jednak prześledzić cały ten proces dogłębienie skupimy się na analizie kodu IL (Intermediate Language). Cały kod przykładu znajduje się w artykule dlatego możemy przystąpić do utworzenia dwóch aplikacji konsolowych w celu porówniania rezultatu. Następnie użyjemy narzędzia JetBrains dotPeek do zbadania kodu IL, który został utworzony przez każdy z kompilatorów.

IL wygenerowany przez C# 5.0

Jeżeli spojrzysz na kod języka C# 5.0, a dokładnie na linię numer 13, zobaczy kod, który uzyskuje dostep do właściwości car.Engine - łączy się to z wywołaniem get_Engine() (59 linia w kodzie IL). Podobnie, w tej samej linii kodu mamy dostep do właściwości car.Engine.ControlUnit , która łączy się z wywołaniami get_Engine() oraz get_ControlUnit() - linie 62 oraz 63.

Dostęp do właściwości car.Engine.ControlUnit.CurrentSpeed w 15 linii wiąże się z wywołaniem get_Engine(), get_ControlUnit() oraz get_CurrentSpeed() w liniach od 79 do 81.

Wynik działania kompilatora:
IL wygenerowany dla C# 5.0

W momencie pisania artykułu zgłosiłem problem dotyczący numeru linii dla IL Viewer -> Request(#820221) dlatego zdecydowałem się również podkreślić wspomniane wyżej linie kodu.

Problemem tak napisanego kodu jest fakt, że inny wątek może przypisać wartość null do poprzedniego sprawdzenia. Gdy kontrola powróci do głównego wątku a my będziemy chcieli uzyskać dostęp do tej właściwości – zostanie zwrócony trudny do odtworzenia wyjątek NullReferenceException.

IL wygenerowany przez C# 6.0

Jeżeli teraz spojrzymy w kod C# 6.0 na linie numer 13, możemy zobaczyć, że operator warunkowy null ma dostęp od razu do wszystkich trzech właściwości przez trzy powiązane wywołania get_Engine(), get_ControlUnit() oraz get_CurrentSpeed() - linie 64, 72 oraz 80 w podglądzie IL produkując bardziej wydajny kod.

Wynik działania kompilatora:
IL wygenerowany przez C# 6.0

Podsumowanie

Dostaliśmy w końcu wyśmienitą funkcjonalność, która pozwala nam na zabezpieczenie się przed wyjątkiem NullReferenceException. Warto zatem pomyśleć o jak najszybszej przesiadce na C# 6.0.