Кто-нибудь знает хороший способ обхода отсутствия общего ограничения enum?

90

Я хочу сделать примерно следующее: у меня есть перечисления с комбинированными помеченными значениями.

public static class EnumExtension
{
    public static bool IsSet<T>( this T input, T matchTo ) 
        where T:enum //the constraint I want that doesn't exist in C#3
    {    
        return (input & matchTo) != 0;
    }
}

Итак, я мог сделать:

MyEnum tester = MyEnum.FlagA | MyEnum.FlagB

if( tester.IsSet( MyEnum.FlagA ) )
    //act on flag a

К сожалению, общий язык C #, в котором ограничения не имеют ограничений перечисления, только class и struct. C # не видит перечисления как структуры (хотя они являются типами значений), поэтому я не могу добавлять такие типы расширений.

Кто-нибудь знает обходной путь?

Кит
источник
2
Кейт: скачайте UnconstrainedMelody версии 0.0.0.2 - я реализовал HasAll и HasAny. Наслаждаться.
Джон Скит,
Что вы имеете в виду, говоря «C # не видит перечисления как структуры»? Вы можете использовать перечислимые типы в качестве параметров типа, которые ограничены structпросто отлично.
Timwi
проверьте эту статью здесь: codeproject.com/KB/cs/ExtendEnum.aspx Методы IsValidEnumValue или IsFlagsEnumDefined, вероятно, являются ответом на ваш вопрос.
dmihailescu 01
1
Проголосуйте за эту идею uservoice , если вы когда-нибудь захотите увидеть ее встроенной в .net.
Matthieu
11
C # 7.3 вводит ограничения перечисления.
Marc Sigrist

Ответы:

49

РЕДАКТИРОВАТЬ: теперь это работает в версии 0.0.0.2 UnconstrainedMelody.

(В соответствии с просьбой в моем сообщении в блоге об ограничениях перечисления . Я включил основные факты ниже для отдельного ответа.)

Лучшее решение - дождаться, пока я включу его в UnconstrainedMelody 1 . Это библиотека, которая использует код C # с «фальшивыми» ограничениями, такими как

where T : struct, IEnumConstraint

и превращает это в

where T : struct, System.Enum

через шаг после сборки.

Это не должно быть слишком сложно написать IsSet... хотя поддержка как Int64-based, так и UInt64-based флагов может быть сложной задачей. (Я чувствую появление некоторых вспомогательных методов, которые позволяют мне обрабатывать любые перечисления флагов, как если бы они имели базовый типUInt64 .)

Каким будет поведение, если вы позвоните

tester.IsSet(MyFlags.A | MyFlags.C)

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

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

РЕДАКТИРОВАТЬ: IsSetКстати, я не уверен, что такое имя. Параметры:

  • Включает в себя
  • Содержит
  • HasFlag (или HasFlags)
  • IsSet (это конечно вариант)

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


1 или, конечно, отправить как патч ...

Джон Скит
источник
1
Вы должны были пойти и упомянуть PostSharp LOL: o postsharp.org/blog/generic-constraints-for-enums-and-delegates
Сэм Харвелл,
1
Или на самом деле более простые HasAny () и HasAll ()
Кейт
1
Да, согласен, это даже лучше. colors.HasAny(Colors.Red | Colors.Blue)выглядит как очень читаемый код. =)
Blixt
1
Ага, мне тоже нравятся HasAny и HasAll. Пойду с этим.
Джон Скит,
5
Начиная с C # 7.3 (выпущен в мае 2018 г.), можно использовать ограничение where T : System.Enum. Об этом уже писали где-то в ветке; просто подумал, что повторю здесь.
Йеппе Стиг Нильсен,
16

Даррен, это сработало бы, если бы типы были конкретными перечислениями - чтобы общие перечисления работали, вы должны преобразовать их в целые числа (или, что более вероятно, uint), чтобы выполнить логическую математику:

public static bool IsSet( this Enum input, Enum matchTo )
{
    return ( Convert.ToUInt32( input ) & Convert.ToUInt32( matchTo ) ) != 0;
}
Ронни
источник
1
А если у вас смехотворное количество флагов, вы можете вызвать GetTypeCode () для аргументов и Convert.ToUint64 ()
Kit
Потрясающе, комбинацию Enum и Convert.ToUInt32я больше нигде не нашел. AFAIK, это единственное достойное решение Pre-Net-4, которое также работает в VB. Кстати, если matchToможет быть несколько битов флага, замените != 0на == Convert.ToUInt32(matchTo).
ToolmakerSteve
1
Обратите внимание, что Convert.ToUInt32при использовании перечисления будет использоваться Convert.ToUInt32(object)перегрузка, что означает, что среда CLR сначала будет упаковывать эти значения, а затем передавать их ToUInt32методу. В большинстве случаев это не имеет значения, но хорошо знать, что у вас будет довольно загруженный сборщик мусора, если вы будете использовать что-то подобное для анализа миллионов перечислений в секунду.
Groo
10

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

