Подтвердить что ты не робот

Оператор switch без дефолта при работе с перечислениями

Это было мое домашнее животное, так как я начал использовать .NET, но мне было любопытно, если я что-то упустил. Мой фрагмент кода не будет компилироваться (пожалуйста, простите принудительный характер образца), потому что (согласно компилятору) отсутствует оператор return:

public enum Decision { Yes, No}

    public class Test
    {
        public string GetDecision(Decision decision)
        {
            switch (decision)
            {
                case Decision.Yes:
                    return "Yes, that my decision";
                case Decision.No:
                    return "No, that my decision";

            }
        }
    }

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

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

Это настолько принципиально неправильно для меня, что я хочу знать, если его просто что-то мне не хватает, или нам просто нужно быть очень осторожным, когда мы используем перечисления в операторах switch?

EDIT: Я знаю, что могу поменять исключения по умолчанию или добавить возврат за пределы коммутатора, но это по-прежнему фундаментальные взломы, чтобы обойти ошибку компилятора, которая не должна быть ошибкой.

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

Decision fred = (Decision)123;

а затем выведите исключение, если кто-то попробует что-то вроде:

int foo = 123;
Decision fred = (Decision)foo;

ИЗМЕНИТЬ 2:

Несколько человек сделали комментарии о том, что происходит, когда перечисление находится в другой сборке и как это приведет к проблемам. Я хочу сказать, что это поведение, которое, как мне кажется, должно произойти. Если я изменю подпись метода, это приведет к проблемам, моя премия заключается в том, что изменение перечисления должно быть таким же. У меня создается впечатление, что многие люди не думают, что я понимаю о перечислениях в .NET. Я просто думаю, что поведение ошибочно, и я надеялся, что кто-то может знать о какой-то очень неясной функции, которая изменила бы мое мнение о .NET enums.

4b9b3361

Ответ 1

Черт, ситуация намного хуже, чем просто переписка. Мы даже не делаем этого для bools!

public class Test {        
  public string GetDecision(bool decision) {
    switch (decision) {
       case true: return "Yes, that my decision";                
       case false: return "No, that my decision"; 
    }
  }
}

Выдает ту же ошибку.

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

Я бы очень хотел это исправить, но, честно говоря, у нас много приоритетов, чем устранение этой глупой маленькой проблемы, поэтому мы никогда не обходились ей.

Ответ 2

Это потому, что значение decision может фактически быть значением, которое не является частью перечисления, например:

string s = GetDecision((Decision)42);

Этот вид не предотвращается компилятором или CLR. Значение также может быть комбинацией значений перечисления:

string s = GetDecision(Decision.Yes | Decision.No);

(даже если перечисление не имеет атрибута Flags)

Из-за этого вы должны всегда поместить случай default в ваш коммутатор, так как вы не можете явно проверить все возможные значения

Ответ 3

Выбросьте исключение в предложении по умолчанию:

default:
    throw new ArgumentOutOfRangeException("decision");

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

Ответ 4

public enum Decision { Yes, No}

public class Test
{
    public string GetDecision(Decision decision)
    {
        switch (decision)
        {
            case Decision.Yes:
                return "Yes, that my decision";
            case Decision.No:
                return "No, that my decision";
            default: throw new Exception(); // raise exception here.

        }
    }
}

Ответ 5

По умолчанию используется защита. Выбросьте исключение из значения по умолчанию, и если кто-то добавит дополнительное перечисление, вы получите что-то, чтобы его пометить.

Ответ 6

Я понимаю, что это воскрешение потока...

Я лично считаю, что способ, которым работает коммутатор, правилен, и он работает так, как я бы логически его хотел.

Я удивлен, услышав такие жалобы на метку по умолчанию.

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

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

switch (decision)
{
    default:
    case Decision.Yes:
        return "Yes, that my decision";
    case Decision.No:
        return "No, that my decision";
}

Если вы не хотите, чтобы значение по умолчанию было Да, поместите метку по умолчанию выше метки Нет.

Ответ 7

Я всегда думаю об этом умолчании как об ошибке/исключении.

Так что здесь это было бы не так, но вместо этого было бы "Invalid Decision, support support".

Я не вижу, как это может произойти, но это будет случай catchall/exception.

Ответ 8

Ради совместного использования причудливой идеи, если ничего другого, здесь идет:

Вы всегда можете реализовать свои собственные сильные перечисления

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

public struct MyEnum : IEquatable<MyEnum>
{
    private readonly string name;
    private MyEnum(string name) { name = name; }

    public string Name
    {
        // ensure observable pureness and true valuetype behavior of our enum
        get { return name ?? nameof(Bork); } // <- by choosing a default here.
    }

