Пользовательские классы перечисление


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

Я придумал следующий абстрактный класс перечисление:

Перечисление

public abstract partial class Enumeration : IConvertible, IComparable, IFormattable
{
    public string Name { get; }
    public int Value { get; }

    protected Enumeration(int id, string name)
    {
        Value = id;
        Name = name;
    }

    #region Equality members

    public override bool Equals(object obj)
    {
        var otherValue = obj as Enumeration;
        if (otherValue == null)
        {
            return false;
        }
        var typeMatches = GetType() == obj.GetType();
        var valueMatches = Value.Equals(otherValue.Value);
        return typeMatches && valueMatches;
    }

    protected bool Equals(Enumeration other)
    {
        return string.Equals(Name, other.Name) && Value == other.Value;
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ Value;
        }
    }

    #endregion

    #region Implementation of IComparable

    public int CompareTo(object other)
    {
        return Value.CompareTo(((Enumeration)other).Value);
    }

    #endregion

    #region ToString methods

    public string ToString(string format)
    {
        if (string.IsNullOrEmpty(format))
        {
            format = "G";
        }
        if (string.Compare(format, "G", StringComparison.OrdinalIgnoreCase) == 0)
        {
            return Name;
        }
        if (string.Compare(format, "D", StringComparison.OrdinalIgnoreCase) == 0)
        {
            return Value.ToString();
        }
        if (string.Compare(format, "X", StringComparison.OrdinalIgnoreCase) == 0)
        {
            return Value.ToString("X8");
        }
        throw new FormatException("Invalid format");
    }

    public override string ToString() => ToString("G");
    public string ToString(string format, IFormatProvider formatProvider) => ToString(format);

    #endregion

    #region Implementation of IConvertible

    TypeCode IConvertible.GetTypeCode() => TypeCode.Int32;
    bool IConvertible.ToBoolean(IFormatProvider provider) => Convert.ToBoolean(Value, provider);
    char IConvertible.ToChar(IFormatProvider provider) => Convert.ToChar(Value, provider);
    sbyte IConvertible.ToSByte(IFormatProvider provider) => Convert.ToSByte(Value, provider);
    byte IConvertible.ToByte(IFormatProvider provider) => Convert.ToByte(Value, provider);
    short IConvertible.ToInt16(IFormatProvider provider) => Convert.ToInt16(Value, provider);
    ushort IConvertible.ToUInt16(IFormatProvider provider) => Convert.ToUInt16(Value, provider);
    int IConvertible.ToInt32(IFormatProvider provider) => Value;
    uint IConvertible.ToUInt32(IFormatProvider provider) => Convert.ToUInt32(Value, provider);
    long IConvertible.ToInt64(IFormatProvider provider) => Convert.ToInt64(Value, provider);
    ulong IConvertible.ToUInt64(IFormatProvider provider) => Convert.ToUInt64(Value, provider);
    float IConvertible.ToSingle(IFormatProvider provider) => Convert.ToSingle(Value, provider);
    double IConvertible.ToDouble(IFormatProvider provider) => Convert.ToDouble(Value, provider);
    decimal IConvertible.ToDecimal(IFormatProvider provider) => Convert.ToDecimal(Value, provider);
    DateTime IConvertible.ToDateTime(IFormatProvider provider) => throw new InvalidCastException("Invalid cast.");
    string IConvertible.ToString(IFormatProvider provider) => ToString();
    object IConvertible.ToType(Type conversionType, IFormatProvider provider)
        => Convert.ChangeType(this, conversionType, provider);

    #endregion
}

public abstract partial class Enumeration
{
    private static readonly Dictionary<Type, IEnumerable<Enumeration>> _allValuesCache =
        new Dictionary<Type, IEnumerable<Enumeration>>();

    #region Parse overloads

    public static TEnumeration Parse<TEnumeration>(string name)
        where TEnumeration : Enumeration
    {
        return Parse<TEnumeration>(name, false);
    }

