Lors de l'écriture d'une instruction switch, il semble y avoir deux limitations sur ce que vous pouvez activer dans les instructions case.
Par exemple (et oui, je sais, si vous faites ce genre de chose, cela signifie probablement que votre architecture orientée objet (OO) est incertaine - ce n'est qu'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 les instructions case échouent avec «Une valeur constante est attendue».
Pourquoi ces restrictions sont-elles en place et quelle est la justification sous-jacente? Je ne vois aucune raison pour laquelle l'instruction switch doit succomber à l'analyse statique uniquement, et pourquoi la valeur activée doit être intégrale (c'est-à-dire primitive). Quelle est la justification?
Réponses:
C'est mon article original, qui a suscité un débat ... car il est faux :
En fait, l'instruction switch C # n'est pas toujours une branche à temps constant.
Dans certains cas, le compilateur utilisera une instruction de commutateur CIL qui est en fait une branche à temps constant utilisant une table de saut. Cependant, dans des cas rares, comme l'a souligné Ivan Hamilton, le compilateur peut générer tout autre chose.
Ceci est en fait assez facile à vérifier en écrivant diverses instructions de commutateur C #, certaines éparses, d'autres denses, et en regardant le CIL résultant avec l'outil ildasm.exe.
la source
switch
instruction (du CIL) qui n'est pas la même que l'switch
instruction de C #.Il est important de ne pas confondre l'instruction de commutateur C # avec l'instruction de commutateur CIL.
Le commutateur CIL est une table de sauts, qui nécessite un index dans un ensemble d'adresses de saut.
Ceci n'est utile que si les cas du commutateur C # sont adjacents:
Mais peu utiles s'ils ne le sont pas:
(Vous auriez besoin d'une table d'environ 3000 entrées en taille, avec seulement 3 emplacements utilisés)
Avec des expressions non adjacentes, le compilateur peut commencer à effectuer des vérifications linéaires if-else-if-else.
Avec des ensembles d'expressions non adjacents plus grands, le compilateur peut commencer par une recherche d'arborescence binaire, et finalement if-else-if-else les derniers éléments.
Avec des ensembles d'expressions contenant des groupes d'éléments adjacents, le compilateur peut rechercher dans l'arborescence binaire, et enfin un commutateur CIL.
C'est plein de "mays" et "mights", et cela dépend du compilateur (peut différer avec Mono ou Rotor).
J'ai répliqué vos résultats sur ma machine en utilisant des cas adjacents:
Ensuite, j'ai également utilisé des expressions de cas non adjacentes:
Ce qui est drôle ici, c'est que la recherche dans l'arbre binaire apparaît un peu (probablement pas statistiquement) plus rapidement que l'instruction de commutation CIL.
Brian, vous avez utilisé le mot « constante », qui a une signification très précise du point de vue de la théorie de la complexité computationnelle. Alors que l'exemple d'entier adjacent simpliste peut produire un CIL considéré comme O (1) (constant), un exemple fragmenté est O (log n) (logarithmique), les exemples groupés se trouvent quelque part entre les deux et les petits exemples sont O (n) (linéaire ).
Cela ne résout même pas la situation de String, dans laquelle une statique
Generic.Dictionary<string,int32>
peut être créée, et subira une surcharge définie lors de la première utilisation. La performance dépendra ici de la performance deGeneric.Dictionary
.Si vous vérifiez la spécification du langage C # (pas la spécification CIL), vous trouverez "15.7.2 L'instruction switch" ne fait aucune mention du "temps constant" ou que l'implémentation sous-jacente utilise même l'instruction switch CIL (soyez très prudent de supposer de telles choses).
À la fin de la journée, un commutateur C # contre une expression entière sur un système moderne est une opération inférieure à la microseconde et ne vaut normalement pas la peine de s'inquiéter.
Bien entendu, ces délais dépendront des machines et des conditions. Je ne ferais pas attention à ces tests de synchronisation, les durées en microsecondes dont nous parlons sont éclipsées par tout code «réel» exécuté (et vous devez inclure du «code réel» sinon le compilateur optimisera la branche), ou gigue dans le système. Mes réponses sont basées sur l'utilisation d' IL DASM pour examiner le CIL créé par le compilateur C #. Bien sûr, ce n'est pas définitif, car les instructions réelles exécutées par le processeur sont ensuite créées par le JIT.
J'ai vérifié les instructions du processeur finales réellement exécutées sur ma machine x86 et je peux confirmer un simple commutateur de jeu adjacent faisant quelque chose comme:
Où une recherche d'arbre binaire est pleine de:
la source
La première raison qui me vient à l'esprit est historique :
Comme la plupart des programmeurs C, C ++ et Java ne sont pas habitués à avoir de telles libertés, ils ne les exigent pas.
Une autre raison, plus valable, est que la complexité de la langue augmenterait :
Tout d'abord, faut-il comparer les objets avec
.Equals()
ou avec l'==
opérateur? Les deux sont valables dans certains cas. Devrions-nous introduire une nouvelle syntaxe pour ce faire? Doit-on permettre au programmeur d'introduire sa propre méthode de comparaison?De plus, permettre d'activer des objets romprait les hypothèses sous-jacentes concernant l'instruction switch . Il existe deux règles régissant l'instruction switch que le compilateur ne pourrait pas appliquer si les objets étaient autorisés à être activés (voir la spécification du langage C # version 3.0 , §8.7.2):
Considérez cet exemple de code dans le cas hypothétique où les valeurs de cas non constantes étaient autorisées:
Que fera le code? Que faire si les instructions de cas sont réorganisées? En effet, l'une des raisons pour lesquelles C # a rendu le basculement de commutateur illégal est que les instructions de commutateur pourraient être arbitrairement réorganisées.
Ces règles sont en place pour une raison - de sorte que le programmeur peut, en regardant un bloc de cas, connaître avec certitude la condition précise dans laquelle le bloc est entré. Lorsque l'instruction switch susmentionnée atteint 100 lignes ou plus (et ce sera le cas), cette connaissance est inestimable.
la source
À propos, VB, ayant la même architecture sous-jacente, permet des
Select Case
instructions beaucoup plus flexibles (le code ci-dessus fonctionnerait en VB) et produit toujours un code efficace là où cela est possible, donc l'argument par contrainte technique doit être examiné attentivement.la source
Select Case
en VB est très flexible et permet de gagner du temps. Ça me manque beaucoup.La plupart du temps, ces restrictions sont en place à cause des concepteurs de langage. La justification sous-jacente peut être la compatibilité avec l'historique de la langue, les idéaux ou la simplification de la conception du compilateur.
Le compilateur peut (et fait) choisir de:
L'instruction switch N'EST PAS une branche à temps constant. Le compilateur peut trouver des raccourcis (en utilisant des seaux de hachage, etc.), mais des cas plus compliqués généreront du code MSIL plus compliqué, certains cas se ramifiant plus tôt que d'autres.
Pour gérer le cas String, le compilateur finira (à un moment donné) en utilisant a.Equals (b) (et éventuellement a.GetHashCode ()). Je pense qu'il serait trival pour le compilateur d'utiliser n'importe quel objet qui satisfait ces contraintes.
Quant au besoin d'expressions de cas statiques ... certaines de ces optimisations (hachage, mise en cache, etc.) ne seraient pas disponibles si les expressions de cas n'étaient pas déterministes. Mais nous avons déjà vu que parfois le compilateur choisit de toute façon la route simpliste if-else-if-else ...
Edit: lomaxx - Votre compréhension de l'opérateur "typeof" n'est pas correcte. L'opérateur "typeof" permet d'obtenir l'objet System.Type pour un type (rien à voir avec ses supertypes ou interfaces). La vérification de la compatibilité d'exécution d'un objet avec un type donné est le travail de l'opérateur «est». L'utilisation de "typeof" ici pour exprimer un objet n'est pas pertinente.
la source
Alors que sur le sujet, selon Jeff Atwood, l'instruction switch est une atrocité de programmation . Utilisez-les avec parcimonie.
Vous pouvez souvent accomplir la même tâche en utilisant une table. Par exemple:
la source
enum
type. Ce n'est pas non plus un hasard si intellisense remplit automatiquement une instruction switch lorsque vous activez une variable d'unenum
type.switch
instruction. Il ne dit pas que vous ne devriez pas écrire de machines à états, mais simplement que vous pouvez faire la même chose en utilisant de jolis types spécifiques. Bien sûr, c'est beaucoup plus facile dans des langages comme F # qui ont des types qui peuvent facilement couvrir des états assez complexes. Pour votre exemple, vous pouvez utiliser des unions discriminées où l'état devient une partie du type et remplacer laswitch
par correspondance de modèle. Ou utilisez des interfaces, par exemple.Dictionary
aurait été considérablement plus lent qu'uneswitch
déclaration optimisée ...?Certes, ce n'est pas obligatoire , et de nombreux langages utilisent en fait des instructions de commutation dynamiques. Cela signifie cependant que réorganiser les clauses "case" peut changer le comportement du code.
Il y a des informations intéressantes derrière les décisions de conception qui sont entrées dans "switch" ici: Pourquoi l'instruction de commutateur C # est-elle conçue pour ne pas autoriser les interruptions, mais nécessite toujours une pause?
Autoriser les expressions de cas dynamiques peut conduire à des monstruosités telles que ce code PHP:
qui devrait franchement simplement utiliser la
if-else
déclaration.la source
Microsoft vous a enfin entendu!
Désormais, avec C # 7, vous pouvez:
la source
Ce n'est pas une raison, mais la section 8.7.2 de la spécification C # indique ce qui suit:
La spécification C # 3.0 se trouve à l' adresse : http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc
la source
La réponse de Juda ci-dessus m'a donné une idée. Vous pouvez "simuler" le comportement du commutateur de l'OP ci-dessus en utilisant un
Dictionary<Type, Func<T>
:Cela vous permet d'associer un comportement à un type dans le même style que l'instruction switch. Je crois qu'il a l'avantage supplémentaire d'être détruit au lieu d'une table de saut de style commutateur lorsqu'il est compilé en IL.
la source
Je suppose qu'il n'y a aucune raison fondamentale pour laquelle le compilateur n'a pas pu traduire automatiquement votre instruction switch en:
Mais il n'y a pas grand-chose à gagner.
Une instruction case sur les types intégraux permet au compilateur de faire un certain nombre d'optimisations:
Il n'y a pas de duplication (sauf si vous dupliquez les étiquettes de cas, que le compilateur détecte). Dans votre exemple, t pourrait correspondre à plusieurs types en raison de l'héritage. Le premier match doit-il être exécuté? Tous?
Le compilateur peut choisir d'implémenter une instruction switch sur un type intégral par une table de saut pour éviter toutes les comparaisons. Si vous activez une énumération qui a des valeurs entières de 0 à 100, il crée un tableau avec 100 pointeurs, un pour chaque instruction switch. Au moment de l'exécution, il recherche simplement l'adresse du tableau en fonction de la valeur entière activée. Cela permet de bien meilleures performances d'exécution que d'effectuer 100 comparaisons.
la source
switch (t) { case typeof(int): ... }
parce que votre traduction implique que la variablet
doit être extraite de la mémoire deux fois sit != typeof(int)
, alors que ce dernier le serait (putativement) lire toujours la valeur d't
une seule fois . Cette différence peut briser l'exactitude du code concurrent qui repose sur ces excellentes garanties. Pour plus d'informations à ce sujet, voir Programmation simultanée deSelon la documentation de l'instruction switch, s'il existe un moyen non ambigu de convertir implicitement l'objet en un type intégral, alors il sera autorisé. Je pense que vous vous attendez à un comportement où, pour chaque déclaration de cas, il serait remplacé par
if (t == typeof(int))
, mais cela ouvrirait toute une boîte de vers lorsque vous surchargeriez cet opérateur. Le comportement changerait lorsque les détails d'implémentation de l'instruction switch changeaient si vous écriviez votre remplacement == de manière incorrecte. En réduisant les comparaisons aux types intégraux et à la chaîne et aux éléments qui peuvent être réduits à des types intégraux (et sont destinés à), ils évitent les problèmes potentiels.la source
Puisque le langage autorise l'utilisation du type de chaîne dans une instruction switch, je suppose que le compilateur est incapable de générer du code pour une implémentation de branche à temps constant pour ce type et doit générer un style if-then.
@mweerden - Ah je vois. Merci.
Je n'ai pas beaucoup d'expérience en C # et .NET mais il semble que les concepteurs de langage n'autorisent pas l'accès statique au système de types sauf dans des circonstances restreintes. Le mot-clé typeof renvoie un objet qui n'est donc accessible qu'au moment de l'exécution.
la source
Je pense que Henk l'a cloué avec le truc "pas d'accès statique au système de type"
Une autre option est qu'il n'y a pas d'ordre dans les types où peuvent être les nombres et les chaînes. Ainsi, un commutateur de type ne peut pas construire un arbre de recherche binaire, juste une recherche linéaire.
la source
Je suis d'accord avec ce commentaire selon lequel il est souvent préférable d'utiliser une approche basée sur une table.
En C # 1.0, cela n'était pas possible car il n'avait pas de génériques et de délégués anonymes. Les nouvelles versions de C # ont l'échafaudage pour que cela fonctionne. Avoir une notation pour les littéraux d'objet est également utile.
la source
Je n'ai pratiquement aucune connaissance de C #, mais je soupçonne que l'un ou l'autre des commutateurs a été simplement pris tel qu'il se produit dans d'autres langages sans penser à le rendre plus général ou le développeur a décidé que l'étendre n'en valait pas la peine.
À proprement parler, vous avez tout à fait raison de dire qu'il n'y a aucune raison de lui imposer ces restrictions. On pourrait soupçonner que la raison en est que pour les cas autorisés, l'implémentation est très efficace (comme suggéré par Brian Ensink ( 44921 )), mais je doute que l'implémentation soit très efficace (avec des instructions if) si j'utilise des entiers et des cas aléatoires (par exemple 345, -4574 et 1234203). Et dans tous les cas, quel mal y a-t-il à l'autoriser pour tout (ou du moins plus) et à dire qu'il n'est efficace que pour des cas spécifiques (comme des nombres (presque) consécutifs).
Je peux cependant imaginer que l'on puisse vouloir exclure des types pour des raisons telles que celle donnée par lomaxx ( 44918 ).
Edit: @Henk ( 44970 ): Si les chaînes sont partagées au maximum, les chaînes de contenu égal seront également des pointeurs vers le même emplacement mémoire. Ensuite, si vous pouvez vous assurer que les chaînes utilisées dans les cas sont stockées consécutivement en mémoire, vous pouvez implémenter très efficacement le commutateur (c'est-à-dire avec une exécution dans l'ordre de 2 compares, une addition et deux sauts).
la source
C # 8 vous permet de résoudre ce problème de manière élégante et compacte en utilisant une expression de commutateur:
En conséquence, vous obtenez:
Vous pouvez en savoir plus sur la nouvelle fonctionnalité ici .
la source