Почему я могу присвоить значениям перечисления 0,0, а не 1,0


90

Просто из любопытства: почему я могу присвоить переменной перечислимого типа 0,0, а не 1,0? Взгляните на следующий код:

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
    }
}

Я думал, что преобразования между числовыми типами и значениями перечисления разрешены только через приведения? То есть я мог бы написать Foo, value2 = (Foo) 1.0;чтобы Mainможно было скомпилировать строку 2 . Почему 0.0в C # есть исключение для значения ?


17
Для меня странно, что пользовательскому enum можно присвоить двойной литерал 0.0. Не то чтобы вы не могли назначить 1.0литерал настраиваемому перечислению.
— Илья Иванов

2
Я подозреваю, что компилятор рассматривает это как 0. У меня был подобный вопрос раз и Ролинг разместил большой ответ здесь .
— Amicable

2
IdeOne не компилирует его.
— Джонни Мопп

Ответы:


98

Это ошибка, которую вы можете использовать в 0.0. Компилятор неявно обрабатывает все константные выражения с нулевым значением как просто 0.

Теперь, это правильно для компилятора , чтобы неявное преобразование из постоянного intвыражения 0 до вашего перечисления согласно разделу 6.1.3 C # 5 спецификации:

Неявное преобразование перечисления позволяет преобразовать десятично-целочисленный литерал 0 в любой перечисляемый тип и в любой обнуляемый тип, базовым типом которого является перечисляемый тип. В последнем случае преобразование оценивается путем преобразования в базовый enum-type и упаковки результата (§4.1.10).

Я уже говорил об этом с командой C # раньше: они хотели бы удалить случайное преобразование из 0,0 (и действительно 0,0 м и 0,0f) в значения перечисления, но, к сожалению, я понимаю, что это сломало слишком много кода - хотя это вообще никогда не должно было быть разрешено.

Mono mcsкомпилятор запрещает все эти преобразования с плавающей точкой, хотя это позволит:

const int Zero = 0;
...

SomeEnum x = Zero;

несмотря на то, что Zeroэто постоянное выражение, а не десятично-целочисленный литерал.

Я не удивлюсь, если в будущем спецификация C # изменится, чтобы разрешить любое целочисленное постоянное выражение со значением 0 (то есть имитировать mcs), но я не ожидал бы, что преобразования с плавающей запятой когда-либо будут официально правильными. (Я, конечно, ошибался, предсказывая будущее C # ...)


3
Согласно спецификации, это должно быть только буквальное значение 0. Поэтому оно должно отклонить 1-1- постоянное intвыражение со значением 0. Но, как вы заметили, компилятор не соответствует спецификации.
— Damien_The_Unbeliever

4
it broke too much code- действительно сложно представить какие-либо причины для написания такого кода.
— Илья Иванов

1
@ObsidianPhoenix: Я не понимаю, о чем вы. Это точно эквивалентно: SomeEnum x = (SomeEnum) 0;. Это тот случай, есть ли указанное нулевое значение или нет.
— Джон Скит,

2
@ObsidianPhoenix: Ну нет, потому что значение Test.Fooравно 1, а не 0 ... опять же, это точно так же, как если бы вы написали Test v1 = (Test) 0;- и такое поведение сохраняется для любого значения, которое не является именованным значением в перечислении.
— Джон Скит

2
@JonSkeet, это будет исправлено в Roslyn?
— Макс

98

Ответ Джона правильный. Я бы добавил к этому следующие моменты.

  • Я вызвал эту глупую и неприятную ошибку. Много извинений.

  • Ошибка была вызвана тем, что я неправильно понял семантику предиката "выражение равно нулю" в компиляторе; Я полагал, что он проверял только целочисленное равенство нулю, тогда как на самом деле он проверял больше по строкам «это значение по умолчанию для этого типа?» Фактически, в более ранней версии ошибки действительно можно было присвоить перечислению значение по умолчанию любого типа! Теперь это только значения чисел по умолчанию. (Урок: внимательно назовите свои вспомогательные предикаты.)

  • Поведение, которое я пытался реализовать, которое я испортил, на самом деле было обходным путем для немного другой ошибки. Вы можете прочитать всю ужасную историю здесь: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one и https://docs.microsoft .com / en-us / archive / blogs / ericlippert / the-root-of-all-evil-part-two (Урок: очень легко вводить новые худшие ошибки, исправляя старые).

  • Команда C # решила закрепить это ошибочное поведение, а не исправлять его, потому что риск взлома существующего кода без существенной выгоды был слишком высок. (Урок: сделай это правильно с первого раза!)

  • Код , который я написал в Рослин , чтобы сохранить это поведение можно найти в методе IsConstantNumericZeroв https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - см. дополнительные сведения о поведении Roslyn. Я написал почти весь код в каталоге Conversions; Я рекомендую вам прочитать все это, поскольку в комментариях есть много интересных фактов о том, как C # отличается от спецификации. Я украсил каждую НАРУШЕНИЕМ СПЕЦИФИКАЦИИ, чтобы их было легко найти.