    public static TEnumeration Parse<TEnumeration>(string name, bool ignoreCase)
        where TEnumeration : Enumeration
    {
        return ParseImpl<TEnumeration>(name, ignoreCase, true);
    }

    private static TEnumeration ParseImpl<TEnumeration>(string name, bool ignoreCase, bool throwEx)
        where TEnumeration : Enumeration
    {
        var value = GetValues<TEnumeration>()
            .FirstOrDefault(entry => StringComparisonPredicate(entry.Name, name, ignoreCase));
        if (value == null && throwEx)
        {
            throw new InvalidOperationException($"Requested value {name} was not found.");
        }
        return value;
    }

    #endregion

    #region TryParse overloads

    public static bool TryParse<TEnumeration>(string name, out TEnumeration value)
        where TEnumeration : Enumeration
    {
        return TryParse(name, false, out value);
    }

    public static bool TryParse<TEnumeration>(string name, bool ignoreCase, out TEnumeration value)
        where TEnumeration : Enumeration
    {
        value = ParseImpl<TEnumeration>(name, ignoreCase, false);
        return value != null;
    }

    #endregion

    #region Format overloads

    public static string Format<TEnumeration>(TEnumeration value, string format)
        where TEnumeration : Enumeration
    {
        return value.ToString(format);
    }

    #endregion

    #region GetNames

    public static IEnumerable<string> GetNames<TEnumeration>()
        where TEnumeration : Enumeration
    {
        return GetValues<TEnumeration>().Select(e => e.Name);
    }

    #endregion

    #region GetValues

    public static IEnumerable<TEnumeration> GetValues<TEnumeration>()
        where TEnumeration : Enumeration
    {
        var enumerationType = typeof(TEnumeration);
        if (_allValuesCache.TryGetValue(enumerationType, out var value))
        {
            return value.Cast<TEnumeration>();
        }
        return AddValueToCache(enumerationType, enumerationType
            .GetFields(BindingFlags.Public | BindingFlags.Static)
            .Select(p => p.GetValue(enumerationType)).Cast<TEnumeration>());
    }

    private static IEnumerable<TEnumeration> AddValueToCache<TEnumeration>(Type key,
        IEnumerable<TEnumeration> value)
        where TEnumeration : Enumeration
    {
        _allValuesCache.Add(key, value);
        return value;
    }

    #endregion

    #region IsDefined overloads

    public static bool IsDefined<TEnumeration>(string name)
        where TEnumeration : Enumeration
    {
        return IsDefined<TEnumeration>(name, false);
    }

    public static bool IsDefined<TEnumeration>(string name, bool ignoreCase)
        where TEnumeration : Enumeration
    {
        return GetValues<TEnumeration>().Any(e => StringComparisonPredicate(e.Name, name, ignoreCase));
    }

    #endregion

    #region Helpers

    private static bool StringComparisonPredicate(string item1, string item2, bool ignoreCase)
    {
        var comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
        return string.Compare(item1, item2, comparison) == 0;
    }

    #endregion
}

Он разделен на 2 файла -

1 отвечает за основную логику, реализации интерфейса, защищенных членов и т. д. и 1 отвечает за статические вспомогательные методы, такие как Parse, TryParseи т. д.

Я реализовал все интерфейсы, что Enum класс обычно. Есть 2 свойства, чтобы получить доступ к Name и Value записи. В настоящее время нет поддержки Flags атрибут, но я мог бы работать это, если я нахожу это нужным.

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

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

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

Пример реализации

public class TestEnumeration : Enumeration
{
    public static TestEnumeration A = new TestEnumeration(0, nameof(A));
    public static TestEnumeration B = new TestEnumeration(1, nameof(B));
    //...

    private static readonly IEnumerable<TestEnumeration> _test;

    protected TestEnumeration(int id, string name)
        : base(id, name)
    {
    }

