Кастинг ints для перечисления в С#
В С# есть что-то, чего я не могу понять. Вы можете вывести вне диапазона int
в enum
, и компилятор не дрогнет. Представьте себе этот enum
:
enum Colour
{
Red = 1,
Green = 2,
Blue = 3
}
Теперь, если вы пишете:
Colour eco;
eco = (Colour)17;
Компилятор считает, что все в порядке. И время исполнения тоже. Мм?
Почему команда С# решила сделать это возможным? Это решение не подходит для использования перечислений, я думаю, в таких сценариях, как это:
void DoSomethingWithColour(Colour eco)
{
//do something to eco.
}
В синтаксическом языке, таком как С#, я хотел бы предположить, что eco
всегда будет иметь юридическое значение Colour
. Но это не так. Программист мог вызвать мой метод со значением 17, назначенным eco
(как в предыдущем фрагменте кода), поэтому код в моем методе не должен предполагать, что eco
содержит юридическое значение Colour
. Мне нужно проверить его явно и обрабатывать исключительные значения, как мне заблагорассудится. Почему это?
По моему скромному мнению, было бы намного лучше, если компилятор выдал сообщение об ошибке (или даже предупреждение) при выдаче вне диапазона int
в enum
, если известно значение int
во время компиляции. В противном случае среда выполнения должна генерировать исключение в инструкции присваивания.
Как вы думаете? Есть ли причина, почему это так?
(Примечание. Это вопрос Я отправил много веков назад в мой блог, но не получил информативного ответа.)
Ответы
Ответ 1
Угадывание "почему" всегда опасно, но подумайте об этом:
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"
Ответ 2
Не знаю, почему, но я недавно нашел эту "особенность" невероятно полезной. Я написал что-то подобное на днях
// 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));
}
Ответ 3
Я, конечно, вижу точку Цезаря, и я помню, что это изначально тоже смутило меня. На мой взгляд, перечисления, в их нынешнем исполнении, действительно немного слишком низки и протекают. Мне кажется, что было бы два решения проблемы.
1) Разрешать произвольные значения только в перечислении, если в его определении был флаг FlagsAttribute. Таким образом, мы можем продолжать использовать их для битмаски, когда это необходимо (и явно объявлено), но при использовании просто как заполнители для констант мы получим значение, проверенное во время выполнения.
2) Введем отдельный примитивный тип, называемый say, bitmask, который позволит любое значение ulong. Опять же, мы ограничиваем стандартные перечисления только объявленными значениями. Это будет иметь дополнительное преимущество, позволяющее компилятору присваивать вам битовые значения. Итак:
[Flags]
enum MyBitmask
{
FirstValue = 1, SecondValue = 2, ThirdValue = 4, FourthValue = 8
}
будет эквивалентно этому:
bitmask MyBitmask
{
FirstValue, SecondValue, ThirdValue, FourthValue
}
В конце концов, значения для любой битмаски полностью предсказуемы, верно? Как программист, я более чем счастлив, что эта деталь отвлечена.
Все еще, слишком поздно, думаю, мы навсегда зациклились на текущей реализации.:/
Ответ 4
Вам не нужно иметь дело с исключениями. Предварительным условием для метода является то, что вызывающие абоненты должны использовать перечисление, а не передавать любые willy nilly int указанному перечислению. Это было бы безумием. Разве это не означает, что перечисления не должны использовать ints?
Любой разработчик, который должен был отбрасывать 17 в переименование цвета, должен был получить 17 ударов по их задней стороне, насколько мне известно.
Ответ 5
Это одна из многих причин, почему вы никогда не должны назначать целочисленные значения для ваших перечислений. Если у них есть важные значения, которые необходимо использовать в других частях кода, переведите перечисление в объект.
Ответ 6
Это неожиданно... мы действительно хотим контролировать кастинг... например:
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!
}
}
Ответ 7
Когда вы определяете перечисление, вы по существу даете имена значениям (синтаксический сахар, если хотите). Когда вы отбрасываете 17 в цвет, вы сохраняете значение для цвета, у которого нет имени. Как вы, наверное, знаете, в конце концов, это просто поле int anyways.
Это сродни объявлению целого числа, которое принимает только значения от 1 до 100; единственным языком, который я когда-либо видел, который поддерживал этот уровень проверки, был CHILL.
Ответ 8
if (!Enum.IsDefined(typeof(Colour), 17))
{
// Do something
}
Ответ 9
Краткая версия:
Не делайте этого.
Попытка изменить enums на int с допустимыми значениями (возможно, по умолчанию для резервного копирования) требует вспомогательных методов. В этот момент у вас нет перечисления - у вас есть класс.
Вдруг так, если ints являются неактивными, - сказал Брайан Роу.
Ответ 10
Думаю, что я поделился бы кодом, который я использовал для проверки Enums, поскольку пока что у нас нет ничего, что работает...
public static class EnumHelper<T>
{
public static bool IsValidValue(int value)
{
try
{
Parse(value.ToString());
}
catch
{
return false;
}
return true;
}
public static T Parse(string value)
{
var values = GetValues();
int valueAsInt;
var isInteger = Int32.TryParse(value, out valueAsInt);
if(!values.Select(v => v.ToString()).Contains(value)
&& (!isInteger || !values.Select(v => Convert.ToInt32(v)).Contains(valueAsInt)))
{
throw new ArgumentException("Value '" + value + "' is not a valid value for " + typeof(T));
}
return (T)Enum.Parse(typeof(T), value);
}
public static bool TryParse(string value, out T p)
{
try
{
p = Parse(value);
return true;
}
catch (Exception)
{
p = default(T);
return false;
}
}
public static IEnumerable<T> GetValues()
{
return Enum.GetValues(typeof (T)).Cast<T>();
}
}
Ответ 11
Старый вопрос, но это меня немного смутило, и 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
имеет объект типа, так как это расширение имеет "перегрузки", которые принимают int, int?, string, Enum и Enum?. Они все в коробке и отправляются в частный метод, который выполняет синтаксический анализ. Используя TryParse
и IsDefined
в этом порядке, я могу разобрать все упомянутые типы, включая смешанные строки case, и это довольно надежное.
Использование такое же (предполагает 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
}
Надеюсь, что кто-то поможет.
Отказ от ответственности: мы не используем флаги/битовые маски... это не было протестировано с использованием этого сценария использования и, вероятно, не сработало.
Ответ 12
Это вызовет ошибку, используя Enum.Parse();
Enum parsedColour = (Colour)Enum.Parse(typeof(Colour), "17");
Возможно, вам стоит использовать, чтобы получить ошибку выполнения, если хотите.