Vraag C # switch verklaring beperkingen - waarom?


Bij het schrijven van een switch-statement lijken er twee beperkingen te bestaan ​​aan wat u in case-statements kunt inschakelen.

Bijvoorbeeld (en ja, ik weet het, als u dit soort dingen doet, betekent dit waarschijnlijk uw Objectgeoriënteerde (OO) architectuur is dubieus - dit is slechts een gekunsteld voorbeeld!),

  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;
  }

Hier mislukt de instructie switch () met 'A-waarde van een integraal type verwacht' en mislukken de case-statements met 'Er wordt een constante waarde verwacht'.

Waarom zijn deze beperkingen van kracht en wat is de onderliggende rechtvaardiging? Ik zie geen reden waarom de switch-instructie heeft om alleen te bezwijken voor statische analyse, en waarom de waarde die wordt ingeschakeld integraal moet zijn (dat wil zeggen, primitief). Wat is de rechtvaardiging?


128
2017-09-04 22:34


oorsprong


antwoorden:


Dit is mijn originele bericht, wat aanleiding gaf tot enig debat ... omdat het verkeerd is:

De switch-instructie is niet hetzelfde   ding als een grote if-else verklaring.   Elk geval moet uniek zijn en worden beoordeeld   statisch. De switch-instructie doet   een constante tijd branch ongeacht   hoeveel gevallen heb je. De if-else   statement evalueert elke voorwaarde   totdat het er een vindt die waar is.


In feite is de instructie C # -schakelaar dat niet altijd een tak met constante tijd.

In sommige gevallen gebruikt de compiler een CIL-schakelinstructie die inderdaad een constante tijdsbaan is met behulp van een springtabel. In schaarse gevallen zoals aangegeven door Ivan Hamilton de compiler genereert misschien iets anders.

Dit is eigenlijk vrij eenvoudig te verifiëren door verschillende C # -verklaringsinstructies te schrijven, sommige schaars, wat compact, en de resulterende CIL te bekijken met de ildasm.exe-tool.


93
2017-09-04 22:51



Het is belangrijk om de instructie C # -schakelaars niet te verwarren met de instructie CIL-schakelaar.

De CIL-schakelaar is een springtabel, die een index vereist voor een set jump-adressen.

Dit is alleen handig als de gevallen van de C # -knop naast elkaar liggen:

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

Maar van weinig nut als ze dat niet zijn:

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

(Je zou een tafel nodig hebben van ~ 3000 ingangen, met slechts 3 slots gebruikt)

Met niet-aangrenzende expressies kan de compiler beginnen met het uitvoeren van lineaire if-else-if-else-controles.

Met grotere niet-aangrenzende expressiesets kan de compiler beginnen met een binaire boomstructuur en ten slotte if-else-if-else de laatste paar items.

Met expressiesets die bundels van aangrenzende items bevatten, kan de compiler binaire tree search en tenslotte een CIL-schakelaar zijn.

Dit staat vol met "mays" en "mights", en het is afhankelijk van de compiler (kan verschillen met Mono of Rotor).

Ik heb uw resultaten op mijn machine gerepliceerd met behulp van aangrenzende cases:

totale tijd om een ​​10-wegenschakelaar uit te voeren, 10000 iteraties (ms): 25.1383
  geschatte tijd per 10-wegschakelaar (ms): 0.00251383

totale tijd om een ​​50-wegenschakelaar uit te voeren, 10000 iteraties (ms): 26.593
  geschatte tijd per 50-wegschakelaar (ms): 0.0026593

totale tijd om een ​​5000-wegschakelaar uit te voeren, 10000 iteraties (ms): 23.7094
  geschatte tijd per 5000-wegschakelaar (ms): 0,00237094

totale tijd om een ​​50000-wegschakelaar uit te voeren, 10000 iteraties (ms): 20.0933
  geschatte tijd per 50000-wegschakelaar (ms): 0,00200933

Vervolgens heb ik ook gebruik gemaakt van niet-aangrenzende case-expressies:

totale tijd om een ​​10-wegenschakelaar uit te voeren, 10000 iteraties (ms): 19.6189
  geschatte tijd per 10-wegschakelaar (ms): 0.00196189

totale tijd om een ​​500-wegschakelaar uit te voeren, 10000 iteraties (ms): 19.1664
  geschatte tijd per 500-wegschakelaar (ms): 0.00191664

totale tijd om een ​​5000-wegschakelaar uit te voeren, 10000 iteraties (ms): 19.5871
  geschatte tijd per 5000-wegschakelaar (ms): 0,00195871

Een niet-aangrenzende 50.000 case switch-instructie compileert niet.
  "Een expressie is te lang of te complex om te compileren in de buurt van 'ConsoleApplication1.Program.Main (string [])'

