Podczas pisania instrukcji switch wydaje się, że istnieją dwa ograniczenia dotyczące tego, co można włączyć w instrukcjach case.
Na przykład (i tak, wiem, jeśli robisz tego typu rzeczy, prawdopodobnie oznacza to, że twoja architektura zorientowana obiektowo (OO) jest niepewna - to tylko wymyślony przykład!),
Type t = typeof(int);
switch (t) {
case typeof(int):
Console.WriteLine("int!");
break;
case typeof(string):
Console.WriteLine("string!");
break;
default:
Console.WriteLine("unknown!");
break;
}
W tym przypadku instrukcja switch () kończy się niepowodzeniem z komunikatem „Oczekiwano wartości typu całkowitego”, a instrukcje przypadku kończą się błędem „Oczekiwana jest stała wartość”.
Dlaczego te ograniczenia istnieją i jakie jest ich uzasadnienie? Nie widzę żadnego powodu, dlaczego Instrukcja switch ma ulec jedynie analizy statycznej i dlaczego wartość istota włączony musi być integralną (czyli prymitywne). Jakie jest uzasadnienie?
źródło
Odpowiedzi:
To jest mój oryginalny post, który wywołał dyskusję ... ponieważ jest zły :
W rzeczywistości instrukcja przełączania języka C # nie zawsze jest stałą gałęzią czasu.
W niektórych przypadkach kompilator użyje instrukcji przełącznika CIL, która w rzeczywistości jest gałęzią czasu o stałej długości korzystającą z tabeli skoków. Jednak w rzadkich przypadkach, jak wskazał Ivan Hamilton, kompilator może wygenerować coś zupełnie innego.
W rzeczywistości jest to dość łatwe do zweryfikowania, pisząc różne instrukcje przełączania C #, niektóre rzadkie, inne zagęszczone, i patrząc na wynikowy CIL za pomocą narzędzia ildasm.exe.
źródło
switch
instrukcji (CIL), która nie jest tym samym, coswitch
instrukcja C #.Ważne jest, aby nie mylić instrukcji przełącznika C # z instrukcją przełącznika CIL.
Przełącznik CIL to tablica skoku, która wymaga indeksu w zestawie adresów skoku.
Jest to przydatne tylko wtedy, gdy przypadki przełącznika C # są sąsiadujące:
Ale mało przydatne, jeśli nie są:
(Potrzebowałbyś tabeli o rozmiarze ~ 3000 wpisów, z użyciem tylko 3 gniazd)
W przypadku wyrażeń niesąsiadujących kompilator może rozpocząć wykonywanie liniowych kontroli if-else-if-else.
W przypadku większych, niesąsiadujących ze sobą zestawów wyrażeń kompilator może rozpocząć od przeszukiwania drzewa binarnego, a na końcu if-else-if-else kilku ostatnich elementów.
W przypadku zestawów wyrażeń zawierających grupy sąsiednich elementów, kompilator może przeszukiwać drzewo binarne, a na końcu przełączać CIL.
Jest to pełne "majów" i "potęg" i jest zależne od kompilatora (może się różnić w przypadku Mono lub Rotora).
Zreplikowałem twoje wyniki na moim komputerze przy użyciu sąsiednich przypadków:
Następnie użyłem również nieprzylegających wyrażeń wielkości liter:
Zabawne jest to, że przeszukiwanie drzewa binarnego pojawia się trochę (prawdopodobnie nie statystycznie) szybciej niż instrukcja przełącznika CIL.
Brian, użyłeś słowa „ stała ”, które ma bardzo określone znaczenie z perspektywy teorii złożoności obliczeniowej. Podczas gdy uproszczony przykład sąsiedniej liczby całkowitej może dać CIL, który jest uważany za O (1) (stała), rzadki przykład to O (log n) (logarytmiczny), przykłady skupione znajdują się gdzieś pomiędzy, a małe przykłady to O (n) (liniowe ).
Nie rozwiązuje to nawet sytuacji typu String, w której
Generic.Dictionary<string,int32>
może zostać utworzony statyczny , a przy pierwszym użyciu będzie to miało określony narzut. Wydajność w tym miejscu będzie zależna od wydajnościGeneric.Dictionary
.Jeśli sprawdzisz specyfikację języka C # (nie specyfikację CIL), zobaczysz "15.7.2 Instrukcja switch" nie wspomina o "stałym czasie" lub że podstawowa implementacja używa nawet instrukcji przełącznika CIL (uważaj przy założeniu takie rzeczy).
Pod koniec dnia przełączenie C # na wyrażenie całkowite w nowoczesnym systemie jest operacją poniżej mikrosekundy i zwykle nie warto się tym martwić.
Oczywiście te czasy będą zależeć od maszyn i warunków. Nie zwracałbym uwagi na te testy czasowe, czas trwania w mikrosekundach, o którym mówimy, jest przyćmiony przez każdy uruchamiany „prawdziwy” kod (i musisz dołączyć jakiś „prawdziwy kod”, w przeciwnym razie kompilator zoptymalizuje gałąź) lub jitter w systemie. Moje odpowiedzi opierają się na używaniu IL DASM do badania CIL utworzonego przez kompilator C #. Oczywiście nie jest to ostateczne, ponieważ JIT tworzy instrukcje, które uruchamia procesor.
Sprawdziłem końcowe instrukcje procesora faktycznie wykonywane na mojej maszynie x86 i mogę potwierdzić prosty sąsiedni przełącznik zestawu, wykonujący coś takiego:
Gdzie wyszukiwanie drzewa binarnego jest pełne:
źródło
Pierwszy powód, który przychodzi na myśl, jest historyczny :
Ponieważ większość programistów C, C ++ i Java nie jest przyzwyczajonych do posiadania takich swobód, nie wymagają ich.
Innym, ważniejszym powodem jest wzrost złożoności języka :
Przede wszystkim, czy obiekty należy porównać z operatorem,
.Equals()
czy z nim==
? Oba są ważne w niektórych przypadkach. Czy powinniśmy wprowadzić nową składnię, aby to zrobić? Czy powinniśmy pozwolić programiście na wprowadzenie własnej metody porównawczej?Ponadto zezwolenie na włączanie obiektów złamałoby podstawowe założenia dotyczące instrukcji switch . Istnieją dwie reguły dotyczące instrukcji switch, których kompilator nie byłby w stanie wymusić, gdyby zezwolono na włączanie obiektów (zobacz specyfikację języka C # w wersji 3.0 , §8.7.2):
Rozważ ten przykład kodu w hipotetycznym przypadku, w którym dozwolone są niestałe wartości wielkości:
Co zrobi kod? Co się stanie, jeśli opisy przypadków zostaną zmienione? Rzeczywiście, jednym z powodów, dla których C # sprawił, że przełącznik przeszedł jako nielegalny, jest to, że instrukcje switch mogą być dowolnie przestawiane.
Te reguły obowiązują nie bez powodu - aby programista, patrząc na jeden blok przypadku, mógł z całą pewnością poznać dokładny warunek, w jakim blok jest wprowadzany. Kiedy wyżej wspomniana instrukcja przełączania rozrasta się do 100 lub więcej wierszy (i tak się stanie), taka wiedza jest nieoceniona.
źródło
Nawiasem mówiąc, VB, mając tę samą podstawową architekturę, pozwala na znacznie bardziej elastyczne
Select Case
instrukcje (powyższy kod działałby w VB) i nadal tworzy wydajny kod tam, gdzie jest to możliwe, więc argument związany z ograniczeniami technicznymi musi być dokładnie rozważony.źródło
Select Case
Pl VB jest bardzo elastyczna i super oszczędność czasu. Bardzo za tym tęsknię.W większości te ograniczenia są spowodowane przez projektantów języka. Podstawowym uzasadnieniem może być zgodność z historią języków, ideały lub uproszczenie projektu kompilatora.
Kompilator może (i robi) wybrać:
Instrukcja switch NIE JEST stałą gałęzią czasu. Kompilator może znaleźć skróty (używając zasobników z mieszaniem itp.), Ale bardziej skomplikowane przypadki wygenerują bardziej skomplikowany kod MSIL, a niektóre przypadki rozgałęziają się wcześniej niż inne.
Aby obsłużyć przypadek typu String, kompilator zakończy (w pewnym momencie) używając a.Equals (b) (i prawdopodobnie a.GetHashCode ()). Myślę, że byłoby to trywialne, gdyby kompilator użył dowolnego obiektu, który spełnia te ograniczenia.
Jeśli chodzi o potrzebę statycznych wyrażeń wielkości liter ... niektóre z tych optymalizacji (haszowanie, buforowanie itp.) Nie byłyby dostępne, gdyby wyrażenia wielkości liter nie były deterministyczne. Ale widzieliśmy już, że czasami kompilator i tak wybiera po prostu uproszczoną drogę, jeśli - inaczej - jeśli - inaczej ...
Edycja: lomaxx - Twoje rozumienie operatora „typeof” nie jest poprawne. Operator „typeof” służy do uzyskiwania obiektu System.Type dla typu (nie ma to nic wspólnego z jego nadtypami lub interfejsami). Zadaniem operatora jest sprawdzenie zgodności obiektu z danym typem w czasie wykonywania. Użycie tutaj „typeof” do wyrażenia obiektu jest nieistotne.
źródło
W tym temacie, według Jeffa Atwooda, oświadczenie przełącznika jest okrucieństwem programowania . Używaj ich oszczędnie.
Często możesz wykonać to samo zadanie, korzystając z tabeli. Na przykład:
źródło
enum
typu. Nie jest też przypadkiem, że funkcja Intellisense automatycznie wypełnia instrukcję przełącznika po włączeniu zmiennej danegoenum
typu.switch
instrukcji. Nie mówi, że nie powinieneś pisać maszyn stanowych, tylko że możesz zrobić to samo, używając ładnych określonych typów. Oczywiście jest to znacznie łatwiejsze w językach takich jak F #, które mają typy, które mogą z łatwością obejmować dość złożone stany. Na przykład można użyć związków rozłącznych, w których stan staje się częścią typu, i zamienić naswitch
dopasowanie do wzorca. Lub użyj na przykład interfejsów.Dictionary
byłoby znacznie wolniejsze niż zoptymalizowanaswitch
instrukcja ...?To prawda, że nie musi , a wiele języków używa dynamicznych instrukcji przełączających. Oznacza to jednak, że zmiana kolejności klauzul „case” może zmienić zachowanie kodu.
Jest kilka interesujących informacji stojących za decyzjami projektowymi, które przeszły tutaj do „przełączenia”: Dlaczego instrukcja przełącznika C # jest zaprojektowana tak, aby nie zezwalać na przejście, ale nadal wymaga przerwy?
Zezwolenie na dynamiczne wyrażenia wielkości liter może prowadzić do potworności, takich jak ten kod PHP:
który szczerze powinien po prostu użyć
if-else
oświadczenia.źródło
Microsoft w końcu cię usłyszał!
Teraz z C # 7 możesz:
źródło
To nie jest powód dlaczego, ale sekcja 8.7.2 specyfikacji języka C # stwierdza, co następuje:
Specyfikacja języka C # 3.0 znajduje się pod adresem : http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc
źródło
Powyższa odpowiedź Judasza dała mi pewien pomysł. Możesz „sfałszować” zachowanie przełącznika OP powyżej za pomocą
Dictionary<Type, Func<T>
:Pozwala to skojarzyć zachowanie z typem w tym samym stylu, co instrukcja switch. Uważam, że ma dodatkową zaletę bycia kluczem zamiast tablicy skoków w stylu przełącznika po kompilacji do IL.
źródło
Przypuszczam, że nie ma podstawowego powodu, dla którego kompilator nie mógł automatycznie przetłumaczyć instrukcji switch na:
Ale niewiele na tym zyskuje.
Instrukcja case dotycząca typów całkowitych umożliwia kompilatorowi wykonanie szeregu optymalizacji:
Nie ma duplikatów (chyba że zduplikujesz etykiety wielkości liter, które kompilator wykryje). W twoim przykładzie t może pasować do wielu typów ze względu na dziedziczenie. Czy powinien zostać wykonany pierwszy mecz? Wszyscy?
Kompilator może zdecydować się na zaimplementowanie instrukcji switch w typie całkowitym za pomocą tabeli skoków, aby uniknąć wszystkich porównań. Jeśli włączasz wyliczenie, które ma wartości całkowite od 0 do 100, tworzy ono tablicę zawierającą 100 wskaźników, po jednym dla każdej instrukcji switch. W czasie wykonywania po prostu wyszukuje adres z tablicy na podstawie włączanej wartości całkowitej. Zapewnia to znacznie lepszą wydajność w czasie wykonywania niż wykonanie 100 porównań.
źródło
switch (t) { case typeof(int): ... }
ponieważ twoje tłumaczenie oznacza, że zmiennat
musi być pobrana z pamięci dwukrotniet != typeof(int)
, podczas gdy ten drugi (prawdopodobnie) zawsze czytaj wartośćt
dokładnie raz . Ta różnica może złamać poprawność współbieżnego kodu, który opiera się na tych doskonałych gwarancjach. Więcej informacji na ten temat można znaleźć w artykule Joe Duffy's Concurrent Programming on WindowsZgodnie z dokumentacją instrukcji switch, jeśli istnieje jednoznaczny sposób niejawnej konwersji obiektu na typ całkowity, będzie to dozwolone. Myślę, że spodziewasz się zachowania, w którym dla każdego stwierdzenia przypadku zostanie ono zastąpione przez
if (t == typeof(int))
, ale otworzyłoby to całą puszkę robaków, gdy przeciążasz ten operator. Zachowanie zmieniłoby się, gdyby szczegóły implementacji instrukcji switch uległy zmianie, jeśli napisałeś == override niepoprawnie. Ograniczając porównania do typów całkowitych i ciągów oraz tych rzeczy, które można zredukować do typów całkowitych (i są przeznaczone), unikają potencjalnych problemów.źródło
Ponieważ język pozwala na użycie typu string w instrukcji switch, zakładam, że kompilator nie jest w stanie wygenerować kodu dla implementacji gałęzi czasu stałego dla tego typu i musi wygenerować styl jeśli-to.
@mweerden - Ah, rozumiem. Dzięki.
Nie mam dużego doświadczenia w C # i .NET, ale wygląda na to, że projektanci języka nie pozwalają na statyczny dostęp do systemu typów, chyba że w wąskich okolicznościach. Typeof kluczowe zwraca obiekt więc jest dostępny tylko w czasie wykonywania.
źródło
Myślę, że Henk przyłożył się do tego, mówiąc o „braku dostępu do systemu typów”
Inną opcją jest to, że nie ma kolejności typów, w których mogą znajdować się liczby i łańcuchy. Tak więc zmiana typu nie może zbudować binarnego drzewa wyszukiwania, tylko liniowe wyszukiwanie.
źródło
Zgadzam się z tym komentarzem, że stosowanie podejścia opartego na tabelach jest często lepsze.
W C # 1.0 nie było to możliwe, ponieważ nie było generycznych i anonimowych delegatów. Nowe wersje języka C # mają szkielet, który umożliwia wykonanie tej czynności. Pomocna jest również notacja literałów obiektowych.
źródło
Praktycznie nie znam języka C #, ale podejrzewam, że któryś z przełączników został po prostu wzięty, tak jak w innych językach, bez zastanawiania się nad uczynieniem go bardziej ogólnym lub deweloper zdecydował, że nie warto go rozszerzać.
Ściśle mówiąc, masz absolutną rację, że nie ma powodu, aby nakładać na to te ograniczenia. Można by podejrzewać, że powodem jest to, że w dozwolonych przypadkach implementacja jest bardzo wydajna (jak zasugerował Brian Ensink ( 44921 )), ale wątpię, czy implementacja jest bardzo wydajna (wrt if-statement), jeśli używam liczb całkowitych i niektórych przypadkowych przypadków (np. 345, -4574 i 1234203). A w każdym razie, jaka jest szkoda w dopuszczaniu tego na wszystko (a przynajmniej na więcej) i mówieniu, że jest to skuteczne tylko w określonych przypadkach (takich jak (prawie) kolejne liczby).
Mogę sobie jednak wyobrazić, że można chcieć wykluczyć typy z powodów takich jak ten podany przez lomaxx ( 44918 ).
Edycja: @Henk ( 44970 ): Jeśli ciągi są maksymalnie współużytkowane, ciągi o równej zawartości będą również wskaźnikami do tej samej lokalizacji pamięci. Następnie, jeśli możesz upewnić się, że ciągi używane w przypadkach są przechowywane kolejno w pamięci, możesz bardzo wydajnie zaimplementować przełącznik (tj. Z wykonaniem w kolejności 2 porównań, dodaniem i dwoma skokami).
źródło
C # 8 umożliwia eleganckie i kompaktowe rozwiązanie tego problemu przy użyciu wyrażenia przełączającego:
W rezultacie otrzymujesz:
Możesz przeczytać więcej o nowej funkcji tutaj .
źródło