Расширяем Enums, Overkill? - PullRequest
       42

Расширяем Enums, Overkill?

4 голосов
/ 10 июня 2010

У меня есть объект, который нужно сериализовать в формат EDI.Для этого примера мы скажем, что это автомобиль.Автомобиль может быть не лучшим примером того, как опции b / c меняются со временем, но для реального объекта Enums никогда не изменится.

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

public enum RoofStyle
{
    [DisplayText("Glass Top")]
    [StringValue("GTR")]
    Glass,
    [DisplayText("Convertible Soft Top")]
    [StringValue("CST")]
    ConvertibleSoft,
    [DisplayText("Hard Top")]
    [StringValue("HT ")]
    HardTop,
    [DisplayText("Targa Top")]
    [StringValue("TT ")]
    Targa,
}

Доступ к атрибутам осуществляется через методы расширения:

public static string GetStringValue(this Enum value)
{
    // Get the type
    Type type = value.GetType();

    // Get fieldinfo for this type
    FieldInfo fieldInfo = type.GetField(value.ToString());

    // Get the stringvalue attributes
    StringValueAttribute[] attribs = fieldInfo.GetCustomAttributes(
        typeof(StringValueAttribute), false) as StringValueAttribute[];

    // Return the first if there was a match.
    return attribs.Length > 0 ? attribs[0].StringValue : null;
}

public static string GetDisplayText(this Enum value)
{
    // Get the type
    Type type = value.GetType();

    // Get fieldinfo for this type
    FieldInfo fieldInfo = type.GetField(value.ToString());

    // Get the DisplayText attributes
    DisplayTextAttribute[] attribs = fieldInfo.GetCustomAttributes(
        typeof(DisplayTextAttribute), false) as DisplayTextAttribute[];

    // Return the first if there was a match.
    return attribs.Length > 0 ? attribs[0].DisplayText : value.ToString();
}

Существует специальный сериализатор EDIкоторый сериализуется на основе атрибутов StringValue следующим образом:

    StringBuilder sb = new StringBuilder();
    sb.Append(car.RoofStyle.GetStringValue());
    sb.Append(car.TireSize.GetStringValue());
    sb.Append(car.Model.GetStringValue());
    ...

Существует еще один метод, который может получить значение Enum из StringValue для десериализации:

   car.RoofStyle = Enums.GetCode<RoofStyle>(EDIString.Substring(4, 3))

Определен как:

public static class Enums
    {
        public static T GetCode<T>(string value)
        {
            foreach (object o in System.Enum.GetValues(typeof(T)))
            {
                if (((Enum)o).GetStringValue() == value.ToUpper())
                    return (T)o;
            }
            throw new ArgumentException("No code exists for type " + typeof(T).ToString() + " corresponding to value of " + value);
        }
} 

И, наконец, для пользовательского интерфейса GetDisplayText() используется для отображения удобного для пользователя текста.

Что вы думаете?Overkill?Есть ли способ лучше?или Голди Локс (просто верно)?

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

Ответы [ 8 ]

7 голосов
/ 10 июня 2010

Часть, которую вы используете для сериализации, в порядке.Часть десериализации написана неловко.Основная проблема в том, что вы используете ToUpper() для сравнения строк, которые легко разбиваются (подумайте о глобализации).Такое сравнение должно выполняться вместо string.Compare или строки . Перегрузка Equals , которая принимает StringComparison.

Другое дело, что выполнение этих поисков снова и снова во время десериализациисобирается довольно медленно.Если вы сериализуете много данных, это может быть весьма заметно.В этом случае вы захотите построить карту от StringValue до самого перечисления - добавьте ее в статический Dictionary<string, RoofStyle> и используйте ее в качестве поиска для кругового обхода.Другими словами:

public static class Enums
{
    private static Dictionary<string, RoofStyle> roofStyles =
        new Dictionary<string, RoofStyle>()
    {
        { "GTR", RoofStyle.Glass },
        { "CST", RoofStyle.ConvertibleSoft },
        { "HT ", RoofStyle.HardTop },
        { "TT ", RoofStyle.TargaTop }
    }

    public static RoofStyle GetRoofStyle(string code)
    {
        RoofStyle result;
        if (roofStyles.TryGetValue(code, out result))
            return result;
        throw new ArgumentException(...);
    }
}

Это не так "универсально", но гораздо эффективнее.Если вам не нравится дублирование строковых значений, извлеките коды как константы в отдельный класс.

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