Wat hier zo grappig is, is dat het binaire boomstamonderzoek een beetje (waarschijnlijk niet statistisch) sneller lijkt dan de CIL-schakelinstructie.

Brian, je hebt het woord gebruikt "constante", wat een zeer duidelijke betekenis heeft vanuit een perspectief van de computationele complexiteitstheorie. Hoewel het simplistische aangrenzende gehele getal een CIL kan produceren die als O (1) (constant) wordt beschouwd, is een dun voorbeeld O (log n) (logaritmisch), geclusterde voorbeelden liggen ergens tussenin, en kleine voorbeelden zijn O (n) (lineair).

Dit gaat niet eens in op de String-situatie, waarin een statische Generic.Dictionary<string,int32> kan worden gemaakt en zal bij het eerste gebruik duidelijk overmatige overhead lijden. De prestaties zijn hier afhankelijk van de prestaties van Generic.Dictionary.

Als u de C # Taalspecificatie (niet de CIL-specificatie) u vindt "15.7.2 De switch-instructie" maakt geen melding van "constante tijd" of dat de onderliggende implementatie zelfs de CIL-schakelinstructie gebruikt (wees voorzichtig met het aannemen van dergelijke dingen).

Aan het einde van de dag is een C # -switch tegen een integerexpressie op een modern systeem een ​​sub-microseconde-bewerking en is het normaal gesproken niet de moeite waard om je zorgen over te maken.


Natuurlijk zijn deze tijden afhankelijk van machines en omstandigheden. Ik zou geen aandacht besteden aan deze timingtests, de duur van de microseconden waar we het over hebben wordt overschaduwd door enige "echte" code die wordt uitgevoerd (en je moet wat "echte code" opnemen anders zal de compiler de tak optimaliseren), of jitter in het systeem. Mijn antwoorden zijn gebaseerd op gebruik IL DASM om de CIL te onderzoeken die is gemaakt door de C # -compiler. Dit is natuurlijk niet definitief, omdat de feitelijke instructies die de CPU uitvoert vervolgens worden gemaakt door de JIT.

Ik heb de uiteindelijke CPU-instructies gecontroleerd die daadwerkelijk op mijn x86-machine zijn uitgevoerd en kan een eenvoudige aangrenzende set-schakelaar bevestigen door iets als:

  jmp     ds:300025F0[eax*4]

Waar een binaire boomstructuur zit vol met:

  cmp     ebx, 79Eh
  jg      3000352B
  cmp     ebx, 654h
  jg      300032BB
  …
  cmp     ebx, 0F82h
  jz      30005EEE

106
2017-09-07 08:47



De eerste reden die in me opkomt is historisch:

Omdat de meeste C-, C ++- en Java-programmeurs niet gewend zijn aan dergelijke vrijheden, eisen ze deze niet.

Een andere, meer geldige, reden is dat de de complexiteit van de taal zou toenemen:

Allereerst, als de objecten worden vergeleken .Equals() of met de == operator? Beide zijn in sommige gevallen geldig. Moeten we nieuwe syntaxis introduceren om dit te doen? Moeten we de programmeur toestaan ​​om zijn eigen vergelijkingsmethode te introduceren?

Bovendien zou het mogelijk maken om objecten in te schakelen verbreken van onderliggende aannames over de switch-instructie. Er zijn twee regels die de switchinstructie bepalen die de compiler niet zou kunnen afdwingen als objecten zouden kunnen worden ingeschakeld (zie de C # versie 3.0 taalspecificatie, §8.7.2):

  • Dat de waarden van schakeletiketten zijn constante
  • Dat de waarden van schakeletiketten zijn onderscheiden (zodat slechts één schakelblok kan worden geselecteerd voor een gegeven schakelaar-uitdrukking)

Beschouw dit codevoorbeeld in het hypothetische geval dat niet-constante case-waarden waren toegestaan:

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;
    }
}

Wat doet de code? Wat als de case-statements opnieuw worden gerangschikt? Inderdaad, een van de redenen waarom C # de overstap illegaal maakte, is dat de switchinstructies willekeurig kunnen worden herschikt.

Deze regels zijn om een ​​reden aanwezig - zodat de programmeur, door naar één gevalblok te kijken, met zekerheid weet onder welke precieze voorwaarde het blok wordt ingevoerd. Wanneer de bovengenoemde switch-verklaring uitgroeit tot 100 lijnen of meer (en dat zal ook zo zijn), is dergelijke kennis van onschatbare waarde.


21
2017-09-05 13:11



Meestal zijn die beperkingen aanwezig vanwege taalontwerpers. De onderliggende rechtvaardiging kan zijn compatibiliteit met languange geschiedenis, idealen of vereenvoudiging van het ontwerp van de compiler.

