C#: Enum.IsDefined для комбинированных флагов

У меня есть это перечисление:

[Flags]
public enum ExportFormat
{
    None = 0,
    Csv = 1,
    Tsv = 2,
    Excel = 4,
    All = Excel | Csv | Tsv
}

Я пытаюсь создать оболочку для этого (или любого другого) перечисления, которое уведомляет об изменении. В настоящее время это выглядит так:

public class NotifyingEnum<T> : INotifyPropertyChanged
    where T : struct
{
    private T value;

    public event PropertyChangedEventHandler PropertyChanged;

    public NotifyingEnum()
    {
        if (!typeof (T).IsEnum)
            throw new ArgumentException("Type T must be an Enum");
    }

    public T Value
    {
        get { return value; }
        set
        {
            if (!Enum.IsDefined(typeof (T), value))
                throw new ArgumentOutOfRangeException("value", value, "Value not defined in enum, " + typeof (T).Name);

            if (!this.value.Equals(value))
            {
                this.value = value;

                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                    handler(this, new PropertyChangedEventArgs("Value"));
            }
        }
    }
}

Поскольку перечислению действительно может быть присвоено любое значение, я хочу проверить, определено ли данное значение. Но я нашел проблему. Если я здесь дам ему перечисление, состоящее, например, из Csv | Excel, то Enum.IsDefined вернет false. По-видимому, потому что я не определил никакого перечисления, состоящего из этих двух. Я предполагаю, что на каком-то уровне это логично, но как мне тогда проверить, действительно ли данное значение? Другими словами, чтобы это заработало, на что мне нужно поменять местами следующую строку?

if (!Enum.IsDefined(typeof (T), value))

person Svish    schedule 09.02.2009    source источник
comment
См. мое расширение Enum.IsDefined ниже.   -  person Dan McCann    schedule 19.10.2012


Ответы (10)


В перечислениях на основе флагов речь идет о том, установлен бит или нет. Таким образом, для «ExportFormat», если установлен бит 1, это формат CSV, даже если может быть установлено больше битов. Установлено ли для битов 1 и 2 недопустимое значение? Это субъективно: с точки зрения значений как группы, это недопустимо (для набора битов 1 и 2 не определен битовый шаблон), однако, поскольку каждое значение является битом, если рассматривать их по отдельности, может случиться так, что значение с установленными битами 1 и 2 является действительным.

Если передать значение 0011111011, это действительное значение? Ну, это зависит от того, что вы ищете: если вы смотрите на все значение, то это недопустимое значение, но если вы смотрите на отдельные биты, это нормальное значение: в нем установлены биты, которые не определены, но это нормально, так как перечисления на основе флагов проверяются «по битам»: вы не сравниваете их со значением, вы проверяете, установлен ли бит или нет.

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

person Frans Bouma    schedule 09.02.2009
comment
И как бы вы написали эту логику? - person Svish; 09.02.2009
comment
Логика, о которой я говорил, — это логика, которая проверяет, установлен ли бит. Итак, в вашем случае у вас есть 3 битных теста, по одному для каждого бита, определенного в перечислении, и действуйте соответственно, например. Установлен «Csv», поэтому экспортируйте с помощью Csv. - person Frans Bouma; 09.02.2009
comment
-› у него есть биты, которые не определены, но это нормально. Я думаю, что это неправильно и делает недействительной часть ответа. Установка битов, которые не имеют значения, означает, что где-то что-то не так, поэтому в этой ситуации вам следует выбросить. - person Ignacio Soler Garcia; 30.12.2014

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

public static bool IsDefinedEx(this Enum yourEnum)
{
    char firstDigit = yourEnum.ToString()[0];
    if (Char.IsDigit(firstDigit) || firstDigit == '-')  // Account for signed enums too..
        return false;

    return true;
}

Используйте этот метод расширения вместо стандартного IsDefined, и это должно решить вашу проблему.

person Dan McCann    schedule 12.01.2012
comment
Вы также можете проверить наличие символа «-», поскольку перечисления могут быть представлены значениями со знаком. - person sethobrien; 26.04.2012
comment
Блестящий подход! Большое тебе спасибо. PS: тип возвращаемого значения вашего метода неверен. Вы, наверное, думали о возврате логического значения? ;) - person KnorxThieus; 31.08.2017

Я бы работал на уровне бит и проверял, все ли биты, установленные в новом значении, установлены в вашем значении All:

if ( ! (All & NewValue) == NewValue )

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

person Treb    schedule 09.02.2009
comment
Это могло бы сработать, за исключением того, что я не могу использовать & ни == для операндов типа T. Так что нужно было бы начать преобразование, приведение и прочее, и я хотел бы предотвратить это... - person Svish; 09.02.2009
comment
Это на самом деле отвечает на вопрос разумным и полезным способом. Он не найдет недопустимых комбинаций, но может легко и просто отсеять недопустимые установленные биты. - person Wilbert; 04.12.2015

Вот небольшой метод расширения, который делает это эффективно.

static void Main(string[] args)
{
  var x = ExportFormat.Csv | ExportFormat.Excel;
  var y = ExportFormat.Csv | ExportFormat.Word;
  var z = (ExportFormat)16; //undefined value

  var xx = x.IsDefined();  //true
  var yy = y.IsDefined();  //false
  var zz = z.IsDefined();  //false
}

public static bool IsDefined(this Enum value)
{
  if (value == null) return false;
  foreach (Enum item in Enum.GetValues(value.GetType()))
    if (item.HasFlag(value)) return true;
  return false;
}

