Qui a décidé (et sur la base de quels concepts) que switch
construction (dans de nombreuses langues) devait utiliser break
dans chaque instruction?
Pourquoi devons-nous écrire quelque chose comme ceci:
switch(a)
{
case 1:
result = 'one';
break;
case 2:
result = 'two';
break;
default:
result = 'not determined';
break;
}
(remarqué cela dans PHP et JS; il y a probablement beaucoup d'autres langages qui l'utilisent)
Si switch
est une alternative à if
, pourquoi ne pouvons-nous pas utiliser la même construction que pour if
? C'est à dire.:
switch(a)
{
case 1:
{
result = 'one';
}
case 2:
{
result = 'two';
}
default:
{
result = 'not determined';
}
}
On dit que break
empêche l'exécution du bloc suivant le bloc courant. Mais, quelqu'un se heurte-t-il vraiment à la situation, où il était nécessaire d'exécuter le bloc actuel et les suivants? Non. Pour moi, break
est toujours là. Dans chaque bloc. Dans chaque code.
C a été l'une des premières langues à avoir l'instruction switch
sous cette forme, et toutes les autres langues principales en ont hérité, choisissant principalement de conserver la sémantique C par défaut - elles non plus n'ont pas pensé aux avantages de les changer, ou les jugeait moins importants que de garder le comportement auquel tout le monde était habitué.
Quant à savoir pourquoi C a été conçu de cette façon, il découle probablement du concept de C comme "assemblage portable". L'instruction switch
est essentiellement une abstraction de table de branche , et une table de branche a également un passage implicite et nécessite une instruction de saut supplémentaire pour l'éviter.
Donc, fondamentalement, les concepteurs de C ont également choisi de conserver la sémantique de l'assembleur par défaut.
Parce que switch
n'est pas une alternative à if ... else
déclarations dans ces langues.
En utilisant switch
, nous pouvons faire correspondre plusieurs conditions à la fois, ce qui est très apprécié dans certains cas.
Exemple:
public Season SeasonFromMonth(Month month)
{
Season season;
switch (month)
{
case Month.December:
case Month.January:
case Month.February:
season = Season.Winter;
break;
case Month.March:
case Month.April:
case Month.May:
season = Season.Spring;
break;
case Month.June:
case Month.July:
case Month.August:
season = Season.Summer;
break;
default:
season = Season.Autumn;
break;
}
return season;
}
Cela a été demandé sur Stack Overflow dans le contexte de C: Pourquoi l'instruction switch a-t-elle été conçue pour avoir besoin d'une pause?
Pour résumer la réponse acceptée, c'était probablement une erreur. La plupart des autres langages viennent probablement de suivre C. Cependant, certains langages comme C # semblent avoir corrigé cela en autorisant les retombées - mais uniquement lorsque le programmeur le dit explicitement (source: le lien ci-dessus, je ne parle pas moi-même C #) .
Je répondrai par un exemple. Si vous souhaitez lister le nombre de jours pour chaque mois d'une année, il est évident que certains mois en ont 31, 30 et 1 28/29. Cela ressemblerait à ça,
switch(month) {
case 4:
case 6:
case 9:
case 11;
days = 30;
break;
case 2:
//Find out if is leap year( divisible by 4 and all that other stuff)
days = 28 or 29;
break;
default:
days = 31;
}
Il s'agit d'un exemple où plusieurs cas ont le même effet et sont tous regroupés. Il y avait évidemment une raison pour le choix du mot-clé break et non la construction if ... else if.
La principale chose à noter ici est qu'une instruction switch avec de nombreux cas similaires n'est pas un if ... else if ... else if ... else for each des cas, mais plutôt si (1, 2, 3) ... sinon si (4,5,6) sinon ...
Il y a deux situations où une "chute" d'un cas à l'autre peut se produire - le cas vide:
switch ( ... )
{
case 1:
case 2:
do_something_for_1_and_2();
break;
...
}
et le cas non vide
switch ( ... )
{
case 1:
do_something_for_1();
/* Deliberately fall through */
case 2:
do_something_for_1_and_2();
break;
...
}
Malgré la référence à Duff's Device , les instances légitimes pour le deuxième cas sont rares et généralement interdites par les normes de codage et signalées lors de l'analyse statique. Et où il se trouve, il est le plus souvent dû à l'omission d'un break
.
Le premier est parfaitement sensé et commun.
Pour être honnête, je ne vois aucune raison d'avoir besoin du break
et de l'analyseur de langage sachant qu'un corps de cas vide est une chute, tandis qu'un cas non vide est autonome.
Il est dommage que le panel ISO C semble plus préoccupé d'ajouter de nouvelles fonctionnalités (indésirables) et mal définies au langage, plutôt que de corriger les fonctionnalités non définies, non spécifiées ou définies par l'implémentation, sans parler de l'illogique.
Ne pas forcer le break
permet un certain nombre de choses qui pourraient autrement être difficiles à faire. D'autres ont relevé des cas de regroupement, pour lesquels il existe un certain nombre de cas non triviaux.
Un cas où il est impératif que le break
ne soit pas utilisé est Duff's Device . Ceci est utilisé pour " dérouler " les boucles où il peut accélérer les opérations en limitant le nombre de comparaisons requises. Je crois que l'utilisation initiale a permis une fonctionnalité qui était auparavant trop lente avec une boucle entièrement enroulée. Il échange la taille du code contre la vitesse dans certains cas.
Il est recommandé de remplacer le break
par un commentaire approprié, si le cas contient du code. Sinon, quelqu'un corrigera le break
manquant et introduira un bogue.
En C, où l'Origine semble être, le bloc de code de l'instruction switch
n'est pas une construction spéciale. Il s'agit d'un bloc de code normal, tout comme un bloc sous une instruction if
.
switch ()
{
}
if ()
{
}
case
et default
sont des étiquettes de saut à l'intérieur de ce bloc, spécifiquement liées à switch
. Ils sont traités comme des étiquettes de saut normales pour goto
. Il y a une règle spécifique qui est importante ici: les étiquettes de saut peuvent être presque partout dans le code, sans interrompre le flux de code.
En tant que bloc de code normal, il n'a pas besoin d'être une instruction composée. Les étiquettes sont également facultatives. Ce sont des instructions switch
valides en C:
switch (a)
case 1: Foo();
switch (a)
Foo();
switch (a)
{
Foo();
}
La norme C elle-même en donne un exemple (6.8.4.2):
switch (expr)
{
int i = 4;
f(i);
case 0:
i=17;
/*falls through into default code */
default:
printf("%d\n", i);
}
Dans le fragment de programme artificiel, l'objet dont l'identifiant est i existe avec une durée de stockage automatique (dans le bloc) mais n'est jamais initialisé, et donc si l'expression de contrôle a une valeur non nulle, l'appel à la fonction printf accède à une valeur indéterminée. De même, l'appel à la fonction f ne peut pas être atteint.
De plus, default
est aussi une étiquette de saut, et peut donc être n'importe où, sans avoir besoin d'être le dernier cas.
Cela explique également l'appareil de Duff:
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);
}
Pourquoi l'échec? Parce que dans le flux de code normal dans un bloc de code normal, le passage à l'instruction suivante est attend, tout comme vous vous y attendriez dans un bloc de code if
.
if (a == b)
{
Foo();
/* "Fall-through" to Bar expected here. */
Bar();
}
switch (a)
{
case 1:
Foo();
/* Automatic break would violate expected code execution semantics. */
case 2:
Bar();
}
Je suppose que la raison en est la facilité de mise en œuvre. Vous n'avez pas besoin de code spécial pour analyser et compiler un bloc switch
, en prenant soin des règles spéciales. Il suffit de l'analyser comme n'importe quel autre code et de ne prendre soin que des étiquettes et de la sélection de saut.
Une question de suivi intéressante de tout cela est si les instructions imbriquées suivantes affichent "Terminé". ou pas.
int a = 10;
switch (a)
{
switch (a)
{
case 10: printf("Done.\n");
}
}
La norme C s'en occupe (6.8.4.2.4):
Un cas ou une étiquette par défaut est accessible uniquement dans l'instruction de commutateur englobante la plus proche.
Pour répondre à deux de vos questions.
Pourquoi C a-t-il besoin de pauses?
Il revient aux racines Cs comme un "assembleur portable". Où le code psudo comme celui-ci était courant: -
targets=(addr1,addr2,addr3);
opt = 1 ## or 0 or 2
switch:
br targets[opt] ## go to addr2
addr1:
do 1stuff
br switchend
addr2:
do 2stuff
br switchend
addr3
do 3stuff
switchend:
......
l'instruction switch a été conçue pour fournir des fonctionnalités similaires à un niveau supérieur.
Avons-nous jamais des commutateurs sans interruption?
Oui, c'est assez courant et il y a quelques cas d'utilisation;
Tout d'abord, vous souhaiterez peut-être effectuer la même action pour plusieurs cas. Pour ce faire, nous empilons les étuis les uns sur les autres:
case 6:
case 9:
// six or nine code
Un autre cas d'utilisation courant dans les machines à états est qu'après avoir traité un état, nous voulons immédiatement entrer et traiter un autre état:
case 9:
crashed()
newstate=10;
case 10:
claim_damage();
break;
Plusieurs personnes ont déjà évoqué la notion d'appariement de plusieurs conditions, qui est très précieuse de temps en temps. Cependant, la possibilité de faire correspondre plusieurs conditions ne nécessite pas nécessairement que vous fassiez exactement la même chose avec chaque condition qui correspond. Considérer ce qui suit:
switch (someCase)
{
case 1:
case 2:
doSomething1And2();
break;
case 3:
doSomething3();
case 4:
doSomething3And4();
break;
default:
throw new Error("Invalid Case");
}
Il existe deux manières différentes de faire correspondre des ensembles de conditions multiples. Avec les conditions 1 et 2, ils tombent simplement dans la même parcelle de code exacte et font exactement la même chose. Avec les conditions 3 et 4, cependant, bien qu'elles se terminent toutes les deux par l'appel de doSomething3And4()
, niquement 3 appels doSomething3()
.