Question

Lors de l'écriture d'une instruction switch, il semble y avoir deux limitations sur ce que vous pouvez allumer 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 en place, et quelle est la justification sous-jacente?Je ne vois pas pourquoi l'instruction switch a succomber à l'analyse statique seulement, et pourquoi la valeur sous tension doit être intégrale (qui est primitif).Quelle est la justification?

Était-ce utile?

La solution

C'est mon premier post, ce qui a suscité un certain débat... parce que 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 l'a souligné 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.

Autres conseils

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" & "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 "constant"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 Generic.Dictionary.

Si vous cochez l' Spécification Du Langage C# (pas 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.


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

J'ai vérifié la finale instructions du PROCESSEUR exécute en réalité sur ma machine x86, et peut confirmer une simple adjacentes placer le commutateur 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

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 l' complexité du langage permettrait d'augmenter:

Tout d'abord, les objets en comparaison avec .Equals() ou avec le == 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 casser 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 constant
  • 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.

Par la voie, VB, ayant la même architecture sous-jacente, permet beaucoup plus de souplesse Select Case états (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é.

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

Alors que sur le sujet, selon Jeff Atwood, l'instruction switch est une programmation atrocité.Utilisez-les avec parcimonie.

Bien souvent, vous pouvez accomplir la même tâche à l'aide d'un tableau.Par exemple:

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

Je ne vois pas pourquoi l'instruction switch est à craquez pour l'analyse statique uniquement

Vrai, il n'a pas ont à, et de nombreuses langues, en fait, l'utilisation dynamique des instructions de commutation.Cela signifie, cependant, que la réorganisation de l ' "affaire" des clauses peut changer le comportement du code.

Il y a quelques infos intéressantes derrière les décisions de conception qui est entré dans "switch" ici: Pourquoi le C# instruction switch conçu pour ne pas permettre à l'automne, mais ont encore besoin d'une pause?

Permettant de cas dynamique expressions peuvent conduire à des monstruosités comme ce code PHP:

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

qui, franchement, faut juste utiliser le if-else l'énoncé.

Microsoft a finalement entendu vous!

Maintenant avec C# 7, vous pouvez:

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

Ce n'est pas une raison, mais la spécification C# section 8.7.2 les états suivants:

Le conseil type de commutateur de la déclaration est établie par le commutateur d'expression.Si le type de l'interrupteur d'expression est sbyte, byte, short, ushort, int, uint, long, ulong, char, string, ou un enum type, alors que c'est le conseil le type de l'instruction switch.Sinon, un définis par l'utilisateur de la conversion implicite (§6.4) doit exister à partir du type de l'interrupteur de l'expression de l'une des opérations suivantes possible régissant types:sbyte, byte, short, ushort, int, uint, long, ulong, char, string.Si aucune conversion implicite existe, ou si plus d'une conversion implicite existe, une erreur de compilation se produit.

Le C# 3.0 spécification est situé à:http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc

Juda est la réponse ci-dessus m'a donné une idée.Vous pouvez "de faux" les OP le commutateur de comportement ci-dessus à l'aide d'un 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());
                    });

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 d'être assortie à la place d'un interrupteur de style de sauter de la table lors de la compilation à l'IL.

Je suppose qu'il n'y a pas de raison fondamentale pour que le compilateur ne pouvais pas traduire automatiquement votre commutateur énoncé:

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

Mais il n'y a pas beaucoup gagné par qui.

Une instruction de cas sur les types intégraux permet au compilateur de faire un certain nombre d'optimisations:

  1. Il n'y a pas de duplication (sauf si vous dupliquez les cas, les étiquettes, le compilateur détecte).Dans votre exemple, t pourrait correspondre à plusieurs types, en raison de l'héritage.Si le premier match être exécuté?Chacun d'eux?

  2. Le compilateur peut choisir de mettre en œuvre une instruction switch sur un type intégral par un saut de la table pour éviter toutes les comparaisons.Si vous passez sur une énumération qui a des valeurs entières de 0 à 100, puis il crée un tableau avec 100 pointeurs, l'un pour chaque instruction switch.À l'exécution, il regarde simplement l'adresse du tableau basé sur la valeur de l'entier soit allumé.Cela rend beaucoup mieux les performances d'exécution que l'exécution de 100 comparaisons.