De compiler kan (en doet) ervoor kiezen om:

  • maak een grote if-else-verklaring
  • gebruik een MSIL-schakelinstructie (springtabel)
  • bouw een Generic.Dictionary <string, int32>, vul deze in bij eerste gebruik en bel Generic.Dictionary <> :: TryGetValue () om een ​​index door te geven aan een MSIL-switch instructie (springtafel)
  • gebruik een combinatie van if-elses en MSIL "switch" springt

De switch-instructie IS GEEN constante tijdsvak. De compiler vindt mogelijk short-cuts (met hash-buckets, enzovoort), maar meer gecompliceerde cases zullen meer gecompliceerde MSIL-code genereren, waarbij sommige gevallen eerder vertakken dan andere.

Om de String-casus af te handelen, zal de compiler (op een bepaald punt) eindigen met a.Equals (b) (en mogelijk een.GetHashCode ()). Ik denk dat het voor de compiler een voordeel zou zijn om elk object te gebruiken dat aan deze beperkingen voldoet.

Wat de behoefte aan statische case-expressies betreft ... sommige van die optimalisaties (hashing, caching, enz.) Zouden niet beschikbaar zijn als de case-expressies niet deterministisch waren. Maar we hebben al gezien dat de compiler soms gewoon de simplistische if-else-if-else weg kiest ...

Bewerk: lomaxx - Uw begrip van de "typeof" -operator is niet correct. De operator "typeof" wordt gebruikt om het System.Type-object voor een type te verkrijgen (niets te maken met de supertypen of interfaces). Het controleren van de runtime-compatibiliteit van een object met een bepaald type is de taak van de operator "is". Het gebruik van "typeof" hier om een ​​object uit te drukken is niet relevant.


10
2017-09-05 11:33



Trouwens, VB, met dezelfde onderliggende architectuur, maakt veel flexibeler Select Case verklaringen (de bovenstaande code zou werken in VB) en nog steeds efficiënte code produceren waar dit mogelijk is, zodat het argument met technische beperkingen zorgvuldig moet worden overwogen.


8
2017-09-05 11:49



Ik zie geen enkele reden waarom de switch-instructie alleen mag werken voor statische analyse

Toegegeven, dat doet het niet hebben naar, en in veel talen worden dynamische schakelinstructies gebruikt. Dit betekent echter dat het herschikken van de "case" -clausules het gedrag van de code kan veranderen.

Er is een aantal interessante informatie achter de ontwerpbeslissingen die hier in "switch" zijn gegaan: Waarom is de instructie C # -schakelaar zo ontworpen dat deze geen doorval mogelijk maakt, maar toch een pauze nodig heeft?

Het toestaan ​​van dynamische case-expressies kan leiden tot monstruositeiten zoals deze PHP-code:

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

die eerlijk gezegd gewoon de if-else uitspraak.


6
2018-06-04 20:40



Microsoft heeft je eindelijk gehoord!

Nu met C # 7 kunt u:

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));
}

6
2018-02-17 19:07



Terwijl op het onderwerp, volgens Jeff Atwood, de switch-instructie is een programmeergod. Gebruik ze spaarzaam.

U kunt vaak dezelfde taak uitvoeren met behulp van een tabel. Bijvoorbeeld:

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]);

5
2017-09-04 23:06



Dit is geen reden waarom, maar de C # -specificatiesectie 8.7.2 vermeldt het volgende:

Het regulerende type van een switch-instructie wordt vastgesteld door de switch-expressie. Als het type switchexpressie sbyte, byte, short, ushort, int, uint, long, ulong, char, string of een enum-type is, dan is dat het regeltype van de switch-instructie. Anders moet er precies één door de gebruiker gedefinieerde impliciete conversie (§6.4) bestaan ​​uit het type switchuitdrukking tot een van de volgende mogelijke beheervormen: sbyte, byte, short, ushort, int, uint, long, ulong, char, string . Als er geen dergelijke impliciete conversie bestaat, of als er meer dan één dergelijke impliciete conversie bestaat, treedt er een compilatietijdfout op.

De C # 3.0-specificatie bevindt zich op: http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc


3
2017-09-04 22:54



Het antwoord van Juda hierboven gaf me een idee. U kunt het bovenstaande schakelgedrag van de OP "neppen" met behulp van a 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());
                    });

Hiermee kunt u gedrag koppelen aan een type in dezelfde stijl als de instructie switch. Ik geloof dat het het extra voordeel heeft dat het wordt gecodeerd in plaats van een jump-style springtabel wanneer het wordt gecompileerd naar IL.


3
2018-03-07 16:27