Приведение целых к перечислениям в C # - PullRequest
35 голосов
/ 18 ноября 2009

Есть кое-что, чего я не могу понять в C #. Вы можете преобразовать int вне диапазона в enum, и компилятор не вздрогнет. Представьте себе это enum:

enum Colour
{
    Red = 1,
    Green = 2,
    Blue = 3
}

Теперь, если вы напишите:

Colour eco;
eco = (Colour)17;

Компилятор считает, что это нормально. И время выполнения тоже. Э-э?

Почему команда C # решила сделать это возможным? Я думаю, что в этом решении отсутствует смысл использования перечислений в сценариях, подобных этому:

void DoSomethingWithColour(Colour eco)
{
    //do something to eco.
}

В языке со строгой типизацией, таком как C #, я хотел бы предположить, что eco всегда будет содержать допустимое значение Colour. Но это не так. Программист может вызвать мой метод со значением 17, присвоенным eco (как в предыдущем фрагменте кода), поэтому код в моем методе не должен предполагать, что eco содержит допустимое значение Colour. Мне нужно проверить это явно и обработать исключительные значения, как мне угодно. Почему это?

По моему скромному мнению, было бы гораздо приятнее, если бы компилятор выдавал сообщение об ошибке (или даже предупреждение) при приведении вне диапазона int в enum, если известно значение int во время компиляции. Если нет, среда выполнения должна выдать исключение в операторе присваивания.

Что ты думаешь? Есть ли причина, почему это так?

(Примечание. Это вопрос Я много лет назад писал в своем блоге , но не получил информативного ответа.)

Ответы [ 12 ]

31 голосов
/ 18 ноября 2009

Гадать о «почему» всегда опасно, но учтите следующее:

enum Direction { North =1, East = 2, South = 4, West = 8 }
Direction ne = Direction.North | Direction.East;

int value = (int) ne; // value == 3
string text = ne.ToString();  // text == "3"

Когда атрибут [Flags] ставится перед перечислением, последняя строка меняется на

string text = ne.ToString();  // text == "North, East"
15 голосов
/ 18 ноября 2009

Не знаю, почему, но недавно я нашел эту "функцию" невероятно полезной. Я написал что-то подобное на днях

// a simple enum
public enum TransmissionStatus
{
    Success = 0,
    Failure = 1,
    Error = 2,
}
// a consumer of enum
public class MyClass 
{
    public void ProcessTransmissionStatus (TransmissionStatus status)
    {
        ...
        // an exhaustive switch statement, but only if
        // enum remains the same
        switch (status)
        {
            case TransmissionStatus.Success: ... break;
            case TransmissionStatus.Failure: ... break;
            case TransmissionStatus.Error: ... break;
            // should never be called, unless enum is 
            // extended - which is entirely possible!
            // remember, code defensively! future proof!
            default:
                throw new NotSupportedException ();
                break;
        }
        ...
    }
}

Вопрос в том, как мне проверить это последнее предложение? Вполне разумно предположить, что кто-то может расширить TransmissionStatus и не обновлять своих потребителей, как бедный маленький MyClass выше. Тем не менее, я все еще хотел бы проверить его поведение в этом сценарии. Одним из способов является использование литья, например

[Test]
[ExpectedException (typeof (NotSupportedException))]
public void Test_ProcessTransmissionStatus_ExtendedEnum ()
{
    MyClass myClass = new MyClass ();
    myClass.ProcessTransmissionStatus ((TransmissionStatus)(10));
}
5 голосов
/ 04 ноября 2010

Я, конечно, вижу точку зрения Сезара, и я помню, что это поначалу меня тоже смутило. По моему мнению, перечисления в их текущей реализации, действительно, слишком низкоуровневые и негерметичные. Мне кажется, что было бы два решения этой проблемы.

1) Разрешить сохранение произвольных значений в перечислении только в том случае, если его определение имеет атрибут FlagsAttribute. Таким образом, мы можем продолжать использовать их для битовой маски, когда это уместно (и объявлено явно), но при использовании просто в качестве заполнителей для констант мы получим значение, проверенное во время выполнения.

2) Введите отдельный примитивный тип, называемый скажем, битовая маска, который допускает любое длинное значение. Опять же, мы ограничиваем стандартные перечисления только объявленными значениями. Это дает дополнительное преимущество, позволяя компилятору назначать битовые значения для вас. Итак, это:

[Flags]
enum MyBitmask
{ 
    FirstValue = 1, SecondValue = 2, ThirdValue = 4, FourthValue = 8 
}

будет эквивалентно этому:

bitmask MyBitmask
{
    FirstValue, SecondValue, ThirdValue, FourthValue 
}