[Flags]
public enum ExportFormat                                      
{
  None = 0,
  Csv = 1,
  Tsv = 2,
  Excel = 4,
  Word = 8,
  All = Excel | Csv | Tsv
}

Следующий подход будет работать для элементов, объединенных кодом, который не сгруппирован в перечислении:

static void Main(string[] args)
{
  var x = ExportFormat.Csv | ExportFormat.Excel;
  var y = ExportFormat.Csv | ExportFormat.Word;
  var z = (ExportFormat)16; //undefined value

  var xx = x.IsDefined();  //true
  var yy = y.IsDefined();  //true
  var zz = z.IsDefined();  //false
}

public static bool IsDefined(this ExportFormat value)
{
  var max = Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>()
    .Aggregate((e1,e2) =>  e1 | e2);
  return (max & value) == value;
}

И если вы используете C# 4.0, где поддерживается DLR, вы можете использовать следующий классный независимый метод расширения:

public static bool IsDefined(this Enum value)
{
  dynamic dyn = value;
  var max = Enum.GetValues(value.GetType()).Cast<dynamic>().
    Aggregate((e1,e2) =>  e1 | e2);
  return (max & dyn) == dyn;
}

Примечание. Это необходимо сделать так, поскольку:

  1. Операторы | и & нельзя применять к операндам типа Enum и Enum.
  2. Эти операторы определены в компиляторе и не отражаются, поэтому нет никакого способа получить их с помощью выражений отражения/Linq, поверьте мне - я пробовал все это...
person Shimmy Weitzhandler    schedule 12.01.2012
comment
Динамическая версия не работает, например. [Flags] enum E { A = '1', B = '2', C = '4' } - person Andriy Tolstoy; 03.12.2018
comment
Вы могли бы (и, возможно, должны) возразить, что атрибут [Flags] в перечислении на основе char не имеет смысла. - person Corniel Nobel; 19.04.2019
comment
@CornielNobel char - это байт. Это делает сцены в этом случае '1' | '2' == '3' - person Wouter; 18.05.2020
comment
@Wouter Было бы, но '1' | "2" не "3". Это побитовый оператор для целочисленного значения char. - person Corniel Nobel; 18.05.2020

может попробовать поймать с разбором?
какие значения вы не хотите передавать?

    public T Value
    {
        get { return value; }
        set
        {
            try
            {
                Enum.Parse(typeof(T), value.ToString());
            }
            catch 
            {
                throw new ArgumentOutOfRangeException("value", value, "Value not defined in enum, " + typeof(T).Name);
            }
            if (!this.value.Equals(value))
            {
                this.value = value;

                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                    handler(this, new PropertyChangedEventArgs("Value"));
            }
        }
    }
person Avram    schedule 09.02.2009

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

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

    ExportFormat format;
    if (!Enum.TryParse<ExportFormat>(value.ToString(), out format))
    {
      // Could not parse
    }

Надеюсь, это поможет.

person David Mulford    schedule 11.08.2011
comment
Не работает для недопустимого значения, например: var value = (ExportFormat)50;. Если значение является строковым представлением целого числа, которое не представляет базовое значение перечисления, метод Enum.TryParse возвращает элемент перечисления, базовое значение которого представляет собой значение, преобразованное в целочисленный тип. - person Andriy Tolstoy; 03.12.2019

Вот как это сделать (использует Linq):

    private static bool IsDefined<T>(long value) where T : struct
    {
        var max = Enum.GetValues(typeof(T)).Cast<T>()
            .Select(v => Convert.ToInt64(v)).
            Aggregate((e1, e2) => e1 | e2);
        return (max & value) == value;
    }
person Iain Ballard    schedule 19.03.2018
comment
Не работает, например. [Flags] enum E { A = '1', B = '2', C = '4' } - person Andriy Tolstoy; 03.12.2018
comment
Это действительно значения символов? '1' будет 0b00110001, что, вероятно, не является набором флагов, который вы планировали? - person Iain Ballard; 04.12.2018
comment
Да, это допустимое перечисление с символьными значениями. Просто использовал его как тестовое перечисление, не стремясь получить какой-либо конкретный набор флагов. - person Andriy Tolstoy; 05.12.2018

Взгляните на IsValid метод библиотеки Enums.NET:

var validExportFormat = ExportFormat.Excel | ExportFormat.Csv;
validExportFormat.IsValid(EnumValidation.IsValidFlagCombination); // => true

var invalidExportFormat = (ExportFormat)100;
invalidExportFormat.IsValid(EnumValidation.IsValidFlagCombination); // => false
person Andriy Tolstoy    schedule 22.11.2019

Любая допустимая комбинация значений перечисления дает нечисловое значение:

public static class EnumExtensions
{
    public static bool IsDefined(this Enum value) => !ulong.TryParse(value.ToString(), out _);
}
person Andriy Tolstoy    schedule 03.12.2019
comment
Можете ли вы объяснить. Любая допустимая комбинация значений перечисления дает нечисловое значение с примерами. - person M.Hassan; 16.03.2020
comment
@M.Hassan, метод ToString не будет соответствовать значению, которое не определено в перечислении с именем константы. Только значения, определенные в перечислении, будут сопоставляться с именами констант. См. примеры здесь — docs.microsoft. com/en-us/dotnet/api/ - person Andriy Tolstoy; 17.03.2020
comment
Спасибо за пояснение и ссылку на пример. Просто предложение, добавьте этот комментарий к своему ответу. - person M.Hassan; 18.03.2020

См. здесь. Довольно много кода.

person Anton Gogolev    schedule 09.02.2009