Para adicionar as respostas aqui, acho que vale a pena considerar a pergunta oposta em conjunto com isso, viz. por que C permitiu a queda em primeiro lugar?
Qualquer linguagem de programação, é claro, serve a dois objetivos:
- Forneça instruções ao computador.
- Deixe um registro das intenções do programador.
A criação de qualquer linguagem de programação é, portanto, um equilíbrio entre a melhor forma de atender a esses dois objetivos. Por um lado, mais fácil é se transformar em instruções de computador (sejam códigos de máquina, códigos de código como IL ou instruções interpretadas na execução), mais capazes de que o processo de compilação ou interpretação seja eficiente, confiável e compacto na saída. Levado ao extremo, esse objetivo resulta em apenas escrever códigos de montagem, IL ou até códigos brutos, porque a compilação mais fácil é onde não há compilação.
Por outro lado, quanto mais a linguagem expressa a intenção do programador, e não os meios adotados para esse fim, mais compreensível é o programa, ao escrever e durante a manutenção.
Agora, switch
sempre poderia ter sido compilado convertendo-o em uma cadeia equivalente de if-else
blocos ou similar, mas foi projetado para permitir a compilação em um padrão de montagem comum em particular em que se obtém um valor, calcula um deslocamento a partir dele (seja consultando uma tabela indexado por um hash perfeito do valor ou por uma aritmética real no valor *). Vale a pena notar que, atualmente, a compilação de C # às vezes se torna switch
equivalente if-else
e às vezes usa uma abordagem de salto baseado em hash (e da mesma forma com C, C ++ e outras linguagens com sintaxe comparável).
Nesse caso, há duas boas razões para permitir falhas:
De qualquer maneira, isso acontece naturalmente: se você criar uma tabela de salto em um conjunto de instruções e um dos lotes anteriores não contiver algum tipo de salto ou retorno, a execução progredirá naturalmente no próximo lote. Permitir falhas era o que "aconteceria" se você transformasse switch
C em uma tabela de salto - usando código de máquina.
Os codificadores que escreveram em assembly já estavam acostumados ao equivalente: ao escrever uma tabela de salto manualmente no assembly, eles teriam que considerar se um determinado bloco de código terminaria com um retorno, um salto fora da tabela ou apenas continuaria. para o próximo bloco. Como tal, fazer com que o codificador adicione um explícito break
quando necessário também era "natural" para o codificador.
Na época, portanto, era uma tentativa razoável de equilibrar os dois objetivos de uma linguagem de computador, pois ela se relaciona ao código de máquina produzido e à expressividade do código-fonte.
Quatro décadas depois, porém, as coisas não são exatamente as mesmas, por algumas razões:
- Atualmente, os codificadores em C podem ter pouca ou nenhuma experiência em montagem. Codificadores em muitas outras linguagens de estilo C são ainda menos propensos a isso (especialmente Javascript!). Qualquer conceito de "com o que as pessoas estão acostumadas a partir da montagem" não é mais relevante.
- Melhorias nas otimizações significam que a probabilidade de
switch
ser transformada if-else
porque foi considerada a abordagem mais eficiente, ou então transformada em uma variante particularmente esotérica da abordagem de salto em mesa é maior. O mapeamento entre as abordagens de nível superior e inferior não é tão forte quanto era antes.
- A experiência demonstrou que o fall-through tende a ser o caso minoritário, e não a norma (um estudo do compilador da Sun descobriu que 3% dos
switch
blocos usavam um fall-through além de vários rótulos no mesmo bloco, e pensava-se que o uso- caso aqui significava que esses 3% eram de fato muito mais altos que o normal). Portanto, a linguagem estudada torna o incomum mais facilmente atendido do que o comum.
- A experiência mostrou que o fall-through tende a ser a fonte de problemas, tanto nos casos em que é feito acidentalmente, quanto nos casos em que o fall-through correto é ignorado por alguém que mantém o código. Essa última é uma adição sutil aos bugs associados ao fall-through, porque mesmo se seu código estiver perfeitamente livre de erros, seu fall-through ainda poderá causar problemas.
Em relação aos dois últimos pontos, considere a seguinte citação da edição atual da K&R:
Passar de um caso para outro não é robusto, sendo propenso à desintegração quando o programa é modificado. Com exceção de vários rótulos para uma única computação, as falhas devem ser usadas com moderação e comentadas.
Por uma questão de boa forma, faça uma pausa após o último caso (o padrão aqui), mesmo que seja logicamente desnecessário. Algum dia, quando outro caso for adicionado no final, esse pouco de programação defensiva o salvará.
Portanto, pela boca do cavalo, a queda em C é problemática. É uma boa prática sempre documentar falhas com comentários, que é uma aplicação do princípio geral de que se deve documentar onde se faz algo incomum, porque é isso que fará o exame posterior do código e / ou fará com que seu código pareça tem um bug de novato quando está de fato correto.
E quando você pensa sobre isso, codifique assim:
switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}
Está adicionando algo para tornar explícito o detalhamento no código, simplesmente não é algo que pode ser detectado (ou cuja ausência pode ser detectada) pelo compilador.
Como tal, o fato de que on tem que ser explícito com fall-through em C # não adiciona nenhuma penalidade às pessoas que escreveram bem em outras linguagens de estilo C de qualquer maneira, pois elas já seriam explícitas em seus fall-throughs. †
Finalmente, o uso goto
aqui já é uma norma do C e de outras linguagens:
switch(x)
{
case 0:
case 1:
case 2:
foo();
goto below_six;
case 3:
bar();
goto below_six;
case 4:
baz();
/* FALLTHRU */
case 5:
below_six:
qux();
break;
default:
quux();
}
Nesse tipo de caso em que queremos que um bloco seja incluído no código executado para um valor diferente daquele que leva um ao bloco anterior, já estamos tendo que usá-lo goto
. (Obviamente, existem meios e maneiras de evitar isso com condicionais diferentes, mas isso é verdade em quase tudo relacionado a essa pergunta). Como tal, o C # se baseou na maneira já normal de lidar com uma situação em que queremos obter mais de um bloco de código em um switch
e apenas generalizá-lo para cobrir falhas. Isso também tornou os dois casos mais convenientes e com autodocumentação, pois precisamos adicionar um novo rótulo em C, mas podemos usá-lo case
como um rótulo em C #. Em C #, podemos nos livrar do below_six
rótulo e usar o goto case 5
que é mais claro quanto ao que estamos fazendo. (Também teríamos que adicionarbreak
para o default
, que deixei de fora apenas para tornar o código C acima claramente não o código C #).
Em resumo, portanto:
- O C # não se relaciona mais à saída não otimizada do compilador, tão diretamente quanto o código C fazia 40 anos atrás (nem o C atualmente), o que torna irrelevante uma das inspirações do fall-through.
- O C # permanece compatível com o C não apenas por ter implícito
break
, para facilitar o aprendizado do idioma por pessoas familiarizadas com idiomas semelhantes e facilitar a portabilidade.
- O C # remove uma possível fonte de erros ou código incompreendido que foi bem documentado como causador de problemas nas últimas quatro décadas.
- O C # torna as práticas recomendadas existentes com C (queda de documentos) aplicáveis pelo compilador.
- O C # torna o caso incomum aquele com código mais explícito, o caso usual aquele com o código que apenas escreve automaticamente.
- O C # usa a mesma
goto
abordagem para atingir o mesmo bloco de case
rótulos diferentes que o usado no C. Ele apenas o generaliza em alguns outros casos.
- O C # torna essa
goto
abordagem baseada em mais conveniente e mais clara do que em C, permitindo que as case
instruções funcionem como rótulos.
Em suma, uma decisão de design bastante razoável
* Algumas formas de BASIC permitiriam fazer coisas GOTO (x AND 7) * 50 + 240
que, embora frágeis e, portanto, um caso particularmente convincente de proibição goto
, servem para mostrar um equivalente em idioma mais alto do tipo de maneira como o código de nível inferior pode dar um salto com base em aritmética sobre um valor, que é muito mais razoável quando é o resultado da compilação, em vez de algo que deve ser mantido manualmente. As implementações do Duff's Device, em particular, se prestam bem ao código de máquina ou IL equivalente, porque cada bloco de instruções geralmente terá o mesmo comprimento sem a necessidade de adição de nop
preenchimentos.
† O dispositivo de Duff aparece aqui novamente, como uma exceção razoável. O fato de que, com esses padrões e similares, há uma repetição de operações, serve para tornar o uso do fall-through relativamente claro, mesmo sem um comentário explícito nesse sentido.