Ao escrever uma instrução switch, parece haver duas limitações sobre o que você pode ativar nas instruções case.
Por exemplo (e sim, eu sei, se você está fazendo esse tipo de coisa, provavelmente significa que sua arquitetura orientada a objetos (OO) é duvidosa - este é apenas um exemplo artificial!),
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;
}
Aqui, a instrução switch () falha com 'Um valor de um tipo integral esperado' e as instruções case falham com 'Um valor constante é esperado'.
Por que essas restrições estão em vigor e qual é a justificativa subjacente? Não vejo razão para que a instrução switch tenha que sucumbir apenas à análise estática e porque o valor que está sendo ativado precisa ser integral (ou seja, primitivo). Qual é a justificativa?
c#
switch-statement
ljs
fonte
fonte
Respostas:
Este é o meu post original, que provocou algum debate ... porque está errado :
De fato, a instrução C # switch nem sempre é uma ramificação de tempo constante.
Em alguns casos, o compilador usará uma instrução switch CIL que é de fato uma ramificação de tempo constante usando uma tabela de salto. No entanto, em casos esparsos, como apontado por Ivan Hamilton, o compilador pode gerar algo totalmente diferente.
Isso é realmente fácil de verificar escrevendo várias instruções de opção C #, algumas esparsas, outras densas e observando o CIL resultante com a ferramenta ildasm.exe.
fonte
switch
instrução (do CIL) que não é a mesma que aswitch
declaração do C #.É importante não confundir a instrução C # switch com a instrução CIL switch.
O comutador CIL é uma tabela de salto, que requer um índice em um conjunto de endereços de salto.
Isso é útil apenas se os casos do comutador C # forem adjacentes:
Mas de pouca utilidade, se não forem:
(Você precisaria de uma tabela com cerca de 3000 entradas, com apenas 3 slots usados)
Com expressões não adjacentes, o compilador pode começar a executar verificações lineares se-se-se-se.
Com conjuntos de expressões não adjacentes maiores, o compilador pode começar com uma pesquisa em árvore binária e, finalmente, se-outro-se-outro nos últimos itens.
Com conjuntos de expressões que contêm grupos de itens adjacentes, o compilador pode pesquisar em árvore binária e, finalmente, uma opção CIL.
Está cheio de "mays" e "mights" e depende do compilador (pode ser diferente com Mono ou Rotor).
Eu repliquei seus resultados na minha máquina usando casos adjacentes:
Então eu também fiz usando expressões de caso não adjacentes:
O engraçado aqui é que a pesquisa em árvore binária aparece um pouco (provavelmente não estatisticamente) mais rapidamente do que a instrução CIL switch.
Brian, você usou a palavra " constante ", que tem um significado muito definido do ponto de vista da teoria da complexidade computacional. Embora o exemplo inteiro adjacente simplista possa produzir CIL considerado O (1) (constante), um exemplo esparso é O (log n) (logarítmico), exemplos agrupados estão em algum lugar no meio e pequenos exemplos são O (n) (linear). )
Isso nem resolve a situação String, na qual uma estática
Generic.Dictionary<string,int32>
pode ser criada, e sofrerá uma sobrecarga definida no primeiro uso. O desempenho aqui dependerá do desempenho deGeneric.Dictionary
.Se você verificar a especificação de idioma C # (não a especificação CIL), encontrará "15.7.2 A instrução switch" não menciona "tempo constante" ou que a implementação subjacente ainda usa a instrução de opção CIL (tenha muito cuidado em assumir tais coisas).
No final do dia, uma opção C # contra uma expressão inteira em um sistema moderno é uma operação de microssegundos e normalmente não vale a pena se preocupar.
É claro que esses tempos dependerão de máquinas e condições. Eu não prestaria atenção a esses testes de temporização, as durações de microssegundos que estamos falando são diminuídas por qualquer código "real" sendo executado (e você deve incluir algum "código real", caso contrário o compilador otimizará a ramificação), ou instabilidade no sistema. Minhas respostas são baseadas no uso do IL DASM para examinar o CIL criado pelo compilador C #. Obviamente, isso não é final, pois as instruções reais que a CPU executa são criadas pelo JIT.
Verifiquei as instruções finais da CPU realmente executadas na minha máquina x86 e posso confirmar um simples comutador de conjunto adjacente fazendo algo como:
Onde uma pesquisa em árvore binária está cheia de:
fonte
A primeira razão que vem à mente é histórica :
Como a maioria dos programadores em C, C ++ e Java não está acostumada a ter essas liberdades, eles não as exigem.
Outro motivo, mais válido, é que a complexidade do idioma aumentaria :
Antes de tudo, os objetos devem ser comparados com
.Equals()
ou com o==
operador? Ambos são válidos em alguns casos. Devemos introduzir uma nova sintaxe para fazer isso? Devemos permitir que o programador introduza seu próprio método de comparação?Além disso, permitir a ativação de objetos quebraria suposições subjacentes sobre a instrução switch . Há duas regras que governam a instrução switch que o compilador não seria capaz de impor se os objetos pudessem ser ativados (consulte a especificação de linguagem do C # versão 3.0 , §8.7.2):
Considere este exemplo de código no caso hipotético de que valores de caso não constantes foram permitidos:
O que o código fará? E se as declarações de caso forem reordenadas? De fato, uma das razões pelas quais o C # tornou ilegal a passagem do switch é que as instruções do switch podem ser arbitrariamente reorganizadas.
Essas regras existem por um motivo - para que o programador possa, olhando para um bloco de caso, saber com certeza a condição exata sob a qual o bloco é inserido. Quando a instrução switch mencionada cresce em 100 linhas ou mais (e será), esse conhecimento é inestimável.
fonte
A propósito, o VB, com a mesma arquitetura subjacente, permite
Select Case
instruções muito mais flexíveis (o código acima funcionaria no VB) e ainda produz código eficiente onde isso é possível, de modo que o argumento por restrição técnica deve ser considerado com cuidado.fonte
Select Case
en VB é muito flexível e economiza muito tempo. Sinto muita falta disso.Principalmente, essas restrições existem devido a designers de idiomas. A justificativa subjacente pode ser a compatibilidade com o histórico de idiomas, os ideais ou a simplificação do design do compilador.
O compilador pode (e faz) optar por:
A instrução switch NÃO é uma ramificação de tempo constante. O compilador pode encontrar atalhos (usando buckets de hash, etc.), mas casos mais complicados geram código MSIL mais complicado, com alguns casos se ramificando antes que outros.
Para lidar com o caso String, o compilador acabará (em algum momento) usando a.Equals (b) (e possivelmente a.GetHashCode ()). Eu acho que seria um privilégio para o compilador usar qualquer objeto que satisfaça essas restrições.
Quanto à necessidade de expressões estáticas de caso ... algumas dessas otimizações (hash, cache, etc) não estariam disponíveis se as expressões de caso não fossem determinísticas. Mas já vimos que, às vezes, o compilador apenas escolhe a estrada simplista se-senão-se-senão ...
Edit: lomaxx - Seu entendimento do operador "typeof" não está correto. O operador "typeof" é usado para obter o objeto System.Type para um tipo (nada a ver com seus supertipos ou interfaces). Verificar a compatibilidade em tempo de execução de um objeto com um determinado tipo é o trabalho do operador "is". O uso de "typeof" aqui para expressar um objeto é irrelevante.
fonte
Enquanto sobre o assunto, de acordo com Jeff Atwood, a declaração switch é uma atrocidade de programação . Use-os com moderação.
Geralmente, você pode realizar a mesma tarefa usando uma tabela. Por exemplo:
fonte
enum
tipo. Também não é coincidência que o intellisense preencha automaticamente uma instrução switch quando você ativa uma variável de umenum
tipo.switch
declaração. Ele não está dizendo que você não deve escrever máquinas de estado, apenas que você pode fazer a mesma coisa usando tipos específicos agradáveis. Obviamente, isso é muito mais fácil em idiomas como o F #, que possuem tipos que podem facilmente cobrir estados bastante complexos. Para o seu exemplo, você pode usar uniões discriminadas onde o estado se torna parte do tipo e substituir aswitch
correspondência de padrão. Ou use interfaces, por exemplo.Dictionary
teria sido consideravelmente mais lento que umaswitch
declaração otimizada ...?É verdade que não é necessário , e muitos idiomas de fato usam instruções de troca dinâmica. Isso significa, no entanto, que reordenar as cláusulas "case" pode alterar o comportamento do código.
Há algumas informações interessantes por trás das decisões de design que foram incluídas no "switch" aqui: Por que a instrução C # switch foi projetada para não permitir falhas, mas ainda exigir uma pausa?
Permitir expressões dinâmicas de caso pode levar a monstruosidades como este código PHP:
que francamente deveria apenas usar a
if-else
declaração.fonte
A Microsoft finalmente ouviu você!
Agora, com o C # 7, você pode:
fonte
Esse não é o motivo, mas a seção de especificação C # 8.7.2 indica o seguinte:
A especificação do C # 3.0 está localizada em: http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc
fonte
A resposta de Judá acima me deu uma ideia. Você pode "falsificar" o comportamento do comutador do OP acima usando um
Dictionary<Type, Func<T>
:Isso permite associar o comportamento a um tipo no mesmo estilo que a instrução switch. Acredito que tenha o benefício adicional de ser digitado em vez de uma tabela de salto no estilo de chave quando compilada para IL.
fonte
Suponho que não haja uma razão fundamental pela qual o compilador não possa traduzir automaticamente sua instrução switch em:
Mas não há muito ganho com isso.
Uma declaração de caso sobre tipos integrais permite que o compilador faça várias otimizações:
Não há duplicação (a menos que você duplique os rótulos de caso, que o compilador detecta). No seu exemplo, t pode corresponder a vários tipos devido à herança. A primeira partida deve ser executada? Todos eles?
O compilador pode optar por implementar uma instrução switch sobre um tipo integral por uma tabela de salto para evitar todas as comparações. Se você estiver ativando uma enumeração que possua valores inteiros de 0 a 100, ela criará uma matriz com 100 ponteiros, um para cada instrução de chave. No tempo de execução, ele simplesmente procura o endereço da matriz com base no valor inteiro que está sendo ativado. Isso contribui para um desempenho de tempo de execução muito melhor do que realizar 100 comparações.
fonte
switch (t) { case typeof(int): ... }
porque sua tradução implica que a variávelt
deve ser buscada na memória duas vezes set != typeof(int)
, enquanto o último seria (putativamente) sempre leia o valort
exatamente uma vez . Essa diferença pode quebrar a correção do código simultâneo que se baseia nessas excelentes garantias. Para mais informações sobre isso, consulte Joe Duffy Programação Concorrente no WindowsDe acordo com a documentação da instrução switch, se houver uma maneira inequívoca de converter implicitamente o objeto em um tipo integral, será permitido. Eu acho que você está esperando um comportamento em que, para cada instrução de caso, ela seja substituída
if (t == typeof(int))
, mas isso abriria uma lata inteira de worms quando você sobrecarregasse esse operador. O comportamento mudaria quando os detalhes de implementação da instrução switch fossem alterados se você escrevesse sua substituição == incorretamente. Ao reduzir as comparações entre tipos e cadeias integrais e as coisas que podem ser reduzidas a tipos integrais (e se destinam a), eles evitam possíveis problemas.fonte
Como a linguagem permite que o tipo de string seja usado em uma instrução switch, presumo que o compilador não possa gerar código para uma implementação de ramificação de tempo constante para esse tipo e precise gerar um estilo if-then.
@weweden - Ah entendo. Obrigado.
Eu não tenho muita experiência em C # e .NET, mas parece que os designers de linguagem não permitem acesso estático ao sistema de tipos, exceto em circunstâncias restritas. A palavra-chave typeof retorna um objeto para que seja acessível apenas em tempo de execução.
fonte
Eu acho que Henk acertou em cheio com a coisa "sem acesso estático ao sistema de tipos"
Outra opção é que não há ordem para os tipos em que números e seqüências de caracteres podem estar. Portanto, uma opção de tipo não pode construir uma árvore de pesquisa binária, apenas uma pesquisa linear.
fonte
Concordo com este comentário que o uso de uma abordagem orientada a tabelas geralmente é melhor.
No C # 1.0, isso não foi possível porque não havia delegados genéricos e anônimos. Novas versões do C # têm o andaime para fazer isso funcionar. Ter uma notação para literais de objetos também ajuda.
fonte
Não tenho praticamente nenhum conhecimento de c #, mas suspeito que qualquer uma das opções foi simplesmente adotada, como ocorre em outros idiomas, sem pensar em torná-la mais geral ou o desenvolvedor decidiu que estendê-la não valia a pena.
A rigor, você está absolutamente certo de que não há motivos para impor essas restrições. Pode-se suspeitar que a razão é que, para os casos permitidos, a implementação é muito eficiente (como sugerido por Brian Ensink ( 44921 )), mas duvido que a implementação seja muito eficiente (declarações if-wrt) se eu usar números inteiros e alguns casos aleatórios (por exemplo, 345, -4574 e 1234203). E, de qualquer forma, qual é o mal em permitir tudo (ou pelo menos mais) e dizer que só é eficiente para casos específicos (como (quase) números consecutivos).
No entanto, posso imaginar que alguém possa querer excluir tipos devido a razões como a apresentada por lomaxx ( 44918 ).
Edit: @Henk ( 44970 ): Se Strings forem compartilhadas ao máximo, as strings com conteúdo igual também serão ponteiros para o mesmo local de memória. Então, se você puder garantir que as seqüências usadas nos casos sejam armazenadas consecutivamente na memória, poderá implementar o comutador com muita eficiência (ou seja, com execução na ordem de 2 compara, uma adição e dois saltos).
fonte
O C # 8 permite que você resolva esse problema de maneira elegante e compacta usando uma expressão de opção:
Como resultado, você obtém:
Você pode ler mais sobre o novo recurso aqui .
fonte