Polecam

.NET Blogs PL
CodeGuru


Operator warunkowy (?:) a instrukcja warunkowa (if)

March 21, 2011 in categories: pro by Gutek

2

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 comments for "Operator warunkowy (?:) a instrukcja warunkowa (if)"

  1. Wojtek(szogun)
    Wojtek(szogun) Says:

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

    • Gutek
      Gutek Says:

      dzieki Smile

      spoko, komentarz pojawil sie tylko raz Smile

      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 Smile jakbysmy chciell zwrocic obiekt DummyType a nie DummyEnum? to juz operator ?? nie mialbym zastosowania, ale to jest chyba zrozumiale samo przez sie Smile

      glownie wykorzystalem taki "beznadziejny" przyklad gdyz wiedzialem iz dla nullable types istnieje znaczaca roznica, dlatego tez nie zamienialem tego na operator ??.

Comments are closed

© 2008-2010 Jakub Gutkowski. Powered by BlogEngine.NET 1.5.1.14. Hosted on OrcsWeb.

Design