Вопрос по switch-statement, c#, .net, enums – Переключатель без значения по умолчанию при работе с перечислениями

35

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

public enum Decision { Yes, No}

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

            }
        }
    }

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

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

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

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

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

Decision fred = (Decision)123;

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

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

EDIT 2:

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

хорошая Джульетта ... но ты, наверное, должен дать понять, что это шутка, я не уверен, что это очевидно для всех;) Thomas Levesque
@Mark: это не связано с реализацией .NETenumэто просто какswitch заявление работает: вы получите ту же ошибку, если вы используетеbool и покрытьtrue а такжеfalse пути, но опуститьdefault; или если вы используетеbyte и охватывают все 256 возможных путей безdefault; или использовать короткий и покрыть все 65536 путей безdefault и т. д. LukeH
Я думаю, что вы имеете в виду & quot; общедоступное решение enum {Да, Нет, FileNotFound} & quot; Juliet
@jasonh работает на вашей основе, что перечисления могут меняться, так же как и методы, означает ли это, что у всех классов должен быть метод по умолчанию, к которому можно обратиться в случае, если .NET не сможет найти запрошенную вами сигнатуру метода? Mark
@ Как, например, компилятор говорит мне, что отсутствует оператор return, поскольку из-за .NET-реализации enum он может найти путь к коду, который не находит возвращаемого значения. Это верно только потому, что перечисления .NET не являются истинными перечислениями. Если бы они были, есть только два возможных маршрута через рутину. Mark

Ваш Ответ

10   ответов
2

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

Это не может быть перехвачено во время компиляции из-за того, что сказал Томас Л., поэтому по умолчанию существует, чтобы убедиться, что он перехвачен.
Я знаю, что могу это сделать, но все равно вводится исключение времени выполнения, которого можно избежать. .NET заставляет вас использовать разрывы, вместо того, чтобы допускать сквозные переходы, чтобы избежать логических ошибок, заставляющих использовать значение по умолчанию, просто вызывающее проблемы. Mark
1

Таким образом, здесь это может быть не так, а вместо этого будет «Неверное решение, обратитесь в службу поддержки».

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

0

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

Хорошая вещь в этом подходе состоит в том, что вы не попадаете в ситуацию и не забываете обновить свой оператор переключателя GetDecision при добавлении нового значения enum, поскольку все это вместе в одном месте - в объявлении enum.

Эффективность такого подхода мне не известна и, на самом деле, даже не рассматривается в данный момент. Это верно,I just don't care потому что это намного легче дляme что я полагаю, & Quot; Phhhtt винт компьютер, и APOS, S, что это & APOS;. ˙s там - работать & Quot; (Я могу сожалеть об этом, когда Skynet вступает во владение.)

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

(Обычно я делаю 'not set' 'в качестве первого элемента enum, потому что, как отмечает OP, перечисление C # действительно intнеинициализированная переменная будет равна нулю или первому значению перечисления)

public enum Decision
{
  [DisplayText("Not Set")]
  NotSet,
  [DisplayText("Yes, that's my decision")]
  Yes,
  [DisplayText("No, that's my decision")]
  No
}

public static class XtensionMethods
{
  public static string ToDisplayText(this Enum Value)
  {
    try
    {
      Type type = Value.GetType();
      MemberInfo[] memInfo =
        type.GetMember(Value.ToString());

      if (memInfo != null && memInfo.Length > 0)
      {
        object[] attrs = memInfo[0]
          .GetCustomAttributes(typeof(DisplayText), false);
        if (attrs != null && attrs.Length > 0)
          return ((DisplayText)attrs[0]).DisplayedText;
      }
    }
    catch (Exception ex)
    {
      throw new Exception(
        "Error in XtensionMethods.ToDisplayText(Enum):\r\n" + ex.Message);
    }
    return Value.ToString();
  }

  [System.AttributeUsage(System.AttributeTargets.Field)]
  public class DisplayText : System.Attribute
  {
    public string DisplayedText;

    public DisplayText(string displayText)
    {
      DisplayedText = displayText;
    }
  }
}

Используйте встроенный как:

myEnum.ToDisplayText();

Или в функции, если вам это нравится:

public string GetDecision(Decision decision)
{
  return decision.ToDisplayText();
}
0

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

17

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

string s = GetDecision((Decision)42);

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

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

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

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

Фактически, это дает вам достаточно веревки для эффективного взаимодействия с существующими API-интерфейсами COM и WIN32, которые определяют перечислимые целочисленные типы для своих флагов. Но да, вы должны быть осторожны с этой веревкой.
Ну, на самом деле это имеет смысл для перечислений сFlags Атрибут ... этот атрибут означает, что вы можете комбинировать значения, что часто приводит к значениям, которые явно не являются частью перечисления, но тем не менее являются действительными. Однако я хотел бы, чтобы компилятор разрешил это только дляFlags перечисления ...
Я ценю, что вы можете делать такие вещи, но для меня это всего лишь случаи, когда .NET дает вам веревку, чтобы повеситься. Mark
7
public enum Decision { Yes, No}

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

        }
    }
}
15