    static TestEnumeration()
    {
        _test = GetValues<TestEnumeration>();
    }

    public static IEnumerable<TestEnumeration> Values()
    {
        foreach (var entry in _test)
        {
            yield return entry;
        }
    }
}

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

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

Любая обратная связь приветствуется! :)



284
5
задан 31 марта 2018 в 03:03 Источник Поделиться
Комментарии
2 ответа

Комментарий


IEquatable<T>


protected bool Equals(Enumeration other)
{
return string.Equals(Name, other.Name) && Value == other.Value;
}

Две вещи об этом методе...


  • Если вы реализовали его, не понизить его до простого protected вспомогательный метод. Он принадлежит IEquatable<T> интерфейс так реализовать эту тоже.

  • Я нахожу это легче реализовать Equals пара forwarting звонок от Equals(object other) для Equals(T other) потому что последний является строго типизированным.


Ура! Исключение NullReferenceException!

Это other.Name будет дуть ли other это null так что удостоверьтесь, чтобы проверить все части выражения.


bool throwEx

Вы не должны использовать параметры, которые exeception бросая переключатель вкл./выкл. Вместо того, чтобы удалить ParseImpl и выполнить разбор одной из TryParse методы затем повторно использовать его в другом месте и бросить исключение Parse методы если necessasry.


readonly

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

Читабельность


  • partial class

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


  • Списки параметров


private static IEnumerable<TEnumeration> AddValueToCache<TEnumeration>(Type key,
IEnumerable<TEnumeration> value)
where TEnumeration : Enumeration

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

private static IEnumerable<TEnumeration> AddValueToCache<TEnumeration>(
Type key,
IEnumerable<TEnumeration> value
) where TEnumeration : Enumeration

Я считаю, что это гораздо легче читать.


  • Цепи для LINQ


var value = GetValues<TEnumeration>()
.FirstOrDefault(entry => StringComparisonPredicate(entry.Name, name, ignoreCase));

Аналогичное правило применяется в LINQ chanins. Либо нет разрывов строк или всех строк. Смешанные стили трудно читать.

    var value = 
GetValues<TEnumeration>()
.FirstOrDefault(entry =>
StringComparisonPredicate(
entry.Name,
name,
ignoreCase
)
);

Это (крайний пример), как я думаю, что длинные выражения должен быть отформатирован, если одной линии слишком долго.

    var value = 
GetValues<TEnumeration>()
.FirstOrDefault(entry => StringComparisonPredicate(entry.Name, name, ignoreCase));

Или просто так, чтобы можно было легко ее читать и, естественно, от

left to 
right

и не от

     right to 
left to
right


Именования

Вы имя свойства Name и Value но параметры непоследовательно name и id. Они должны быть name и value тоже.


Улучшения


Вы можете существенно упростить свой код путем внедрения генериков. Если вы сделаете свой базовый класс Enumeration<T> вы можете удалить все boilerplate кода от производного класса к новым общим Enumeration<T>. Компилятор будет генерировать static поля и свойства каждого T отдельно, поэтому вам не придется писать их самостоятельно больше. Вот короткий пример.

(Я удалил все интерфейсы или другие помощники, как Parse и TryParse или операторы для простоты, но они должны быть включены в фактические реализации позже.)

public abstract partial class Enumeration<T> 
{
private static readonly IDictionary<string, int> _valueCache = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private static readonly IDictionary<int, string> _nameCache = new Dictionary<int, string>();

public string Name { get; }

public int Value { get; }

protected Enumeration(string name, int value)
{
Value = value;
Name = name;
_valueCache.Add(name, value);
_nameCache.Add(value, name);
}

public static IEnumerable<string> Names => _nameCache.Values;

public static IEnumerable<int> Values => _valueCache.Values;

// Examples only! TODO: add null checks or redicrect `IComparable<T>`
public static bool operator >(Enumeration<T> left, Enumeration<T> right) => left.Value > right.Value;
public static bool operator <(Enumeration<T> left, Enumeration<T> right) => left.Value < right.Value;
}