Selon l'instruction switch documentation si il y a une façon non ambiguë pour convertir implicitement l'objet à un type intégral, alors il sera permis.Je pense que vous vous attendez à un comportement où pour chaque instruction de cas, il serait remplacé par if (t == typeof(int)), mais qui permettrait d'ouvrir un ensemble de vers quand vous arrivez à la surcharge de l'opérateur.Le comportement serait de changer lorsque les détails de l'implémentation de l'instruction switch changé, si vous avez écrit votre == remplacer de manière incorrecte.En réduisant les comparaisons de types intégraux et de la ficelle, et les choses qui peuvent être réduits à des types intégraux (et sont destinées à) ils éviter d'éventuels problèmes.

a écrit:

"L'instruction switch ne une constante de temps de la branche, indépendamment de la façon dont de nombreux cas que vous avez."

Étant donné que la langue permet à l' chaîne type à être utilisé dans une instruction switch je suppose que le compilateur ne peut pas générer de code pour une constante de temps de la succursale de la mise en œuvre de ce type et les besoins de générer un " si-alors style.

@mweerden - Ah, je vois.Merci.

Je n'ai pas beaucoup d'expérience en C# et .NET, mais il semble que la langue, les concepteurs ne permettent pas statique d'accès pour le système de type, sauf dans des circonstances limitées.L' typeof mot-clé renvoie un objet c'est donc accessibles au moment de l'exécution seulement.

Je pense que Henk cloué avec la "aucun sttatic l'accès au système de type" chose

Une autre option est qu'il n'y a pas d'ordre à des types comme des nombres et chaînes de caractères peuvent être.Ainsi, un type de commutateur ne peut pas construire un arbre de recherche binaire, juste une recherche linéaire.

Je suis d'accord avec ce commentaire que l'utilisation d'une table par l'approche est souvent préférable.

En C# 1.0 ce n'était pas possible car il n'avait pas de génériques et de délégués anonymes.De nouvelles versions de C# et de l'échafaudage pour faire ce travail.Ayant une notation pour les littéraux d'objet est également utile.

Je n'ai pratiquement aucune connaissance de C#, mais je soupçonne que ce soit le commutateur a été tout simplement pris comme cela se produit dans d'autres langues, sans penser à le rendre plus général ou le développeur a décidé que l'extension elle n'était pas la peine.

Strictement parlant, vous avez absolument raison, il n'y a pas de raison de mettre ces restrictions.On pourrait croire que la raison est que pour les cas autorisés de la mise en œuvre est très efficace (comme suggéré par Brian Ensink (44921)), mais je doute que la mise en œuvre est très efficace (w.r.t.si-états) si j'utilise des nombres entiers et du hasard cas (par ex.345, -4574 et 1234203).Et dans tous les cas, qu'est-ce que le mal en lui permettant de tout (ou au moins plus) et en disant que c'est seulement efficace pour certains cas (comme (presque) numéros consécutifs).

Par contre, je peux imaginer que l'on pourrait vouloir exclure les types, pour des raisons telles que celle donnée par lomaxx (44918).

Edit:@Henk (44970):Si les Chaînes sont au maximum partagé, chaînes à contenu égal sera pointeurs vers le même emplacement de mémoire ainsi.Alors, si vous pouvez assurez-vous que les chaînes de caractères utilisées dans le cas sont stockés de manière consécutive dans la mémoire, vous pouvez de manière très efficace de mettre en œuvre l'interrupteur (c'est à direavec l'exécution de l'ordre de 2 compare, d'un ajout et deux sauts).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top