146 votes

C# instruction switch limitations - pourquoi?

Lors de l'écriture d'une instruction switch il semble y avoir deux limitations sur ce que l'on peut activer en cas de déclarations.

Par exemple (et oui, je sais, si vous êtes en train de faire ce genre de chose, cela signifie probablement que votre orientée objet (OO), l'architecture est vraiment difficile - c'est juste un exemple artificiel!),

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

Ici, l'instruction switch() échoue avec "Une valeur d'un type intégral attendu" et l'affaire états échouer avec 'Une valeur constante est attendu".

Pourquoi ces restrictions sont en place, et quelle est la justification sous-jacente? Je ne vois pas pourquoi l'instruction switch est à craquez pour l'analyse statique seulement, et pourquoi la valeur sous tension doit être intégrale (primitive). Quelle est la justification?

114voto

Ivan Hamilton Points 1396

Il est important de ne pas confondre le C# instruction switch avec le CIL instruction switch.

Le CIL commutateur est un saut de la table, qui nécessite un index dans un jeu de saut d'adresses.

C'est utile seulement si le C# switch cas sont adjacentes:

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

Mais de peu d'utilité si elles ne sont pas:

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

(Vous aurez besoin d'un tableau de ~3000 entrées dans la taille, avec seulement 3 slots utilisés)

Avec les non-adjacentes, les expressions, le compilateur peut commencer à effectuer linéaire si-sinon-si-sinon vérifie.

Avec la plus grande non - adjacents expression définit, le compilateur peut commencer par un arbre binaire de recherche, et, finalement, si-sinon-si-sinon les derniers articles.

Avec l'expression des ensembles contenant des amas d'éléments adjacents, le compilateur peut-arbre binaire de recherche, et enfin un CIL de l'interrupteur.

C'est plein de "mays" et "pouvoir", et elle dépend du compilateur (peut varier avec Mono ou Rotor).

J'ai répliqué vos résultats sur ma machine adjacentes cas:

temps total à l'exécution d'une voie 10 interrupteur, 10000 itérations (ms) : 25.1383
temps approximatif pour 10 bouton (ms) : 0.00251383

temps total d'exécution de 50 moyen de l'interrupteur, 10000 itérations (ms) : 26.593
temps approximatif pour 50 moyen de l'interrupteur (ms) : 0.0026593

temps total d'exécution de 5000 moyen de l'interrupteur, 10000 itérations (ms) : 23.7094
temps approximatif pour 5000 moyen de l'interrupteur (ms) : 0.00237094

temps total pour exécuter une 50000 moyen de l'interrupteur, 10000 itérations (ms) : 20.0933
temps approximatif pour 50000 moyen de l'interrupteur (ms) : 0.00200933

Puis j'ai également fait de l'aide non-adjacents cas expressions:

temps total à l'exécution d'une voie 10 interrupteur, 10000 itérations (ms) : 19.6189
temps approximatif pour 10 bouton (ms) : 0.00196189

temps total pour exécuter une 500 moyen de l'interrupteur, 10000 itérations (ms) : 19.1664
temps approximatif pour 500 moyen de l'interrupteur (ms) : 0.00191664

temps total d'exécution de 5000 moyen de l'interrupteur, 10000 itérations (ms) : 19.5871
temps approximatif pour 5000 moyen de l'interrupteur (ms) : 0.00195871

Un non-adjacentes de 50 000 cas de l'instruction switch ne serait pas compiler.
"Une expression est trop long ou complexe pour compiler près de 'ConsoleApplication1.Programme.Main(string[])'

Ce qui est drôle ici, c'est que l'arbre binaire de recherche apparaît un peu (probablement pas statistiquement) plus rapide que le CIL instruction switch.

Brian, tu as utilisé le mot "constante", qui a un très net de sens que dans une perspective de la théorie de la complexité algorithmique. Alors que le simpliste adjacentes entier exemple peut produire CIL qui est considéré comme O(1) (constant), éparse exemple est O(log n) (logarithmique), cluster exemples se situent quelque part entre les deux, et les petits sont des exemples O(n) (linéaire).

Ce n'est même pas l'adresse de la Chaîne de situation, dans laquelle un statique Generic.Dictionary<string,int32> peut être créé, et subira certaine surcharge lors de la première utilisation. Performance dépendra de la performance de l' Generic.Dictionary.

Si vous cochez la Spécification du Langage C# (et non le CIL spec) vous trouverez "15.7.2 L'instruction switch" ne fait aucune mention de la "constante de temps" ou l'implémentation sous-jacente utilise même le CIL instruction switch (être très prudent de supposer que de telles choses).

À la fin de la journée, C# commutateur contre une expression entière sur un système moderne est un sous-microseconde opération, et normalement pas la peine de s'inquiéter au sujet de.

Non seulement cela, il n'est pas pertinente à la question posée - mais j'aime les mots "de façon concluante" et "empirique" d'être galvaudé.


Bien sûr, ces temps dépendra de machines et de conditions. Je ne voudrais pas prêter attention à ces délais tests, la microseconde durées nous parlons sont éclipsés par une "vraie" code à exécuter (et vous devez inclure certains "code réel", sinon, le compilateur d'optimiser la direction générale de suite), ou de la gigue dans le système. Mes réponses sont basées sur l'utilisation de l'IL DASM pour examiner le CIL créé par le compilateur C#. Bien sûr, ce n'est pas définitive, comme les instructions d'exécution du PROCESSEUR sont ensuite créées par le JIT.

Personnellement, j'ai vérifié la finale instructions du PROCESSEUR effectivement exécutées sur mon x386 de la machine, et peut confirmer une simple touche adjacente de faire quelque chose comme:

  jmp     ds:300025F0[eax*4]

Où un arbre binaire de recherche est plein de:

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

102voto

Brian Ensink Points 7579

C'est mon premier post, ce qui a suscité un certain débat... car c'est mal:

L'instruction switch n'est pas la même chose comme un gros if-else. Chaque cas doit être unique et évalués de manière statique. L'instruction switch n' un temps constant quelle que soit la branche de combien de cas vous avez. L'if-else déclaration évalue chaque condition jusqu'à ce qu'il en trouve une qui est vrai.


En fait, le C# instruction switch est pas toujours une constante de temps de la branche.

Dans certains cas, le compilateur utilisera un CIL instruction switch qui est en effet une constante de temps de la branche à l'aide d'une table de saut. Cependant, dans éparses cas, comme le souligne Ivan Hamilton le compilateur peut générer quelque chose d'autre entièrement.

C'est en fait assez facile à vérifier par la rédaction de diverses C# commutateur de déclarations, certains rares, certains denses, et en regardant l'résultant CIL avec la ildasm.exe outil de.

23voto

Antti Sykäri Points 10381

La première raison qui vient à l'esprit est historique:

Depuis plus C, C++ et Java programmeurs ne sont pas habitués à avoir de telles libertés, ils ne demandent pas à eux.

Un autre, plus valide, la raison est que la complexité du langage permettrait d'augmenter:

Tout d'abord, les objets en comparaison avec .Equals() ou avec l' == opérateur? Les deux sont valables dans certains cas. Doit-on introduire une nouvelle syntaxe pour ce faire? Doit-on permettre au programmeur de présenter leur propre méthode de comparaison?

En outre, en permettant de passer sur les objets briser les hypothèses sous-jacentes sur l'instruction switch. Il y a deux règles qui régissent l'instruction switch que le compilateur ne sera pas en mesure d'appliquer si des objets ont été autorisés à être allumé (voir la version de C# 3.0 spécification du langage, §8.7.2):

  • Que les valeurs de commutation d'étiquettes sont constants
  • Que les valeurs de commutation d'étiquettes sont distinctes (de sorte qu'un seul bloc de commutateurs peuvent être sélectionnés pour un interrupteur-expression)

Considérons cet exemple de code dans le cas hypothétique que les non-constante valeurs de cas ont été admis:

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

Quel sera le code? Que faire si le cas déclarations sont réorganisées? En effet, une des raisons pour lesquelles C# commutateur fait tomber à travers illégal est que les instructions de commutation peut être arbitrairement réarrangés.

Ces règles sont en place pour une raison - de sorte que le programmeur peut, en regardant un cas, de bloquer, de connaître avec précision les conditions dans lesquelles le bloc est entré. Quand celui-ci instruction switch pousse dans les 100 lignes ou plus (et il le fera), une telle connaissance est inestimable.

10voto

Ivan Hamilton Points 1396

Pour la plupart, ces restrictions sont en place à cause de la langue des designers. La justification sous-jacente peut être la compatibilité avec languange l'histoire, les idéaux, ou la simplification de la conception du compilateur.

Le compilateur peut (et ne doit) faire le choix de:

  • créer un grand if-else
  • utiliser un langage MSIL instruction switch (saut de la table)
  • construire un Générique.Dictionary<string,int32>, le remplir lors de la première utilisation, et de les appeler Génériques.Dictionnaire<>::TryGetValue() pour un indice de passer à un MSIL commutateur instruction (saut de la table)
  • l'utilisation d'un combinaison de si-elses & MSIL "switch" sauts

L'instruction switch n'EST PAS une constante de temps de la branche. Le compilateur peut trouver des raccourcis (à l'aide de hachage seaux, etc), mais les cas les plus compliqués va générer plus compliqué code MSIL avec certains cas, la ramification plus tôt que d'autres.

Pour gérer la Chaîne de cas, le compilateur sera à la fin (à un point) à l'aide d'un.Equals(b) (et, éventuellement, une.GetHashCode() ). Je pense qu'il serait trival pour le compilateur à utiliser n'importe quel objet qui répond à ces contraintes.

Quant à la nécessité pour le cas statique expressions... certaines de ces optimisations (malaxage, la mise en cache, etc) ne serait pas disponible si le cas des expressions n'étaient pas déterministe. Mais nous avons déjà vu que, parfois, le compilateur choisit juste le simpliste if-else-if-else route de toute façon...

Edit: lomaxx - Votre compréhension de la "typeof" l'opérateur n'est pas correct. Le "typeof" opérateur est utilisé pour obtenir le Système.Objet de Type pour un type (rien à voir avec ses aux supertypes ou des interfaces). De vérifier au moment de l'exécution de la compatibilité d'un objet avec un type donné est le "est" de l'opérateur de travail. L'utilisation de "typeof" ici pour exprimer un objet est hors de propos.

10voto

Konrad Rudolph Points 231505

Par la voie, VB, ayant la même architecture sous-jacente, permet beaucoup plus de souplesse Select Case des déclarations (le code ci-dessus serait de travailler en VB) et produit toujours un code efficace, lorsque cela est possible, de sorte que l'argument par la technique de la contrainte doit être soigneusement étudié.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X