public abstract class Enums<Temp> where Temp : class {
    public static TEnum Parse<TEnum>(string name) where TEnum : struct, Temp {
        return (TEnum)Enum.Parse(typeof(TEnum), name); 
    }
}
public abstract class Enums : Enums<Enum> { }

Enums.IsSet<DateTimeKind>("Local")

Если вы хотите, вы можете предоставить Enums<Temp>частный конструктор и общедоступный вложенный абстрактный унаследованный класс с помощью Tempas Enum, чтобы предотвратить унаследованные версии для не перечислений.

SLaks
источник
8

Вы можете добиться этого с помощью IL Weaving и ExtraConstraints.

Позволяет написать этот код

public class Sample
{
    public void MethodWithDelegateConstraint<[DelegateConstraint] T> ()
    {        
    }
    public void MethodWithEnumConstraint<[EnumConstraint] T>()
    {
    }
}

Что компилируется

public class Sample
{
    public void MethodWithDelegateConstraint<T>() where T: Delegate
    {
    }

    public void MethodWithEnumConstraint<T>() where T: struct, Enum
    {
    }
}
Саймон
источник
7

Начиная с C # 7.3, вы можете использовать ограничение Enum для универсальных типов:

public static TEnum Parse<TEnum>(string value) where TEnum : Enum
{
    return (TEnum) Enum.Parse(typeof(TEnum), value);
}

Если вы хотите использовать перечисление Nullable, вы должны оставить ограничение исходной структуры:

public static TEnum? TryParse<TEnum>(string value) where TEnum : struct, Enum
{
    if( Enum.TryParse(value, out TEnum res) )
        return res;
    else
        return null;
}
Mik
источник
4

Это не отвечает на исходный вопрос, но теперь в .NET 4 есть метод Enum.HasFlag, который делает то, что вы пытаетесь сделать в своем примере.

Фил Девани
источник
Проголосовали за, потому что на данный момент большинство всех должны использовать .NET 4 (или выше), и поэтому им следует использовать этот метод вместо того, чтобы пытаться взломать его вместе.
CptRobby
Проголосовали. Однако в их решении используется бокс аргументации flag. .NET 4.0 исполнилось пять лет.
Jeppe Stig Nielsen
3

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

чашка
источник
7
где T: struct, IComparable, IFormattable, IConvertible - это самое близкое к enum :)
Kit
1

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

public static class EnumExtension
{
    public static bool IsSet<T>( this T input, T matchTo )
    {
        if (!typeof(T).IsEnum)
        {
            throw new ArgumentException("Must be an enum", "input");
        }
        return (input & matchTo) != 0;
    }
}
Скотт Дорман
источник
2
Спасибо, но это превращает проблему времени компиляции (ограничение where) в проблему времени выполнения (ваше исключение). Также вам все равно нужно преобразовать входные данные в целые числа, прежде чем вы сможете что-либо с ними делать.
Кейт,
1

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

public static class EnumExtensions
{
    public static bool ContainsFlag(this Enum source, Enum flag)
    {
        var sourceValue = ToUInt64(source);
        var flagValue = ToUInt64(flag);

        return (sourceValue & flagValue) == flagValue;
    }

    public static bool ContainsAnyFlag(this Enum source, params Enum[] flags)
    {
        var sourceValue = ToUInt64(source);

        foreach (var flag in flags)
        {
            var flagValue = ToUInt64(flag);

            if ((sourceValue & flagValue) == flagValue)
            {
                return true;
            }
        }

        return false;
    }

    // found in the Enum class as an internal method
    private static ulong ToUInt64(object value)
    {
        switch (Convert.GetTypeCode(value))
        {
            case TypeCode.SByte:
            case TypeCode.Int16:
            case TypeCode.Int32:
            case TypeCode.Int64:
                return (ulong)Convert.ToInt64(value, CultureInfo.InvariantCulture);

            case TypeCode.Byte:
            case TypeCode.UInt16:
            case TypeCode.UInt32:
            case TypeCode.UInt64:
                return Convert.ToUInt64(value, CultureInfo.InvariantCulture);
        }

        throw new InvalidOperationException("Unknown enum type.");
    }
}
Брайан Суровец
источник
0

