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ść:

  1. Bezpośrednie z castowanie obiektu null na Nullable type -> (DummyEnum?)null;
  2. Zwrócenie nowego Nullable DummyEum -> new DummyEnum?();
  3. Zwrócenie wartości domyślnej DummyEnum? -> default(DummyEnum?);
  4. 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:

  1. ?: 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);
  2. 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;
  3. 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; }
    }
}

2 KOMENTARZE

  1. 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ć).

  2. 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.