    // our enum values:
    public static readonly MyEnum Bork;
    public static readonly MyEnum Foo;
    public static readonly MyEnum Bar;
    public static readonly MyEnum Bas;

    // automatic initialization:
    static MyEnum()
    {
        FieldInfo[] values = typeof(MyEnum).GetFields(BindingFlags.Static | BindingFlags.Public);
        foreach (var value in values)
            value.SetValue(null, new MyEnum(value.Name));
    }

    /* don't forget these: */
    public override bool Equals(object obj)
    {
        return obj is MyEnum && Equals((MyEnum)obj);
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
    public override string ToString()
    {
        return Name.ToString();
    }
    public bool Equals(MyEnum other)
    {
        return Name.Equals(other.Name);
    }
    public static bool operator ==(MyEnum left, MyEnum right)
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyEnum left, MyEnum right)
    {
        return !left.Equals(right);
    }
}

и используйте его таким образом:

public int Example(MyEnum value)
{
    switch(value.Name)
    {
        default: //case nameof(MyEnum.Bork):
            return 0;
        case nameof(MyEnum.Foo):
            return 1;
        case nameof(MyEnum.Bar):
            return 2;
        case nameof(MyEnum.Bas):
            return 3;
    }
}

и вы, конечно, называете этот метод следующим:
int test = Example(MyEnum.Bar); // returns 2

Теперь мы можем легко получить имя, в основном, просто бонус, и да, некоторые читатели могут указать, что это в основном переименование Java без нулевого случая (так как это не класс), И точно так же, как в Java, вы можете добавить любые дополнительные данные и/или свойства, которые вы хотите, например, порядковое значение.

Считываемость: проверьте!
Intellisense: проверьте!
Рефакторимость: проверьте!
Является ValueType: Check!
Истинное перечисление: проверьте!
...
Это исполнитель? По сравнению с родными перечислениями; нет.
Должны ли вы использовать это? Ммм....

Насколько важно для вас иметь истинные перечисления, чтобы вы могли избавиться от проверок времени выполнения enum и их сопровождающих исключений?
Я не знаю. Не могу ответить на этот вопрос для вас, дорогой читатель; каждому из них.

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

ОБНОВЛЕНИЕ:

Yepp, провел ночь, проверяя мои идеи, и я был прав: теперь у меня есть почти идеальные переходы в стиле Java в С#. Чистое использование и улучшение производительности. Лучше всего: все неприятное дерьмо инкапсулировано в базовый класс, ваша конкретная реализация может быть такой же чистой, как это:

// example java-style enum:
public sealed class Example : Enumeration<Example, Example.Const>, IEnumerationMarker
{
    private Example () {}

    /// <summary> Declare your enum constants here - and document them. </summary>
    public static readonly Example Foo = new Example ();
    public static readonly Example Bar = new Example ();
    public static readonly Example Bas = new Example ();

    // mirror your declaration here:
    public enum Const
    {
        Foo,
        Bar,
        Bas,
    }
}

Это то, что вы можете сделать:

  • Вы можете добавить любые частные поля, которые вы хотите.
  • Вы можете добавлять любые общедоступные нестатические поля.
  • Вы можете добавить любые свойства и методы, которые вы хотите.
  • Вы можете проектировать свои конструкторы, как хотите, потому что:
  • Вы можете забыть проблему с базовым конструктором. Базовый конструктор не зависит от параметра!

Это то, что вы должны сделать:

  • Ваше перечисление должно быть закрытым классом.
  • Все ваши конструкторы должны быть закрытыми.
  • Ваше перечисление должно наследовать непосредственно из Enumeration < T, U > и наследует пустой интерфейс IEnumerationMarker.
  • Первый общий параметр типа Enumeration < T, U > должен быть вашим классом enum.
  • Для каждого открытого статического поля должно существовать одинаковое именованное значение в System.Enum(которое вы указали как второй общий тип параметра Enumeration < T, U > ).
  • Все ваши общедоступные статические поля должны быть только для чтения и вашего типа перечисления.
  • Во время инициализации типа во всех ваших открытых статических полях должно быть назначено уникальное ненулевое значение.

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

