Post dotyczy języka C#, nie wypowiadam się na temat innych języków gdyż nie wiem jak operator i instrukcja warunkowa są w nich zaimplementowane.
Bardzo często spotykam się ze stwierdzeniem, że operator warunkowy (?:) jest równoważny instrukcji warunkowej (if).
Info. Nie wiem czemu statement to instrukcja, a expression to wyrażenie. Jakoś mi nie pasuje to tłumaczenie ale będę się jego tutaj trzymał.
Jak ktoś tłumaczy ?: to często daje prosty przykład IF i ?: pisząc, że to to samo. Prowadzi do przykładów podobnych do tego niżej wymienionego. Pokazującego, że ?: == IF, jednak nie wspominając absolutnie o tym, że różnica pomiędzy nimi jest znacząca i to, że coś działa dla jednego przykładu nie oznacza, że zadziała dla innego. Także pomijana jest całkowicie informacja o walidacji, jaką kompilator wykonuje w tych dwóch przypadkach, a różnica między walidacjami jest ona znacząca. Jednak po kolei. Najpierw popatrzmy na przykład kiedy zachowanie ?: I IF jest takie same, jednak nie do końca takie same. A potem przejdziemy do przypadku kiedy zachowanie dla pseudo podobnych ?: i IF jest całkowicie inne w rzeczywistości.
using System; namespace ConditionalAndIf { class Program { static void Main(string[] args) { var one = GetDummyByConditionalOperator(new DummyType()); var two = GetDummyByIf(null); Console.WriteLine("By ?: :t{0}", one); Console.WriteLine("By IF :t{0}", two); Console.ReadLine(); } public static DummyType GetDummyByConditionalOperator(DummyType dummyType) { return dummyType == null ? new DummyType() : dummyType; } public static DummyType GetDummyByIf(DummyType dummyType) { if(dummyType == null) { return new DummyType(); } else { return dummyType; } } } public class DummyType { public override string ToString() { return "Test"; } } }
Zachowanie w tym wypadku zarówno operatora warunkowego jak i instrukcji warunkowej będzie takie same. Różnica będzie widoczna w kodzie IL. Można zaobserwować iż IF wykorzystuje dodatkową zmienną bool w której przechowywany jest wynik operacji porównania. Dokładnie mówiąc nasze porównanie zamienione jest na pseudo kod:
bool = (dummyType eq null) eq 0
Eq zwraca 0 dla false, 1 dla true. Teraz jeżeli powyższe wyrażenie zostanie ewaluowane do wartości true, to zostanie zwrócony nam obiekt istniejący, w przeciwnym wypadku zostanie utworzony nowy obiekt.
Kod IL wygląda tak:
.method public hidebysig static class ConditionalAndIf.DummyType GetDummyByIf(class ConditionalAndIf.DummyType dummyType) cil managed { // Code size 28 (0x1c) .maxstack 2 .locals init ([0] class ConditionalAndIf.DummyType CS$1$0000, [1] bool CS$4$0001) IL_0000: nop IL_0001: ldarg.0 IL_0002: ldnull IL_0003: ceq IL_0005: ldc.i4.0 IL_0006: ceq IL_0008: stloc.1 IL_0009: ldloc.1 IL_000a: brtrue.s IL_0015 IL_000c: nop IL_000d: newobj instance void ConditionalAndIf.DummyType::.ctor() IL_0012: stloc.0 IL_0013: br.s IL_001a IL_0015: nop IL_0016: ldarg.0 IL_0017: stloc.0 IL_0018: br.s IL_001a IL_001a: ldloc.0 IL_001b: ret } // end of method Program::GetDummyByIf
Teraz dla naszego operatora warunkowego sytuacja wygląda trochę inaczej. Jeżeli wartość na stack jest null to utwórz i zwróć wartość, w przeciwnym wypadku zwróć obiekt. I dosłownie tylko to jest wykonywane. Kod IL wygląda tak:
.method public hidebysig static class ConditionalAndIf.DummyType GetDummyByConditionalOperator(class ConditionalAndIf.DummyType dummyType) cil managed { // Code size 17 (0x11) .maxstack 2 .locals init ([0] class ConditionalAndIf.DummyType CS$1$0000) IL_0000: nop IL_0001: ldarg.0 IL_0002: brfalse.s IL_0007 IL_0004: ldarg.0 IL_0005: br.s IL_000c IL_0007: newobj instance void ConditionalAndIf.DummyType::.ctor() IL_000c: stloc.0 IL_000d: br.s IL_000f IL_000f: ldloc.0 IL_0010: ret } // end of method Program::GetDummyByConditionalOperator
Oczywiście tłumaczenie tego konkretnego przykładu uzależnione jest od kompilatora, i jeżeli kompilator by wprowadził optymalizację to wynik konwersji kodu do IL mógłby być taki sam.
Teraz zmodyfikujmy lekko nasz przykład do takiej postaci:
using System; namespace ConditionalAndIf { class Program { static void Main(string[] args) { var dummyOne = new DummyType { EnumVal = null }; var dummyTwo = new DummyType { EnumVal = null }; var dummyThree = new DummyType { EnumVal = DummyEnum.DummyVal2 }; var one = GetDummyByConditionalOperator(dummyOne); var two = GetDummyByIf(dummyTwo); var three = GetDummyByIf(dummyThree); Console.WriteLine("By ?: (Fake) :t{0}", one); //Console.WriteLine("By ?: :t{0}", one); Console.WriteLine("By IF :t{0}", two); Console.WriteLine("By IF :t{0}", three); Console.ReadLine(); } public static DummyEnum? GetDummyByConditionalOperator(DummyType dummyType) { //return dummyType.EnumVal.HasValue ? dummyType.EnumVal.Value : null; return DummyEnum.DummyVal; } public static DummyEnum? GetDummyByIf(DummyType dummyType) { if(dummyType.EnumVal.HasValue) { return dummyType.EnumVal.Value; } else { return null; } } } public enum DummyEnum { DummyVal, DummyVal2 } public class DummyType { public DummyEnum? EnumVal { get; set; } } }
Linia z // są celowo zakomentowane. Jeżeli uruchomimy aplikację otrzymamy następujący wynik:
By ?: (Fake) : DummyVal By IF : By IF : DummyVal2
Teraz, odkomentujmy linie z // (możemy zakomentować return jak i Console.WriteLine z Fake. Przy buildzie dostaniemy następujący wyjątek:
Type of conditional expression cannot be determined because there is no implicit conversion between ‘ConditionalAndIf.DummyEnum’ and ‘<null>’
Jest to spowodowane tym iż operator warunkowy wymaga by wynik wyrażenia pierwszego i wyrażenia drugiego (condition ? first_expression : second_expression) zwracał taki sam typ lub typ który posiada implicit convertion do typu z poprzedniego/następnego wyrażenia (chociaż swojego czasu i to było złamane w C#). Ze względu na to, że kompilator nie bierze pod uwagę całego wyrażenia ?: (całego w sensie od tego czego oczekujemy przez warunki) przy analizie a jedynie pojedyncze wyrażenia, nie jest on wstanie określić jaki typ danych powinien zostać zwrócony, przez co też nie ma on pewności iż następuje konwersja implicit pomiędzy null a w tym wypadku Nullable type – dokładniej mówiąc, wyrażenie pierwsze zwraca typ DummyEnum zaś wyrażenie drugie zwraca null, pomiędzy tymi dwoma typami nie ma implicit jak i nawet explicit konwersji nawet jeżeli oczekiwany wynik to Nullable type od DummyEnum..
Możemy rozwiązać więc problem za pomocą czterechróżnych podejść:
- Bezpośrednie z castowanie obiektu null na Nullable type -> (DummyEnum?)null;
- Zwrócenie nowego Nullable DummyEum -> new DummyEnum?();
- Zwrócenie wartości domyślnej DummyEnum? -> default(DummyEnum?);
- Bezpośrednie z castowanie wartości enum na Nullable type –> (DummyEnum?)dummyType.EnumVal.Value.
Ten problem nie istnieje w instrukcji warunkowej gdyż kompilator w tym wypadku sprawdza oczekiwany wynik metody – stosuje walidację po return a nie per wyrażenie. Także nie oczekuje on by zarówno w IF jak i ELSE nastąpiło zwrócenie wartości. Równie dobrze w ELSE możemy wykonać operację przypisania referencji do naszego obiektu i zwrócić go po zakończeniu bloku ELSE. Możemy tutaj też wykonać wiele innych operacji, jedyne co będzie kompilator interesowało to to, by nastąpił return oraz by typ zwracany był zgodny z typem zadeklarowanym w metodzie (uproszczony opis, nie obejmuje innych walidacji i analiz). Co prowadzi do tego iż bloki IF I ELSE są niezależne od siebie (w sensie, że mogą sobie robić co im się żywnie podoba, ich wykonanie zaś jest uzależnione od spełnienia/niespełnienia warunku).
Podsumowując, istnieją trzy różnice:
- ?: jest operatorem, zaś IF instrukcją (IMO to znacząca różnica) – przynajmniej na tyle, że kompilator inne reguły walidacji stosuje (nie wspominając już o patrz PS2 i opis powyżej);
- Kompilator inaczej tłumaczy ?: i IF, jednak to może być uzależnione od optymalizacji kompilatora jak i jego implementacji – przynajmniej dla prostych przykładów;
- Obsługa Nullable types musi być explicit w ?: zaś w IF może być implicit.
Można więc powiedzieć, iż operator warunkowy czasami (w większości przypadków) jest jednoznaczny z instrukcją warunkową, jednak nie jest on równoważny – przynajmniej kiedy mówimy o języku C# do wersji 4.0.
PS.: wiem, że komentarz do opisu operator warunkowego na MSDN zawiera przykład mówiący to samo ale w bardzo skróconej formie – dla chętnych zapraszam do zapoznania się z nim.
PS2.: oczywiście pomijam fakt, że w IF ELSE możemy zrobić o wiele więcej rzeczy niż w ?: jak chociażby zwrócić wartość albo nie w zależności od naszej zachcianki :)
Pełny przykład:
using System; namespace ConditionalAndIf { class Program { static void Main(string[] args) { var dummyZero = new DummyType { EnumVal = DummyEnum.DummyVal2 }; var dummyOne = new DummyType { EnumVal = null }; var dummyTwo = new DummyType { EnumVal = null }; var dummyThree = new DummyType { EnumVal = DummyEnum.DummyVal2 }; var zero = GetDummyByConditionalOperator(dummyZero); var one = GetDummyByConditionalOperator(dummyOne); var two = GetDummyByIf(dummyTwo); var three = GetDummyByIf(dummyThree); Console.WriteLine("By ?: :t{0}", zero); Console.WriteLine("By ?: :t{0}", one); Console.WriteLine("By IF :t{0}", two); Console.WriteLine("By IF :t{0}", three); Console.ReadLine(); } public static DummyEnum? GetDummyByConditionalOperator(DummyType dummyType) { return dummyType.EnumVal.HasValue ? dummyType.EnumVal.Value : new DummyEnum?(); // three additional options //return dummyType.EnumVal.HasValue ? dummyType.EnumVal.Value : default(DummyEnum?); //return dummyType.EnumVal.HasValue ? dummyType.EnumVal.Value : (DummyEnum?)null; //return dummyType.EnumVal.HasValue ? (DummyEnum?)dummyType.EnumVal.Value : null; } public static DummyEnum? GetDummyByIf(DummyType dummyType) { if(dummyType.EnumVal.HasValue) { return dummyType.EnumVal.Value; } else { return null; } } } public enum DummyEnum { DummyVal, DummyVal2 } public class DummyType { public DummyEnum? EnumVal { get; set; } } }
Post fajny.
Pomijając aspekty techniczne pragnę dodać że operatora trójargumentowego nie powinno się stosować w złożonych warunkach logicznych. Zwłaszcza jeżeli jego wynik jest wartością logiczną. Pamiętam jak w pięciu siedzieliśmy nad kilkoma linijkami kodu szukaliśmy błędu ponad pół godziny i go nie widzieliśmy.
Co do sprawdzenia czy element jest nullem to do tego chyba lepiej czyta się operator ?? a w przypadku typu Nullable<T> GetValueOrDefault
Jeżeli komentarz dodałem drugi raz to proszę usunięcie duplikatu (pierwszy nie chciał się dodać).
dzieki :)
spoko, komentarz pojawil sie tylko raz :)
zgadzam sie zarowno ?: jak i ?? powinno sie stosowac wtedy i tylko wtedy kiedy poprawi to czytelnosc kodu, stosowanie zlozonych konstrukcji operatorow ?? jak i ?: moze spowodowac ze kod bedzie kompletnie nie przejrzysty.
zas co do operatora ?? to roznie z tym bywa, zalezy co chcemy zwrocic :) jakbysmy chciell zwrocic obiekt DummyType a nie DummyEnum? to juz operator ?? nie mialbym zastosowania, ale to jest chyba zrozumiale samo przez sie :)
glownie wykorzystalem taki "beznadziejny" przyklad gdyz wiedzialem iz dla nullable types istnieje znaczaca roznica, dlatego tez nie zamienialem tego na operator ??.
Comments are closed.