Чому я можу призначити 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;щоб рядок 2 в Mainкомпілювався. Чому існує виняток для значення 0.0в C #?


17
Для мене дивно, що ви можете призначити подвійний літерал 0.0 для користувацького перерахування. Не те, що ви не можете призначити 1.0літерал на власний перелік.
Ілля Іванов

2
Я підозрюю, що компілятор розглядає це як 0натомість. У мене був подібний питання раз і Ролінг розмістив великий відповідь тут .
Мирова дружба

2
IdeOne його не компілює.
Джонні Мопп,

Відповіді:


98

Це помилка, яку ви можете використовувати 0.0. Компілятор неявно розглядає всі константні вирази зі значенням нуля як просто 0.

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

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

Я вже говорив з командою C # про це раніше: вони хотіли б видалити випадкове перетворення з 0.0 (і справді 0.0m і 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 це буде виправлено в Росліні?
Макс

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 - подивіться, щоб дізнатись більше про те, якою саме є поведінка Росліна. Я написав майже весь код у каталозі Conversions; Я закликаю вас прочитати це все, оскільки у коментарях є багато цікавих фактів про те, як C # відхиляється від специфікації. Кожен я прикрасив SPEC VIOLATION, щоб їх було легко знайти.

Ще одна цікавинка: C # також дозволяє використовувати будь-яке значення переліку в ініціалізаторі перерахування незалежно від його нульового значення:

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

Характеристики дещо розмиті щодо того, чи має це бути законним чи ні, але знову ж таки, оскільки це було у компіляторі довгий час, нові компілятори, швидше за все, збережуть поведінку.


10
Це справді круто, нарешті я бачу код, який ви написали. Приємно, що вихідний код Roslyn є відкритим. Тепер я повністю розумію, що існують вагомі причини (технічні / юридичні) не надавати історію змін, але було б надзвичайно дивовижно побачити історію змін, щоб побачити, як розвивався код.
Рішення

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.Я не думаю, що було б багато людей, які покладаються на таку поведінку, і це одне з цих дивацтв, яке, можливо, було б краще просто виправити. Однак це насправді також не завдає великої шкоди (за винятком проектів, що реалізують специфікацію).
Айдіякапі,

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.

Концептуально, все в порядку повинно додати ціле число n до перерахованого значення, отримати n значень далі по рядку або взяти val2 - val1, щоб побачити, наскільки вони віддалені, але якщо специфікація мови це явно не дозволяє, I уникав би цього. (Подумайте про перераховане значення як про вказівник на С, таким чином, як ви можете ним скористатися.) Немає жодної причини, коли перелічення не можуть бути реалізовані з числами з плаваючою комою та фіксованим приростом між ними, але я не чув про це робиться будь-якою мовою.


Я знаю, що я не повинен використовувати перерахування таким чином у 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.