Это как новый класс может быть реализован. У вас сейчас два словаря для кэширования как имена и значения для ускорения поиска.

Это все статично! Не нужно писать этот код снова и снова и снова...

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

public class TestEnumeration : Enumeration<TestEnumeration>
{
public readonly static TestEnumeration A = new TestEnumeration(nameof(A), 0);
public readonly static TestEnumeration B = new TestEnumeration(nameof(B), 1);

protected TestEnumeration(string name, int value)
: base(name, value)
{
}
}

Это то, что остается от вашего первоначального класса. Я swithched порядок параметров формы value и имя name и value потому что я считаю это тем более естественно.

Я также предлагаю осуществления IEquatable<Enumerable<T>> интерфейс перенаправить его в пользовательскую реализацию IEqualityComparer<Enumerable<T>> потому что он не только лучше инкапсулирует логику, но и лучше Equals метод принимает два параметра для left и right а потом работать с невидимым this и other.

Примеры использования:

TestEnumeration.A.Dump(); // (A, 0)

TestEnumeration.Names.Dump(); // A, B

(TestEnumeration.A > TestEnumeration.B).Dump(); // False
(TestEnumeration.B > TestEnumeration.A).Dump(); // True


Мне нравится идея Enumeration класс и я использую подобные решения сам много (сейчас совершенствуется), потому что они часто лучше, чем обычные enumс:


  • они могут иметь гораздо больше пользовательских функций

  • это легче писать расширения

  • они быстрее и эффективнее в тех случаях, когда иначе бокс будет участвовать напр. Dictionar<string, object>

3
ответ дан 31 марта 2018 в 07:03 Источник Поделиться

После включения предложений t3chb0t, класс теперь выглядит так:

[DebuggerDisplay("{Name} = {Value}")]
public abstract partial class Enumeration<T>
where T : Enumeration<T>
{
private static readonly IDictionary<int, Enumeration<T>> _valueCache =
new Dictionary<int, Enumeration<T>>();
public static IDictionary<int, Enumeration<T>> ValueCache
{
get
{
RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);
return _valueCache;
}
}

private static readonly IDictionary<string, Enumeration<T>> _nameCache =
new Dictionary<string, Enumeration<T>>(StringComparer.OrdinalIgnoreCase);
public static IDictionary<string, Enumeration<T>> NameCache
{
get
{
RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);
return _nameCache;
}
}

public string Name { get; }
public int Value { get; }

public static IEnumerable<string> Names => NameCache.Keys;
public static IEnumerable<int> Values => ValueCache.Keys;

protected Enumeration(string name, int value)
{
Name = name;
Value = value;
ValueCache.Add(value, this);
NameCache.Add(name, this);
}

public static T Parse(string name)
{
if (TryParse(name, out var value))
{
return value;
}
throw new InvalidOperationException($"Requested value {name} was not found.");
}

public static bool TryParse(string name, out T value)
{
if (NameCache.TryGetValue(name, out var containedValue))
{
value = (T)containedValue;
return true;
}
value = default(T);
return false;
}

public static string Format(T value, string format)
{
return value.ToString(format);
}

public static bool IsDefined(string name)
{
return NameCache.ContainsKey(name);
}
}