default пункт:

default:
    throw new ArgumentOutOfRangeException("decision");

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

37

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

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

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

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

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

Ага. Тот факт, что перечисления являются не более чем необычными целочисленными значениями, и тот факт, что перечисления могут меняться от версии к версии, делают их опасными для использования. Это неудачно; в следующий раз, когда вы создадите систему типов с нуля, вы не будете повторять эту ошибку.
Спасибо Эрику за то, что он смог увидеть мои проблемы до глубины души. Однако вы не видите, что в случае перечисления это может фактически привести к проблемам, форсируя избыточный случай по умолчанию, который я превращаю в опасный. Mark
2

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

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

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

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

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

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

Опять же, я понимаю, что это нить воскресения - ирония не потеряна для меня ... Смысл ОП в том, что если бы вы добавилиDecision.Maybe к вашему перечислению ваше предложение не сработает. Ни один из текущих путей кода не применяется дляMaybe так что ваш код будет скрывать ошибку. Если бы вам было разрешено опустить значение по умолчанию, компилятор мог бы сказать «Держись! Вы ничего не вернули, если решение было, возможно! и во время компиляции вам будет помечена потенциальная ошибка. Я подозреваю, что OP знает, что вы можете установить default на существующую ветку, но это не то, что они хотят сделать.
2

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

You can always implement your own strong enums

... и с момента введения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 enum без нулевого регистра (поскольку это не класс). И так же, как в Java, вы можете добавить к нему любые дополнительные данные и / или свойства, например, например. порядковый номер.

Читаемость:Check!
Intellisense:Check!
Refactorability:Check!
Является ли ValueType:Check!
Истинное перечисление:Check!
...
Это исполнитель?Compared to native enums; no.
Вы должны использовать это?Hmmm....

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

... На самом деле, когда я писал это, я понял, что, вероятно, было бы чище позволить struct & quot; wrap & quot; нормальное перечисление. (Поля статической структуры и соответствующее нормальное перечисление отражают друг друга с помощью аналогичного отражения, как указано выше.) Просто никогда не используйте обычное перечисление в качестве параметра, и вы хороши.

UPDATE :

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

// 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,
    }
}

Это то что тыcan делать:

You can add any private fields you want. You can add any public non-static fields you want. You can add any properties and methods you want. You can design your constructors however you wish, because: You can forget base constructor hassle. Base constructor is parameter-less!

Это то что тыmust делать:

Your enum must be a sealed class. All your constructors must be private. Your enum must inherit directly from Enumeration<T, U> and inherit the empty IEnumerationMarker interface. The first generic type parameter to Enumeration<T, U> must be your enum class. For each public static field there must exist an identically named value in the System.Enum (that you specified as the second generic type parameter to Enumeration<T, U>). All your public static fields must be readonly and of your enum type. All your public static fields must be assigned a unique non-null value during type initialization.

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

Обоснование требований:

Your enum must be sealed because if it isn't then other invariants become a lot more complicated for no obvious benefit. Allowing public constructors makes no sense. It's an enumeration type, which is basically a singleton type but with a fixed set of instances instead of just one. Same reason as the first one. The reflection and some of the other invariant and constraint checking just becomes messy if it isn't. We need this generic type parameter so the type-system can be used to uniquely store our enum data with performant compile/Jit time bindings. No hash-tables or other slow mechanisms are used! In theory it can be removed, but I don't think it's worth the added cost in complexity and performance to do so. This one should be quite obvious. We need these constants for making elegant switch-statements. Of course I could make a second enum-type that doesn't have them; you would still be able to switch using the nameof method shown earlier. It just would not be as performant. I'm still contemplating if a should relax this requirement or not. I'll experiment on it... Your enum constants must be public and static for obvious reasons, and readonly fields because a) having readonly enumeration instances means all equality checks simplifies to reference equality; b) properties are more flexible and verbose, and frankly both of those are undesirable properties for an enumeration implementation. Lastly all public static fields must be of your enum-type simply because; a) it keeps your enumeration type cleaner with less clutter; b) makes the reflection simpler; and c) you are free to do whatever with properties anyway so it's a very soft restriction. This is because I'm trying hard to keep "nasty reflection magic" to a minimum. I don't want my enum implementation to require full-trust execution. That would severely limit its usefulness. More precisely, calling private constructors or writing to readonly fields can throw security exceptions in a low-trust environment. Thus your enum must instantiate your enum constants at initialization time itself - then we can populate the (internal) base-class data of those instances "cleanly".

So anyway, how can you use these java-style enums?

Ну, я реализовал это на данный момент:

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;
    }
}

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

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

So where is this code then?

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

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

Все * кроме самых последних измененийfully задокументировано и дружественно к IntelliSense.
(*The java enum types are still wip and will be properly documented once I've finalized their design.)

Похожие вопросы