Требования Обоснование:

  • Ваше перечисление должно быть запечатано, потому что, если это не так, другие инварианты становятся намного более сложными без какой-либо очевидной выгоды.
  • Разрешение публичных конструкторов не имеет смысла. Это тип перечисления, который является в основном одноэлементным типом, но с фиксированным набором экземпляров вместо одного.
  • По той же причине, что и первая. Отражение и некоторые другие проверки инварианта и ограничений просто становятся беспорядочными, если это не так.
  • Нам нужен этот общий тип типа, поэтому система типов может использоваться для уникального хранения наших данных перечисления с помощью компиляции /Jit времени выполнения. Не используются хэш-таблицы или другие медленные механизмы! Теоретически это можно удалить, но я не думаю, что это стоит дополнительных затрат по сложности и производительности для этого.
  • Это должно быть совершенно очевидно. Нам нужны эти константы для создания элегантных операторов switch. Конечно, я мог бы сделать второй тип перечисления, который их не имеет; вы все равно сможете переключиться с помощью метода nameof, показанного ранее. Это было бы не так хорошо. Я все еще размышляю о том, следует ли смягчить это требование или нет. Я буду экспериментировать на этом...
  • Константы перечисления должны быть общедоступными и статичными по очевидным причинам и полям только для чтения, потому что: a) имея экземпляры переиздания только для чтения, все проверки равенства упрощают обращение к равенству; б) свойства более гибкие и подробные, и, откровенно говоря, оба из них являются нежелательными свойствами для реализации перечисления. Наконец, все публичные статические поля должны быть вашего типа перечисления просто потому, что; a) он держит ваш очиститель типа перечисления с меньшим беспорядком; б) делает отражение проще; и c) вы можете делать все, что со свойствами, так что это очень мягкое ограничение.
  • Это потому, что я стараюсь свести к минимуму "неприятную магию отражения". Я не хочу, чтобы моя реализация enum требовала выполнения полного доверия. Это серьезно ограничило бы его полезность. Точнее, вызов частных конструкторов или запись в поля только для чтения могут вызывать исключения безопасности в среде с низким доверием. Таким образом, ваше перечисление должно создавать константы enum во время самого инициализации - тогда мы можем заполнить (внутренние) данные базового класса этих экземпляров "чисто".

Так или иначе, как вы можете использовать эти перечисления в стиле Java?

Ну, я реализовал это сейчас:

int ordinal = Example.Bar.Ordinal; // will be in range: [0, Count-1]
string name = Example.Bas.Name; // "Bas"
int count = Enumeration.Count<Example>(); // 3
var value = Example.Foo.Value; // <-- Example.Const.Foo

Example[] values;
Enumeration.Values(out values);

foreach (var value in Enumeration.Values<Example>())
    Console.WriteLine(value); // "Foo", "Bar", "Bas"

public int Switching(Example value)
{
    if (value == null)
        return -1;

    // since we are switching on a System.Enum tabbing to autocomplete all cases works!
    switch (value.Value)
    {
        case Example.Const.Foo:
            return 12345;
        case Example.Const.Bar:
            return value.GetHasCode();
        case Example.Const.Bas:
            return value.Ordinal * 42;
        default:
            return 0;
    }
}

Абстрактный класс перечисления также будет реализовывать интерфейс IEquatable<Example> для нас, включая == и!= операторы, которые будут работать в примерах экземпляров.

Помимо отражения, необходимого при инициализации типа, все чисто и качественно. Вероятно, перейдем к реализации специализированных коллекций java для перечислений.

Итак, где же этот код?

Я хочу посмотреть, смогу ли я немного очистить его до того, как я его выпущу, но к концу недели он, вероятно, попадет на ветку dev на GitHub - если не найду других сумасшедших проектов, над которыми нужно работать! ^ _ ^

Теперь на GitHub
См. Enumeration.cs и Enumeration_T2.cs.
В настоящее время они являются частью ветки dev из очень большой библиотеки wip, над которой я работаю.
(Ничто не" освобождается "и в любой момент может быть нарушено.)
... На данный момент остальная часть библиотеки - это, в основном, трюк шаблона для расширения всех методов массива до массивов с несколькими рангами, создания массивов с несколькими рангами, которые можно использовать с Linq, и исполняемых оболочек ReadOnlyArray (неизменяемых структур) для раскрытия (private) массивы безопасным способом без хрупкой необходимости создавать копии все время.

Все * кроме самых последних коммитов dev полностью документировано и IntelliSense.
(* Типы перечисления java по-прежнему прокручиваются и будут правильно задокументированы после завершения их разработки.)

Ответ 9

В дополнение к случаю, вы можете использовать любой int для вашего перечисления и иметь перечисление, которое вы не обрабатываете. Также существует случай, когда enum находится во внешнем .dll и что .dll обновляется, он не нарушает ваш код, если к перечислению добавляется дополнительная опция (например, "Да", "Нет", "Возможно" ). Итак, чтобы справиться с этими будущими изменениями, вам также нужен случай по умолчанию. Во время компиляции невозможно гарантировать, что вы знаете каждое значение, которое будет иметь перечислимое число для жизни.