Ograniczenia instrukcji przełącznika języka C # - dlaczego?

140

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?

ljs
źródło
1
Zobacz możliwe obejście. Czy istnieje lepsza alternatywa dla „włączania typu”?
Michael Freidgeim,
Inną opcją włączania typów wbudowanych jest użycie TypeCode Enum .
Erik Philips
Po prostu utwórz ENUM i użyj NameOf w przypadku Switch. Będzie działać jako stała na zmiennej dynamicznej.
Vaibhav Zainspirowany

Odpowiedzi:

98

To jest mój oryginalny post, który wywołał dyskusję ... ponieważ jest zły :

Instrukcja switch nie jest tym samym, co duża instrukcja if-else. Każdy przypadek musi być unikalny i oceniany statycznie. Instrukcja switch ma stałą gałąź czasu, niezależnie od liczby przypadków. Instrukcja if-else ocenia każdy warunek, aż znajdzie taki, który jest prawdziwy.


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.

Brian Ensink
źródło
4
Jak zauważono w innych odpowiedziach (w tym mojej), twierdzenia zawarte w tej odpowiedzi są nieprawidłowe. Poleciłbym usunięcie (choćby po to, aby uniknąć narzucenia tego (prawdopodobnie powszechnego) błędnego przekonania).
mweerden,
Proszę zobaczyć mój post poniżej, w którym pokazuję, moim zdaniem, ostatecznie, że instrukcja switch ma stałą gałąź czasową.
Brian Ensink,
Bardzo dziękuję za odpowiedź, Brian. Zobacz odpowiedź Ivana Hamiltona ((48259) [ beta.stackoverflow.com/questions/44905/#48259] ). Krótko mówiąc: mówisz o switch instrukcji (CIL), która nie jest tym samym, co switchinstrukcja C #.
mweerden,
Nie wierzę, że kompilator generuje rozgałęzienia w stałym czasie podczas włączania łańcuchów.
Drew Noakes
Czy to nadal ma zastosowanie z dopasowywaniem wzorców w instrukcjach przełączania przypadków w języku C # 7.0?
B. Darren Olson
114

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:

case 3: blah; break;
case 4: blah; break;
case 5: blah; break;

Ale mało przydatne, jeśli nie są:

case 10: blah; break;
case 200: blah; break;
case 3000: blah; break;

(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:

całkowity czas wykonania 10-pozycyjnego przełącznika, 10000 iteracji (ms): 25,1383
przybliżony czas na 10-pozycyjny przełącznik (ms): 0,00251383

całkowity czas wykonania 50-stykowego przełącznika, 10000 iteracji (ms): 26,593
przybliżony czas na 50-kierunkowy przełącznik (ms): 0,0026593

całkowity czas do wykonania przełącznika z 5000 kierunków, 10000 iteracji (ms): 23,7094
przybliżony czas na przełączenie z 5000 kierunków (ms): 0,00237094

całkowity czas do wykonania przełącznika 50000, 10000 iteracji (ms): 20,0933
przybliżony czas na 50000 przełącznika kierunkowego (ms): 0,00200933

Następnie użyłem również nieprzylegających wyrażeń wielkości liter:

całkowity czas wykonania 10-pozycyjnego przełącznika, 10000 iteracji (ms): 19,6189
przybliżony czas na 10-pozycyjny przełącznik (ms): 0,00196189

całkowity czas wykonania 500-kierunkowego przełącznika, 10000 iteracji (ms): 19,1664
przybliżony czas na 500-kierunkowy przełącznik (ms): 0,00191664

całkowity czas do wykonania przełącznika z 5000 kierunków, 10000 iteracji (ms): 19,5871
przybliżony czas na przełączenie z 5000 kierunków (ms): 0,00195871

Nie sąsiadująca instrukcja przełączania wielkości liter 50 000 nie zostanie skompilowana.
„Wyrażenie jest zbyt długie lub zbyt złożone, aby można je było skompilować w pobliżu„ ConsoleApplication1.Program.Main (string []) ”

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ści Generic.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:

  jmp     ds:300025F0[eax*4]

Gdzie wyszukiwanie drzewa binarnego jest pełne:

  cmp     ebx, 79Eh
  jg      3000352B
  cmp     ebx, 654h
  jg      300032BB
  
  cmp     ebx, 0F82h
  jz      30005EEE
Ivan Hamilton
źródło
Wyniki twoich eksperymentów trochę mnie zaskakują. Czy zamieniłeś swoją na Briana? Jego wyniki pokazują wzrost wraz z rozmiarem, podczas gdy twoje nie. Brakuje mi czegoś? W każdym razie dziękuję za jasną odpowiedź.
mweerden,
Trudno jest dokładnie obliczyć czas przy tak małej operacji. Nie udostępnialiśmy kodu ani procedur testowych. Nie rozumiem, dlaczego jego czas miałby się wydłużyć w przypadku sąsiednich przypadków. Moje były 10 razy szybsze, więc środowiska i kod testów mogą się znacznie różnić.
Ivan Hamilton,
23

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):

  • Czy wartości etykiet przełączników są stałe
  • Czy wartości etykiet przełączników są różne (tak, że dla danego wyrażenia przełączającego można wybrać tylko jeden blok przełącznika)

Rozważ ten przykład kodu w hipotetycznym przypadku, w którym dozwolone są niestałe wartości wielkości:

void DoIt()
{
    String foo = "bar";
    Switch(foo, foo);
}

void Switch(String val1, String val2)
{
    switch ("bar")
    {
        // The compiler will not know that val1 and val2 are not distinct
        case val1:
            // Is this case block selected?
            break;
        case val2:
            // Or this one?
            break;
        case "bar":
            // Or perhaps this one?
            break;
    }
}

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.

Antti Kissaniemi
źródło
2
Warto zwrócić uwagę na zmianę kolejności przełącznika. Upadek jest legalny, jeśli etui nie zawiera kodu. Na przykład Przypadek 1: Przypadek 2: Console.WriteLine ("Cześć"); przerwa;
Joel McBeth
10

Nawiasem mówiąc, VB, mając tę ​​samą podstawową architekturę, pozwala na znacznie bardziej elastyczne Select Caseinstrukcje (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.

Konrad Rudolph
źródło
1
Select CasePl VB jest bardzo elastyczna i super oszczędność czasu. Bardzo za tym tęsknię.
Eduardo Molteni
@EduardoMolteni Przełącz się na F # w takim razie. W porównaniu z tym przełączniki Pascala i VB wydają się idiotycznymi dziećmi.
Luaan
10

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

  • utwórz dużą instrukcję if-else
  • użyj instrukcji przełącznika MSIL (tablica skoków)
  • zbuduj Generic.Dictionary <string, int32>, wypełnij go przy pierwszym użyciu i wywołaj Generic.Dictionary <> :: TryGetValue (), aby indeks został przekazany do instrukcji przełącznika MSIL (tabela skoku)
  • użyj kombinacji if-elses i skoków "przełącznika" MSIL

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.

Ivan Hamilton
źródło
6

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:

var table = new Dictionary<Type, string>()
{
   { typeof(int), "it's an int!" }
   { typeof(string), "it's a string!" }
};

Type someType = typeof(int);
Console.WriteLine(table[someType]);
Judah Gabriel Himango
źródło
7
Poważnie cytujesz czyjś post na Twitterze bez żadnych dowodów? Przynajmniej link do wiarygodnego źródła.
Ivan Hamilton
4
Pochodzi z pewnego źródła; ten post na Twitterze pochodzi od Jeffa Atwooda, autora oglądanej witryny. :-) Jeff ma kilka postów na blogach na ten temat, jeśli jesteś ciekawy.
Judah Gabriel Himango
Uważam, że to całkowite BS - niezależnie od tego, czy napisał to Jeff Atwood. To zabawne, jak dobrze instrukcja switch nadaje się do obsługi maszyn stanu i innych przykładów zmiany przepływu kodu w oparciu o wartość enumtypu. Nie jest też przypadkiem, że funkcja Intellisense automatycznie wypełnia instrukcję przełącznika po włączeniu zmiennej danego enumtypu.
Jonathon Reinhart,
@JonathonReinhart Tak, myślę, że o to chodzi - są lepsze sposoby obsługi kodu polimorficznego niż użycie switchinstrukcji. 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ć na switchdopasowanie do wzorca. Lub użyj na przykład interfejsów.
Luaan
Stara odpowiedź / pytanie, ale pomyślałbym, że (popraw mnie, jeśli się mylę) Dictionarybyłoby znacznie wolniejsze niż zoptymalizowana switchinstrukcja ...?
Paul
6

Nie widzę żadnego powodu, dla którego instrukcja switch miałaby ustępować tylko analizie statycznej

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:

switch (true) {
    case a == 5:
        ...
        break;
    case b == 10:
        ...
        break;
}

który szczerze powinien po prostu użyć if-elseoświadczenia.

Roman Starkov
źródło
1
To właśnie uwielbiam w PHP (teraz, kiedy przechodzę na C #), to wolność. Z tym przychodzi wolność napisać zły kod, ale to jest coś naprawdę brakuje mi w C #
silkfire
5

Microsoft w końcu cię usłyszał!

Teraz z C # 7 możesz:

switch(shape)
{
case Circle c:
    WriteLine($"circle with radius {c.Radius}");
    break;
case Rectangle s when (s.Length == s.Height):
    WriteLine($"{s.Length} x {s.Height} square");
    break;
case Rectangle r:
    WriteLine($"{r.Length} x {r.Height} rectangle");
    break;
default:
    WriteLine("<unknown shape>");
    break;
case null:
    throw new ArgumentNullException(nameof(shape));
}
dimaaan
źródło
3

To nie jest powód dlaczego, ale sekcja 8.7.2 specyfikacji języka C # stwierdza, co następuje:

Zarządzający typ instrukcji switch jest ustalany przez wyrażenie switch. Jeśli typ wyrażenia przełączającego to sbyte, byte, short, ushort, int, uint, long, ulong, char, string lub enum-type, to jest to typ nadrzędny instrukcji switch. W przeciwnym razie musi istnieć dokładnie jedna niejawna konwersja zdefiniowana przez użytkownika (§6.4) z typu wyrażenia przełączającego na jeden z następujących możliwych typów zarządzających: sbyte, byte, short, ushort, int, uint, long, ulong, char, string . Jeśli taka niejawna konwersja nie istnieje lub jeśli istnieje więcej niż jedna taka niejawna konwersja, wystąpi błąd w czasie kompilacji.

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

markus
źródło
3

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

Dictionary<Type, Func<object, string,  string>> typeTable = new Dictionary<Type, Func<object, string, string>>();
typeTable.Add(typeof(int), (o, s) =>
                    {
                        return string.Format("{0}: {1}", s, o.ToString());
                    });

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.

Dave Swersky
źródło
0

Przypuszczam, że nie ma podstawowego powodu, dla którego kompilator nie mógł automatycznie przetłumaczyć instrukcji switch na:

if (t == typeof(int))
{
...
}
elseif (t == typeof(string))
{
...
}
...

Ale niewiele na tym zyskuje.

Instrukcja case dotycząca typów całkowitych umożliwia kompilatorowi wykonanie szeregu optymalizacji:

  1. 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?

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

Rob Walker
źródło
1
Ważną złożonością, na którą należy tutaj zwrócić uwagę, jest to, że model pamięci .NET ma pewne silne gwarancje, które sprawiają, że twój pseudokod nie jest dokładnie równoważny z (hipotetycznym, nieprawidłowym C # ), switch (t) { case typeof(int): ... }ponieważ twoje tłumaczenie oznacza, że ​​zmienna t musi być pobrana z pamięci dwukrotnie t != 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 Windows
Glenn Slayden,
0

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

fryguybob
źródło
0

napisał:

„Instrukcja switch ma stałą gałąź czasu, niezależnie od liczby przypadków”.

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.

Henk
źródło
0

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.

BCS
źródło
0

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.

HS.
źródło
0

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

mweerden
źródło
0

C # 8 umożliwia eleganckie i kompaktowe rozwiązanie tego problemu przy użyciu wyrażenia przełączającego:

public string GetTypeName(object obj)
{
    return obj switch
    {
        int i => "Int32",
        string s => "String",
        { } => "Unknown",
        _ => throw new ArgumentNullException(nameof(obj))
    };
}

W rezultacie otrzymujesz:

Console.WriteLine(GetTypeName(obj: 1));           // Int32
Console.WriteLine(GetTypeName(obj: "string"));    // String
Console.WriteLine(GetTypeName(obj: 1.2));         // Unknown
Console.WriteLine(GetTypeName(obj: null));        // System.ArgumentNullException

Możesz przeczytać więcej o nowej funkcji tutaj .

smolchanovsky
źródło