пятница, июня 06, 2008

Surprise! Explicit Enum Cast in C#

О сколько нам открытий чудных
Готовит просвещенья дух.
(с) А. С. Пушкин

Как вы думаете, что выдаст на консоль приведенный ниже код?


public enum Parts
{
Engine = 1,
Wheels,
Brakes
}
static void Main(string[] args)
{
try
{
Parts lineItem = (Parts)4;
Console.WriteLine(lineItem);
}
catch
{
Console.WriteLine("Exception");
}
}

Не знаю, как вы, а я был убежден, что это будет "Exception". Однако на выходе получаем "4". Кстати, если Console.WriteLine(lineItem) заменить на Console.WriteLine(Enum.GetName(typeof(Parts),lineItem)) получим пустую строку. Вот так засада! Явным приведением можно загнать в enum любое значение underlying типа. Судя по обсуждению на RSDN это стало сюрпризом для многих.

Оказывается Anders Hejlsberg пишет в книге "The C# Programming Language":
"Each enum type has a corresponding integral type called the underlying type of the enum type. An enum type that does not explicitly declare an underlying type has an underlying type of int. An enum type’s storage format and range of possible values are determined by its underlying type. The set of values that an enum type can take on is not limited by its enum members. In particular, any value of the underlying type of an enum can be cast to the enum type and is a distinct valid value of that enum type."


Указания на такое поведение есть и в стандарте C#, дык только кто-ж это все читает...
Теперь, когда я думаю о том, сколько кода написано в твердой уверенности, что enum не может содержать никаких значений, кроме объявленных, мне становится как-то не по себе.
Надо взять за правило в switch-ах по enum-мам всегда ставить метку default.

5 комментариев:

Анонимный комментирует...

да... ну ты дал серега...
вот поэтому входные enum-парамерты функций нужно проверять что они в допустимых пределах!

Sergey Rozovik комментирует...

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

Анонимный комментирует...

>> Кстати, проверять enum-ы задача не тривиальная.

Enum.IsDefined()

в случае если не правильный кидать

InvalidEnumArgumentException

Анонимный комментирует...

По ходу последений коментрий не отменяет default или другой обработки оставшихся. Ведь в енум всегда могут добавить елмент, и если код это не ожидает, то вобщемто будет никрасивая ситуация.

Unknown комментирует...

Parts lineItem = (Parts) 4;
Parts lineItem2 = Parts.Engine;

Эквивалентно:
L_0002: ldc.i4.4
L_0003: stloc.0
L_0004: ldc.i4.1
L_0005: stloc.1

А внутри енумчика храниться:
public int value__;

Действительно подсказанный IsDefined это делает. Смотрю рефлектором:
IsDefined(System.Type enumType, object value)
....
string[] names = GetHashEntry(enumType).names;
for (int i = 0; i < names.Length; i++)
{
if (names[i].Equals((string) value))
{
return true;
}
}
return false;