Por que posso atribuir 0,0 aos valores de enumeração, mas não 1,0


90

Só por curiosidade: por que posso atribuir 0,0 a uma variável que é do tipo enumeração, mas não 1,0? Dê uma olhada no seguinte código:

public enum Foo
{
    Bar,
    Baz
}

class Program
{
    static void Main()
    {
        Foo value1 = 0.0;
        Foo value2 = 1.0;   // This line does not compile
        Foo value3 = 4.2;   // This line does not compile
    }
}

Eu pensei que as conversões entre tipos numéricos e valores de enumeração só são permitidas por meio de conversões? Ou seja, eu poderia escrever Foo value2 = (Foo) 1.0;para que a linha 2 Mainpudesse ser compilada. Por que há uma exceção para o valor 0.0em C #?


17
Para mim, é estranho que você possa atribuir o literal duplo 0,0 ao enum personalizado. Não que você não possa atribuir 1.0literal ao enum personalizado.
Ilya Ivanov

2
Suspeito que o compilador o esteja tratando como 0. Eu tive uma pergunta semelhante uma vez e Rawling postou uma ótima resposta aqui .
Amigável de

2
IdeOne não o compila.
Johnny Mopp

Respostas:


98

É um bug que você pode usar 0.0. O compilador trata implicitamente todas as expressões constantes com valor zero como apenas 0.

Agora, é correto para o compilador permitir uma conversão implícita de uma intexpressão constante de 0 para seu enum conforme a seção 6.1.3 da especificação C # 5:

Uma conversão de enumeração implícita permite que o literal inteiro decimal 0 seja convertido em qualquer tipo enum e em qualquer tipo anulável cujo tipo subjacente seja um tipo enum. No último caso, a conversão é avaliada convertendo para o tipo de enum subjacente e envolvendo o resultado (§4.1.10).

Já falei com a equipe C # sobre isso antes: eles gostariam de ter removido a conversão acidental de 0,0 (e de fato 0,0m e 0,0f) para valores enum, mas, infelizmente, deduzi que quebrou muito código - embora isso nunca deveria ter sido permitido em primeiro lugar.

O Mono mcscompilador proíbe todas essas conversões de ponto flutuante, apesar de não permitir que:

const int Zero = 0;
...

SomeEnum x = Zero;

apesar do fato de que Zeroé uma expressão constante, mas não um literal inteiro decimal.

Eu não ficaria surpreso em ver a alteração da especificação C # no futuro para permitir qualquer expressão constante de inteiro com um valor de 0 (ou seja, para imitar mcs), mas eu não esperaria que as conversões de ponto flutuante estivessem oficialmente corretas. (Eu estive errado antes sobre prever o futuro do C #, é claro ...)


3
Per a especificação, ele só está destinado a ser o literal 0. Por isso, deve rejeitar 1-1- uma constante intexpressão com um valor de 0. Mas, como você observa, o compilador está fora de linha com as especificações aqui.
Damien_The_Unbeliever

4
it broke too much code- é realmente difícil imaginar qualquer razão para escrever tal código.
Ilya Ivanov

1
@ObsidianPhoenix: Não tenho certeza do que você quer dizer. É exatamente equivalente a: SomeEnum x = (SomeEnum) 0;. Esse é o caso, haja ou não um valor zero nomeado.
Jon Skeet

2
@ObsidianPhoenix: Bem, não, porque o valor de Test.Fooé 1, não 0 ... de novo, é exatamente o mesmo que se você escrevesse Test v1 = (Test) 0;- e esse comportamento é válido para qualquer valor que não seja um valor nomeado no enum.
Jon Skeet de

2
@JonSkeet vai ser consertado em Roslyn?
Máx.

98

A resposta de Jon está correta. Eu acrescentaria os seguintes pontos.

  • Eu causei esse bug bobo e constrangedor. Muitas desculpas.

  • O bug foi causado por eu não entender a semântica de um predicado "expressão é zero" no compilador; Eu acreditava que ele estava verificando apenas a igualdade de zero inteiro, quando na verdade ele estava verificando mais do que "este é o valor padrão deste tipo?" Na verdade, em uma versão anterior do bug, era possível atribuir o valor padrão de qualquer tipo a um enum! Agora são apenas valores padrão de números. (Lição: Nomeie seus predicados auxiliares com cuidado.)

  • O comportamento que eu estava tentando implementar e que baguncei era, na verdade, uma solução alternativa para um bug ligeiramente diferente. Você pode ler toda a terrível história aqui: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one e https://docs.microsoft .com / en-us / archive / blogs / ericlippert / the-root-of-all-evil-part-two (Lição: É muito fácil introduzir novos bugs piores enquanto corrige os antigos.)

  • A equipe C # decidiu consagrar esse comportamento problemático em vez de corrigi-lo porque o risco de quebrar o código existente sem nenhum benefício atraente era muito alto. (Lição: acertar da primeira vez!)

  • O código que escrevi no Roslyn para preservar esse comportamento pode ser encontrado no método IsConstantNumericZeroem https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - consulte para obter mais detalhes sobre o que exatamente é o comportamento de Roslyn. Escrevi quase todo o código no diretório Conversions; Recomendo que você leia tudo isso, pois há muitos fatos interessantes sobre como o C # diverge da especificação nos comentários. Decorei cada um com SPEC VIOLATION para torná-los fáceis de encontrar.

