Étant donné une simple déclaration de commutateur
switch (int)
{
case 1 :
{
printf("1\n");
break;
}
case 2 :
{
printf("2\n");
}
case 3 :
{
printf("3\n");
}
}
L'absence d'instruction break dans le cas 2 implique que l'exécution se poursuivra dans le code pour le cas 3. Ce n'est pas un accident; il a été conçu de cette façon. Pourquoi cette décision a-t-elle été prise? Quel avantage cela apporte-t-il par rapport à une sémantique de rupture automatique pour les blocs? Quelle était la raison?
Beaucoup de réponses semblent se focaliser sur la possibilité de tomber comme raison pour avoir besoin de l'instruction break
.
Je pense que c'était simplement une erreur, principalement en raison du fait que, lors de la conception de C, l'expérience de la manière dont ces constructions seraient utilisées était bien moins connue.
Peter Van der Linden en parle dans son livre "Expert C Programming":
Nous avons analysé les sources du compilateur Sun C pour voir à quelle fréquence la chute par défaut était utilisée. Le frontal du compilateur Sun ANSI C comporte 244 instructions de commutateur, chacune d’entre elles ayant en moyenne sept observations. La chute survient dans seulement 3% de ces cas.
En d'autres termes, le comportement normal d'un commutateur est mauvais 97% du temps. Ce n'est pas seulement dans un compilateur - au contraire, lorsque l'analyse a été utilisée dans cette analyse, c'était souvent pour des situations qui se produisent plus fréquemment dans un compilateur que dans d'autres logiciels, par exemple, lors de la compilation d'opérateurs pouvant avoir un ou deux opérandes. :
switch (operator->num_of_operands) { case 2: process_operand( operator->operand_2); /* FALLTHRU */ case 1: process_operand( operator->operand_1); break; }
La perte de cas est si largement reconnue comme un défaut qu’il existe même une convention de commentaire spéciale, illustrée ci-dessus, qui indique que "c’est vraiment un cas parmi ces 3% des cas où une chute était désirée".
Je pense que c'était une bonne idée pour C # d'exiger une instruction de saut explicite à la fin de chaque bloc de cas (tout en permettant d'empiler plusieurs libellés de cas - tant qu'il n'y a qu'un seul bloc d'instructions). En C #, vous pouvez toujours faire passer un cas à un autre - il vous suffit de rendre explicite le passage à travers en passant au cas suivant en utilisant un goto
.
C’est dommage Java n’a pas profité de l’occasion pour rompre avec la sémantique C.
À bien des égards, c n’est qu’une interface épurée avec les idiomes standard d’Assemblée. Lors de l'écriture d'un contrôle de flux piloté par une table de sauts, le programmeur a le choix de sauter ou de sauter de la "structure de contrôle", et un saut nécessite une instruction explicite.
Alors, c fait la même chose ...
Pour implémenter le périphérique de Duff, évidemment:
dsend(to, from, count)
char *to, *from;
int count;
{
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
Si les cas étaient conçus pour être résolus implicitement, vous ne pourriez pas avoir d'échec.
case 0:
case 1:
case 2:
// all do the same thing.
break;
case 3:
case 4:
// do something different.
break;
default:
// something else entirely.
Si le commutateur était conçu pour se déclencher implicitement après chaque cas, vous n’auriez pas le choix. La structure d'interrupteur a été conçue pour être plus flexible.
Les instructions case dans une instruction switch sont simplement des étiquettes.
Lorsque vous activez une valeur, l’instruction switch applique essentiellement goto à l’étiquette avec la valeur correspondante.
Cela signifie que la pause est nécessaire pour éviter de passer au code sous la prochaine étiquette.
Pour ce qui est de la raison pourquoi elle a été mise en œuvre de cette façon - la nature indirecte d’une instruction switch peut être utile dans certains scénarios. Par exemple:
case optionA:
// optionA needs to do its own thing, and also B's thing.
// Fall-through to optionB afterwards.
// Its behaviour is a superset of B's.
case optionB:
// optionB needs to do its own thing
// Its behaviour is a subset of A's.
break;
case optionC:
// optionC is quite independent so it does its own thing.
break;
Pour permettre des choses comme:
switch(foo) {
case 1:
/* stuff for case 1 only */
if (0) {
case 2:
/* stuff for case 2 only */
}
/* stuff for cases 1 and 2 */
case 3:
/* stuff for cases 1, 2, and 3 */
}
Pensez au mot-clé case
comme une étiquette goto
et cela vient beaucoup plus naturellement.
Il élimine la duplication de code lorsque plusieurs cas doivent exécuter le même code (ou le même code en séquence).
Comme au niveau de la langue d'assemblage, peu importe que vous fassiez une rupture ou non, il n'y a aucun frais généraux pour les cas de chute, alors pourquoi ne pas les autoriser, car ils offrent des avantages significatifs dans certains cas.
J'ai eu l'occasion d'assister à un cas d'attribution de valeurs dans des vecteurs à des structures: il fallait le faire de telle sorte que si le vecteur de données était plus court que le nombre de données membres de la structure, le reste des membres resterait dans leur valeur par défaut. Dans ce cas, omettre break
était très utile.
switch (nShorts)
{
case 4: frame.leadV1 = shortArray[3];
case 3: frame.leadIII = shortArray[2];
case 2: frame.leadII = shortArray[1];
case 1: frame.leadI = shortArray[0]; break;
default: TS_ASSERT(false);
}
Comme beaucoup d’entre eux l’ont précisé, c’est pour permettre à un seul bloc de code de fonctionner dans plusieurs cas. Ceci devrait est une occurrence plus courante pour vos instructions switch que le "bloc de code par cas" que vous spécifiez dans votre exemple.
Si vous avez un bloc de code par cas sans perte, vous devriez peut-être envisager d'utiliser un bloc if-elseif-else, car cela semblerait plus approprié.