static Dictionary<string, T> CreateEnumLookup<T>()
{
    return Enum.GetValues(typeof(T)).ToDictionary(o => ((Enum)o).GetStringValue(),
        o => (T)o);
}

PS Незначительные детали, но вы можете рассмотреть возможность использования Attribute.GetCustomAttribute вместо MemberInfo.GetCustomAttributes, если вы ожидаете, что будет только один атрибут.Нет никакой причины для всего массива, когда вам нужен только один элемент.

4 голосов
/ 10 июня 2010

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

3 голосов
/ 10 июня 2010

Почему бы вам не создать тип со статическими элементами, такими как mikerobi сказал

Пример ...

public class RoofStyle
{
    private RoofStyle() { }
    public string Display { get; private set; }
    public string Value { get; private set; }

    public readonly static RoofStyle Glass = new RoofStyle
    {
        Display = "Glass Top",  Value = "GTR",
    };
    public readonly static RoofStyle ConvertibleSoft = new RoofStyle
    {
        Display = "Convertible Soft Top", Value = "CST",
    };
    public readonly static RoofStyle HardTop = new RoofStyle
    {
        Display = "Hard Top", Value = "HT ",
    };
    public readonly static RoofStyle Targa = new RoofStyle
    {
        Display = "Targa Top", Value = "TT ",
    };
}

КСТАТИ ...

При компиляции в IL Enum очень похож на структуру этого класса.

... Перечислить вспомогательные поля ...

.field public specialname rtspecialname int32 value__
.field public static literal valuetype A.ERoofStyle Glass = int32(0x00)
.field public static literal valuetype A.ERoofStyle ConvertibleSoft = int32(0x01)
.field public static literal valuetype A.ERoofStyle HardTop = int32(0x02)
.field public static literal valuetype A.ERoofStyle Targa = int32(0x03)

... Поля поддержки класса ...

.field public static initonly class A.RoofStyle Glass
.field public static initonly class A.RoofStyle ConvertibleSoft
.field public static initonly class A.RoofStyle HardTop
.field public static initonly class A.RoofStyle Targa
2 голосов
/ 11 июня 2010

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

public abstract class Enumeration<T, TId> : IEquatable<T> where T : Enumeration<T, TId>
{
    public static bool operator ==(Enumeration<T, TId> x, T y)
    {
        return Object.ReferenceEquals(x, y) || (!Object.ReferenceEquals(x, null) && x.Equals(y));
    }

    public static bool operator !=(Enumeration<T, TId> first, T second)
    {
        return !(first == second);
    }

    public static T FromId(TId id)
    {
        return AllValues.Where(value => value.Id.Equals(id)).FirstOrDefault();
    }

    public static readonly ReadOnlyCollection<T> AllValues = FindValues();

    private static ReadOnlyCollection<T> FindValues()
    {
        var values =
            (from staticField in typeof(T).GetFields(BindingFlags.Static | BindingFlags.Public)
            where staticField.FieldType == typeof(T)
            select (T) staticField.GetValue(null))
            .ToList()
            .AsReadOnly();

        var duplicateIds =
            (from value in values
            group value by value.Id into valuesById
            where valuesById.Skip(1).Any()
            select valuesById.Key)
            .Take(1)
            .ToList();

        if(duplicateIds.Count > 0)
        {
            throw new DuplicateEnumerationIdException("Duplicate ID: " + duplicateIds.Single());
        }

        return values;
    }

    protected Enumeration(TId id, string name)
    {
        Contract.Requires(((object) id) != null);
        Contract.Requires(!String.IsNullOrEmpty(name));

        this.Id = id;
        this.Name = name;
    }

    protected Enumeration()
    {}

    public override bool Equals(object obj)
    {
        return Equals(obj as T);
    }

    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }

    public override string ToString()
    {
        return this.Name;
    }

    #region IEquatable

    public virtual bool Equals(T other)
    {
        return other != null && this.IdComparer.Equals(this.Id, other.Id);
    }
    #endregion

    public virtual TId Id { get; private set; }

    public virtual string Name { get; private set; }

    protected virtual IEqualityComparer<TId> IdComparer
    {
        get { return EqualityComparer<TId>.Default; }
    }
}

Реализация будет выглядеть так:

public sealed class RoofStyle : Enumeration<RoofStyle, int>
{
    public static readonly RoofStyle Glass = new RoofStyle(0, "Glass Top", "GTR");
    public static readonly RoofStyle ConvertibleSoft = new RoofStyle(1, "Convertible Soft Top", "CST");
    public static readonly RoofStyle HardTop = new RoofStyle(2, "Hard Top", "HT ");
    public static readonly RoofStyle Targa = new RoofStyle(3, "Targa Top", "TT ");