если кому-то нужен общий IsSet (созданный из коробки на лету, который можно улучшить), и / или строка для преобразования Enum onfly (которое использует EnumConstraint, представленный ниже):

  public class TestClass
  { }

  public struct TestStruct
  { }

  public enum TestEnum
  {
    e1,    
    e2,
    e3
  }

  public static class TestEnumConstraintExtenssion
  {

    public static bool IsSet<TEnum>(this TEnum _this, TEnum flag)
      where TEnum : struct
    {
      return (((uint)Convert.ChangeType(_this, typeof(uint))) & ((uint)Convert.ChangeType(flag, typeof(uint)))) == ((uint)Convert.ChangeType(flag, typeof(uint)));
    }

    //public static TestClass ToTestClass(this string _this)
    //{
    //  // #generates compile error  (so no missuse)
    //  return EnumConstraint.TryParse<TestClass>(_this);
    //}

    //public static TestStruct ToTestStruct(this string _this)
    //{
    //  // #generates compile error  (so no missuse)
    //  return EnumConstraint.TryParse<TestStruct>(_this);
    //}

    public static TestEnum ToTestEnum(this string _this)
    {
      // #enum type works just fine (coding constraint to Enum type)
      return EnumConstraint.TryParse<TestEnum>(_this);
    }

    public static void TestAll()
    {
      TestEnum t1 = "e3".ToTestEnum();
      TestEnum t2 = "e2".ToTestEnum();
      TestEnum t3 = "non existing".ToTestEnum(); // default(TestEnum) for non existing 

      bool b1 = t3.IsSet(TestEnum.e1); // you can ommit type
      bool b2 = t3.IsSet<TestEnum>(TestEnum.e2); // you can specify explicite type

      TestStruct t;
      // #generates compile error (so no missuse)
      //bool b3 = t.IsSet<TestEnum>(TestEnum.e1);

    }

  }

Если кому-то все еще нужен пример для создания ограничения кодирования Enum:

using System;

/// <summary>
/// would be same as EnumConstraint_T&lt;Enum>Parse&lt;EnumType>("Normal"),
/// but writen like this it abuses constrain inheritence on System.Enum.
/// </summary>
public class EnumConstraint : EnumConstraint_T<Enum>
{

}

/// <summary>
/// provides ability to constrain TEnum to System.Enum abusing constrain inheritence
/// </summary>
/// <typeparam name="TClass">should be System.Enum</typeparam>
public abstract class EnumConstraint_T<TClass>
  where TClass : class
{

  public static TEnum Parse<TEnum>(string value)
    where TEnum : TClass
  {
    return (TEnum)Enum.Parse(typeof(TEnum), value);
  }

  public static bool TryParse<TEnum>(string value, out TEnum evalue)
    where TEnum : struct, TClass // struct is required to ignore non nullable type error
  {
    evalue = default(TEnum);
    return Enum.TryParse<TEnum>(value, out evalue);
  }

  public static TEnum TryParse<TEnum>(string value, TEnum defaultValue = default(TEnum))
    where TEnum : struct, TClass // struct is required to ignore non nullable type error
  {    
    Enum.TryParse<TEnum>(value, out defaultValue);
    return defaultValue;
  }

  public static TEnum Parse<TEnum>(string value, TEnum defaultValue = default(TEnum))
    where TEnum : struct, TClass // struct is required to ignore non nullable type error
  {
    TEnum result;
    if (Enum.TryParse<TEnum>(value, out result))
      return result;
    return defaultValue;
  }

  public static TEnum Parse<TEnum>(ushort value)
  {
    return (TEnum)(object)value;
  }

  public static sbyte to_i1<TEnum>(TEnum value)
  {
    return (sbyte)(object)Convert.ChangeType(value, typeof(sbyte));
  }

  public static byte to_u1<TEnum>(TEnum value)
  {
    return (byte)(object)Convert.ChangeType(value, typeof(byte));
  }

  public static short to_i2<TEnum>(TEnum value)
  {
    return (short)(object)Convert.ChangeType(value, typeof(short));
  }

  public static ushort to_u2<TEnum>(TEnum value)
  {
    return (ushort)(object)Convert.ChangeType(value, typeof(ushort));
  }

  public static int to_i4<TEnum>(TEnum value)
  {
    return (int)(object)Convert.ChangeType(value, typeof(int));
  }

  public static uint to_u4<TEnum>(TEnum value)
  {
    return (uint)(object)Convert.ChangeType(value, typeof(uint));
  }

}

надеюсь, это кому-то поможет.

SoLaR
источник
0

Я просто хотел добавить Enum в качестве общего ограничения.

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

Я решил просто создать structограничение и добавить проверку времени выполнения для IsEnum. Для преобразования переменной из T в Enum я сначала привел ее к объекту.

    public static Converter<T, string> CreateConverter<T>() where T : struct
    {
        if (!typeof(T).IsEnum) throw new ArgumentException("Given Type is not an Enum");
        return new Converter<T, string>(x => ((Enum)(object)x).GetEnumDescription());
    }
Юрген Штайнблок
источник