Mais um ponto de interesse: C # também permite que qualquer valor enum seja usado em um inicializador de enum, independentemente de seu zero:

enum E { A = 1 }
enum F { B = E.A }  // ???

A especificação é um tanto vaga quanto a se isso deve ser legal ou não, mas, novamente, como está no compilador há muito tempo, os novos compiladores provavelmente manterão o comportamento.


10
Isso é muito legal, finalmente consegui ver o código que você escreveu. É incrível que o código-fonte do Roslyn seja open source. Agora eu entendo perfeitamente que existem razões válidas (técnicas / legais) para não fornecer o histórico de alterações, mas teria sido muito bom ver o histórico de alterações para ver como o código evoluiu.
SolutionYogi

The C# team decided to enshrine this buggy behaviour rather than fixing it because the risk of breaking existing code for no compelling benefit was too high.Não acho que haveria muitas pessoas que dependem desse comportamento, e é uma dessas esquisitices que pode ter sido melhor apenas consertar. No entanto, ele realmente não causa muito dano (exceto para projetos que implementam a especificação).
Aidiakapi

5
@Aidiakapi: De fato, o número de pessoas afetadas deve ser pequeno; não é zero. A equipe C # leva as alterações significativas muito a sério. É fácil para você dizer que é melhor fazer a correção; você não precisa lidar com clientes irados que ligam para o seu vice-presidente para reclamar que sua mudança trivial que não adiciona nenhum benefício atrasou a integração do sistema em um dia.
Eric Lippert

3
Fica pior. Todas essas mudanças significativas serão (idealmente) listadas no guia de migração do Framework da Microsoft. Quanto maior for a lista, mais os usuários hesitarão em migrar seus aplicativos. Portanto, mesmo uma pequena alteração de interrupção causa: 1. Um pequeno número de aplicativos interrompidos. 2. Um pequeno número de usuários se recusam a atualizar (mesmo que o problema não os afete). 3. Um pequeno número de usuários para desperdiçar recursos, avaliando se a alteração significativa os afeta. 4. Usuários de 1, 2 e 3 para reclamar com todos os outros.
Brian de

@EricLippert Se "A especificação é um tanto vaga", não faria sentido atualizá-la? (Pergunta genuína!)
Tiago

10

Enumerações em C # são, por definição, valores integrais. Para consistência, C # não deve aceitar nenhuma dessas atribuições, mas 0.0é silenciosamente tratado como integral 0. Isso provavelmente é um resquício de C, onde o literal 0foi tratado de maneira especial e poderia essencialmente receber qualquer tipo dado - inteiro, número de ponto flutuante, ponteiro nulo ... você escolhe.


3
a questão é por quê ? Se você for IL- ele está colocando um valor inteiro na pilhaIL_0001: ldc.i4.0
Ilya Ivanov

@IlyaIvanov Veja atualização. Mas, para ser honesto, a resposta é “nenhuma boa razão”.
Konrad Rudolph

2
Acho que este é um daqueles casos em que se você olhar para a especificação C # , não é legal, mas se você olhar para qualquer compilador C # que a MS produziu, ele faz isso.
Damien_The_Unbeliever

3

enum é realmente pretendido (em todas as linguagens que o suportam) ser uma maneira de trabalhar com strings significativas e exclusivas (rótulos) em vez de valores numéricos. Portanto, em seu exemplo, você só deve usar Bar e Baz ao lidar com um tipo de dados enumerado por Foo . Você nunca deve usar (comparar ou atribuir) um número inteiro, embora muitos compiladores permitam que você se safe (enums são geralmente inteiros internamente) e, neste caso, 0,0 é tratado descuidadamente como 0 pelo compilador.

Conceitualmente, deve ser correto adicionar um número inteiro n a um valor enumerado, para obter n valores mais adiante na linha, ou tomar val2 - val1 para ver a que distância eles estão, mas a menos que a especificação da linguagem permita isso explicitamente, eu evitaria. (Pense em um valor enumerado como sendo um ponteiro C, nas maneiras como você pode usá-lo.) Não há razão para que enums não possam ser implementados com números de ponto flutuante e um incremento fixo entre eles, mas nunca ouvi falar isso sendo feito em qualquer idioma.


Eu sei que não devo usar enums dessa forma em C # - mas eu encontrei este quebra-cabeças e queria saber por que 0.0 está funcionando, mas 1.0 não está. Eu sabia que tinha que ser algo com o compilador C # porque você pode ver que o código IL para Foo v1 = 0.0;é o mesmo que para Foo v2 = Foo.Bar.
feO2x
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.