    public static RoofStyle FromStringValue(string stringValue)
    {
        return AllValues.FirstOrDefault(value => value.StringValue == stringValue);
    }

    private RoofStyle(int id, string name, string stringValue) : base(id, name)
    {
        StringValue = stringValue;
    }

    public string StringValue { get; private set; }
}

Вы бы использовали его во время сериализации следующим образом:

var builder = new StringBuilder();

builder.Append(car.RoofStyle.StringValue);
...

Для десериализации:

car.RoofStyle = RoofStyle.FromStringValue(EDIString.Substring(4, 3));
1 голос
/ 11 июня 2010

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

using System;
using System.Collections.Generic;

namespace ScratchPad
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var p = new Program();
            p.Run();
        }

    private void Run()
    {
        double earthWeight = 175;
        double mass = earthWeight / Planet.Earth.SurfaceGravity();

        foreach (Planet planet in Enum.GetValues(typeof(Planet))) {
            Console.WriteLine("Your weight on {0} is {1}", planet, planet.SurfaceWeight(mass));
        }
    }
}

public enum Planet
{
    Mercury,
    Venus,
    Earth,
    Mars,
    Jupiter,
    Saturn,
    Uranus,
    Neptune
}

public static class PlanetExtensions
{
    private static readonly Dictionary<Planet, PlanetData> planetMap = new Dictionary<Planet, PlanetData>
      {
          {Planet.Mercury, new PlanetData(3.303e+23, 2.4397e6)},
          {Planet.Venus, new PlanetData(4.869e+24, 6.0518e6)},
          {Planet.Earth, new PlanetData(5.976e+24, 6.37814e6)},
          {Planet.Mars, new PlanetData(6.421e+23, 3.3972e6)},
          {Planet.Jupiter, new PlanetData(1.9e+27,   7.1492e7)},
          {Planet.Saturn, new PlanetData(5.688e+26, 6.0268e7)},
          {Planet.Uranus, new PlanetData(8.686e+25, 2.5559e7)},
          {Planet.Neptune, new PlanetData(1.024e+26, 2.4746e7)}
      };

    private const double G = 6.67300E-11;

    public static double Mass(this Planet planet)
    {
        return GetPlanetData(planet).Mass;
    }

    public static double Radius(this Planet planet)
    {
        return GetPlanetData(planet).Radius;
    }

    public static double SurfaceGravity(this Planet planet)
    {
        PlanetData planetData = GetPlanetData(planet);

        return G * planetData.Mass / (planetData.Radius * planetData.Radius);
    }

    public static double SurfaceWeight(this Planet planet, double mass)
    {
        return mass * SurfaceGravity(planet);
    }

    private static PlanetData GetPlanetData(Planet planet)
    {
        if (!planetMap.ContainsKey(planet))
            throw new ArgumentOutOfRangeException("planet", "Unknown Planet");

        return planetMap[planet];
    }

    #region Nested type: PlanetData

    public class PlanetData
    {            
        public PlanetData(double mass, double radius)
        {
            Mass = mass;
            Radius = radius;
        }

        public double Mass { get; private set; }
        public double Radius { get; private set; }
    }

    #endregion
    }
}
1 голос
/ 10 июня 2010

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

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

return attribs.Length > 0 ? attribs[0].StringValue : null;

и замените его на

return attribs[0].StringValue;

для упрощения интерфейса для потребительского кода.

1 голос
/ 10 июня 2010

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

Вы можете попробовать кэшировать возвращаемые значения в Dictionary<RoofStyle, string>, чтобы ониотражается только один раз, а затем извлекается из кэша.

Примерно так:

    private static Dictionary<Enum, string> stringValues 
      = new Dictionary<Enum,string>();

    public static string GetStringValue(this Enum value)
    {
        if (!stringValues.ContainsKey(value))
        {
            Type type = value.GetType();
            FieldInfo fieldInfo = type.GetField(value.ToString());
            StringValueAttribute[] attribs = fieldInfo.GetCustomAttributes(
                typeof(StringValueAttribute), false) as StringValueAttribute[];
            stringValues.Add(value, attribs.Length > 0 ? attribs[0].StringValue : null);
        }
        return stringValues[value];
    }
1 голос
/ 10 июня 2010

Я не вижу проблем с этим - на самом деле, я делаю то же самое.Этим я достигаю многословия с помощью enum и могу определить, как enum должен переводиться, когда я использую его для запроса данных, например.RequestTarget.Character приведет к «char».

...