В конце концов, значения для любой битовой маски полностью предсказуемы, верно? Как программист, я более чем счастлив, что эту деталь абстрагировали.

Тем не менее, уже слишком поздно, я думаю, что мы застряли с текущей реализацией навсегда. : /

4 голосов
/ 16 марта 2012

Это неожиданно ... мы действительно хотим контролировать кастинг ... например:

Colour eco;
if(Enum.TryParse("17", out eco)) //Parse successfully??
{
  var isValid = Enum.GetValues(typeof (Colour)).Cast<Colour>().Contains(eco);
  if(isValid)
  {
     //It is really a valid Enum Colour. Here is safe!
  }
}
4 голосов
/ 18 ноября 2009

Это одна из многих причин, по которой вам никогда не следует присваивать целочисленные значения вашим перечислениям. Если они имеют важные значения, которые необходимо использовать в других частях кода, превратите перечисление в объект.

4 голосов
/ 18 ноября 2009

Вам не нужно иметь дело с исключениями. Предварительным условием для этого метода является то, что вызывающие абоненты должны использовать перечисление, а не приводить любые willy nilly int к указанному перечислению. Это было бы безумием. Не является ли смысл перечислений не в использовании целых чисел?

Насколько мне известно, любому разработчику, который бросит 17 в Color enum, понадобится 17 ударов по спине.

3 голосов
/ 10 января 2014
if (!Enum.IsDefined(typeof(Colour), 17))
{
    // Do something
}
3 голосов
/ 18 ноября 2009

Когда вы определяете enum, вы, по сути, присваиваете имена значениям (синтетический сахар, если хотите). Когда вы переводите 17 в Color, вы сохраняете значение для Color, у которого нет имени. Как вы, наверное, знаете, в конце концов, это просто поле int в любом случае.

Это похоже на объявление целого числа, которое будет принимать только значения от 1 до 100; единственный язык, который я когда-либо видел, который поддерживал этот уровень проверки, был CHILL.

1 голос
/ 18 ноября 2009

Короткая версия:

Не делай этого.

Попытка преобразовать перечисления в целые с разрешением только допустимых значений (возможно, запасной вариант по умолчанию) требует вспомогательных методов. В этот момент у вас нет перечисления - у вас действительно есть класс.

Вдвойне так, если интты являются имплантантами - как сказал Брайан Роу.

0 голосов
/ 11 марта 2015

Старый вопрос, но это меня недавно смутило, и Google привел меня сюда. Я нашел статью с дополнительным поиском, которая, наконец, заставила ее щелкнуть меня, и подумал, что я вернусь и поделюсь.

Суть в том, что Enum является структурой, что означает, что это тип значения ( source ). Но по сути он действует как производный тип базового типа (int, byte, long и т. Д.). Так что если вы можете думать о типе Enum как о том же самом, что и его базовый тип с некоторой дополнительной функциональностью / синтаксическим сахаром (как сказал Отавио), то вы будете знать об этой «проблеме» и сможете ее защитить.

Говоря об этом, вот сердце метода, который я написал, чтобы легко конвертировать / анализировать вещи в Enum:

if (value != null)
{
    TEnum result;
    if (Enum.TryParse(value.ToString(), true, out result))
    {
        // since an out-of-range int can be cast to TEnum, double-check that result is valid
        if (Enum.IsDefined(typeof(TEnum), result.ToString()))
        {
            return result;
        }
    }
}

// deal with null and defaults...

Имеется переменная value типа object, так как это расширение имеет «перегрузки», которые принимают int, int ?, string, Enum и Enum ?. Они все упакованы и отправлены в закрытый метод, который выполняет анализ. Используя TryParse и IsDefined в указанном порядке , я могу анализировать все только что упомянутые типы, включая строки со смешанным регистром, и это довольно надежно.

Использование примерно так (предполагает NUnit):

[Test]
public void MultipleInputTypeSample()
{
    int source;
    SampleEnum result;

    // valid int value
    source = 0;
    result = source.ParseToEnum<SampleEnum>();
    Assert.That(result, Is.EqualTo(SampleEnum.Value1));

    // out of range int value
    source = 15;
    Assert.Throws<ArgumentException>(() => source.ParseToEnum<SampleEnum>());

    // out of range int with default provided
    source = 30;
    result = source.ParseToEnum<SampleEnum>(SampleEnum.Value2);
    Assert.That(result, Is.EqualTo(SampleEnum.Value2));
}

private enum SampleEnum
{
    Value1,
    Value2
}

Надеюсь, это кому-нибудь поможет.

Отказ от ответственности: мы не используем флаги / битовые маски ... это не было проверено в этом сценарии использования и, вероятно, не будет работать.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...