Еще один интересный момент: C # также позволяет использовать любое значение перечисления в инициализаторе перечисления независимо от его нулевой точки:

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

Спецификация несколько расплывчата относительно того, должно ли это быть законным или нет, но, опять же, поскольку это было в компиляторе в течение длительного времени, новые компиляторы, вероятно, сохранят такое поведение.


10
Это действительно здорово, я наконец-то смог увидеть написанный вами код. Замечательно, что исходный код Roslyn открыт. Теперь я полностью понимаю, что существуют веские причины (технические / юридические) не предоставлять историю изменений, но было бы очень здорово увидеть историю изменений, чтобы увидеть, как развивался код.
— 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.Я не думаю, что будет много людей, которые полагаются на такое поведение, и это одна из тех странностей, которую, возможно, было бы лучше просто исправить. Однако особого вреда это тоже не приносит (кроме проектов, реализующих спецификацию).
— Aidiakapi

5
@Aidiakapi: Действительно, число пострадавших должно быть небольшим; это не ноль. Команда C # очень серьезно относится к критическим изменениям. Вам легко сказать, что лучше исправить; вам не нужно иметь дело с разгневанными клиентами, которые звонят вашему вице-президенту и жалуются, что ваше банальное изменение, не приносящее никакой пользы, задержало их системную интеграцию на день.
— Эрик Липперт

3
Становится хуже. Все такие критические изменения (в идеале) должны быть перечислены в руководстве по миграции Microsoft Framework. Чем длиннее этот список, тем больше пользователей не решаются перенести свое приложение. Итак, даже незначительное критическое изменение вызывает: 1. Сбои в работе небольшого количества приложений. 2. Небольшое количество пользователей отказываются от обновления (даже если проблема их не затрагивает). 3. Небольшое количество пользователей тратить ресурсы на оценку того, затрагивает ли их критическое изменение. 4. Пользователи №1, №2 и №3 должны жаловаться всем остальным.
— Брайан

@EricLippert Если «Спецификация несколько расплывчата», не имеет ли смысла обновлять спецификацию? (Подлинный вопрос!)
— Джеймс

10

Перечисления в C # по определению являются целыми значениями. Для согласованности C # не должен принимать ни одно из этих назначений, но 0.0он молча считается интегральным 0. Вероятно, это пережиток C, где литерал 0обрабатывался специально и мог по существу принимать любой заданный тип - целое число, число с плавающей запятой, нулевой указатель… вы называете это.


3
вопрос почему ? Если вы перейдете в IL- он помещает целое значение в стекIL_0001: ldc.i4.0
— Илья Иванов

@IlyaIvanov Смотрите обновление. Но, честно говоря, ответ - «без уважительной причины».
— Конрад Рудольф

2
Я думаю, что это один из тех случаев, когда если вы посмотрите на спецификацию C # , это не законно, но если вы посмотрите на любой компилятор C #, созданный MS, он делает это.
— Damien_The_Unbeliever

3

enum действительно предназначен (на всех поддерживающих его языках) как способ работы с осмысленными и уникальными строками (метками), а не с числовыми значениями. Таким образом, в вашем примере вы должны использовать только Bar и Baz при работе с перечисляемым типом данных Foo . Вы никогда не должны использовать (сравнивать или присваивать) целое число, даже если многие компиляторы позволят вам уйти от этого (перечисления обычно являются целыми числами внутри), и в этом случае 0,0 неосторожно обрабатывается компилятором как 0.

По идее, должно быть нормально добавить целое число n к перечисляемому значению, чтобы получить n значений дальше по строке, или взять val2 - val1, чтобы увидеть, насколько они удалены друг от друга, но если спецификация языка явно не разрешает это, я бы избежать этого. (Думайте о перечисляемом значении как о указателе C в том, как вы можете его использовать.) Нет причин, по которым перечисления не могут быть реализованы с числами с плавающей запятой и фиксированным приращением между ними, но я не слышал о это делается на любом языке.


Я знаю, что мне не следует использовать перечисления таким образом в C #, но я нашел эту головоломку и хотел узнать, почему 0.0 работает, а 1.0 - нет. Я знал, что это должно быть что-то с компилятором C #, потому что вы можете видеть, что код IL Foo v1 = 0.0;для Foo v2 = Foo.Bar.
— feO2x
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.