public abstract partial class Enumeration<T> : IConvertible
{
TypeCode IConvertible.GetTypeCode() => TypeCode.Int32;
bool IConvertible.ToBoolean(IFormatProvider provider) => Convert.ToBoolean(Value, provider);
char IConvertible.ToChar(IFormatProvider provider) => Convert.ToChar(Value, provider);
sbyte IConvertible.ToSByte(IFormatProvider provider) => Convert.ToSByte(Value, provider);
byte IConvertible.ToByte(IFormatProvider provider) => Convert.ToByte(Value, provider);
short IConvertible.ToInt16(IFormatProvider provider) => Convert.ToInt16(Value, provider);
ushort IConvertible.ToUInt16(IFormatProvider provider) => Convert.ToUInt16(Value, provider);
int IConvertible.ToInt32(IFormatProvider provider) => Value;
uint IConvertible.ToUInt32(IFormatProvider provider) => Convert.ToUInt32(Value, provider);
long IConvertible.ToInt64(IFormatProvider provider) => Convert.ToInt64(Value, provider);
ulong IConvertible.ToUInt64(IFormatProvider provider) => Convert.ToUInt64(Value, provider);
float IConvertible.ToSingle(IFormatProvider provider) => Convert.ToSingle(Value, provider);
double IConvertible.ToDouble(IFormatProvider provider) => Convert.ToDouble(Value, provider);
decimal IConvertible.ToDecimal(IFormatProvider provider) => Convert.ToDecimal(Value, provider);
DateTime IConvertible.ToDateTime(IFormatProvider provider) => throw new InvalidCastException("Invalid cast.");
string IConvertible.ToString(IFormatProvider provider) => ToString();

object IConvertible.ToType(Type conversionType, IFormatProvider provider)
=> Convert.ChangeType(this, conversionType, provider);
}

public abstract partial class Enumeration<T> : IComparable<Enumeration<T>>
{
public int CompareTo(Enumeration<T> other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
return Value.CompareTo(other.Value);
}
}

public abstract partial class Enumeration<T> : IFormattable
{
public string ToString(string format)
{
if (string.IsNullOrEmpty(format))
{
format = "G";
}
if (string.Compare(format, "G", StringComparison.OrdinalIgnoreCase) == 0)
{
return Name;
}
if (string.Compare(format, "D", StringComparison.OrdinalIgnoreCase) == 0)
{
return Value.ToString();
}
if (string.Compare(format, "X", StringComparison.OrdinalIgnoreCase) == 0)
{
return Value.ToString("X8");
}
throw new FormatException("Invalid format");
}

public override string ToString() => ToString("G");
public string ToString(string format, IFormatProvider formatProvider) => ToString(format);
}

public abstract partial class Enumeration<T> : IEquatable<Enumeration<T>>
{
public bool Equals(Enumeration<T> other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Value == other.Value;
}

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((Enumeration<T>)obj);
}

public override int GetHashCode()
{
return Value.GetHashCode();
}

public static bool operator >(Enumeration<T> item1, Enumeration<T> item2) => item1.CompareTo(item2) > 0;
public static bool operator <(Enumeration<T> item1, Enumeration<T> item2) => item1.CompareTo(item2) < 0;

public static explicit operator Enumeration<T>(int value)
{
return (Enumeration<T>)Activator.CreateInstance(
typeof(T),
BindingFlags.NonPublic | BindingFlags.Instance, null,
new object[] { value.ToString(), value }, null);
}
}

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

Все вспомогательные методы не должны объявить аргумент универсального типа, так как они могут использовать класс' один. Также большая часть перегрузки были удалены, из-за Теперь имея 2 индивидуальная словарь схрон, который позволяет StringComparer должен быть принят во время инициализации, которая также означает, что все Nameбудут лечить с помощью StringComparer.OrdinalIgnoreCase.

Всех реализаций интерфейсов были разделены на частичные файлы. IEquatable<Enumeration<T>> унаследовала и реализовала вместе с некоторыми изменениями в равенстве государств-членов. Теперь они принимают только Value объекта к рассмотрению. Как несколько повторяющихся значений не допускается. Оно призвано работать таким образом, потому что из моего опыта с enums это довольно головная боль для работы с повторяющимися значениями в той же enum.

2 операторы были переопределены < && > и явное приведение от int для Enumeration<T> была добавлена.

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

1
ответ дан 1 апреля 2018 в 08:04 Источник Поделиться