【问题标题】:Extending Enums, Overkill?扩展枚举,矫枉过正?
【发布时间】:2010-06-10 15:33:31
【问题描述】:

我有一个需要序列化为 EDI 格式的对象。对于这个例子,我们会说它是一辆汽车。汽车可能不是 b/c 选项随时间变化的最佳示例,但对于真实对象,枚举永远不会改变。

我有许多应用了自定义属性的枚举,如下所示。

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());
    ...

还有一种方法可以从StringValue中获取Enum Value进行反序列化:

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

最后,对于 UI,GetDisplayText() 用于显示用户友好的文本。

你怎么看?矫枉过正?有没有更好的办法?还是 Goldie Locks(刚刚好)?

只是想在我将其永久集成到我的个人框架中之前获得反馈。谢谢。

【问题讨论】:

  • 感谢所有不同的方法和讨论。这里有很多很棒的信息,我将使用所有这些信息来重构我原来的方法,以获得最大的性能、可重用性和可维护性。再次感谢大家。

标签: c# enums attributes extension-methods


【解决方案1】:

您用来序列化的部分很好。反序列化部分写得很尴尬。主要问题是您使用ToUpper() 来比较字符串,这很容易被破坏(想想全球化)。此类比较应使用string.Compare 代替,或者使用StringComparisonstring.Equals overload

另一件事是在反序列化期间一次又一次地执行这些查找会非常缓慢。如果您要序列化大量数据,这实际上可能非常明显。在这种情况下,您需要构建从 StringValue 到枚举本身的映射 - 将其放入静态 Dictionary&lt;string, RoofStyle&gt; 并将其用作往返的查找。换句话说:

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

附:次要细节,但如果您只希望有一个属性,您可能需要考虑使用Attribute.GetCustomAttribute 而不是MemberInfo.GetCustomAttributes。当您只需要一项时,没有理由摆弄所有数组。

【讨论】:

  • 字符串的要点。比较一下,我会整合。至于 Enum.Parse,如果我使用 Enum 值,这将起作用,但是这里的值是 StringValues、“GTR”、“TT”等。
  • @CkH:是的,我认出了它并删除了它。我认为您最好使用查找来代替,在编译时生成它们或在运行时延迟加载。
【解决方案2】:

就我个人而言,我认为您在滥用语言并试图以一种从未有过的方式使用枚举。我将创建一个静态类 RoofStyle,并创建一个简单的结构 RoofType,并为每个枚举值使用一个实例。

【讨论】:

  • 枚举值必须是原始类型。你需要澄清你在这里想说的是什么,因为写出来没有意义。
  • @Aaronaught,我想我很清楚,如果你不想这种行为不要使用枚举。枚举将标识符映射到整数;从我的角度来看,OP 本质上是将字符串属性附加到整数类型。将 int 用作字符串的存储容器是一种不好的做法。
  • 是的,但您没有解释创建“简单结构 RoofType”实际上如何解决将 4 个不同概念/标签组合到单个地图中的问题。无论哪种方式,您都不正确;从我记事起,向枚举字段添加属性一直是recommended way of annotating them
  • @Aaronaught,“RoofStyle.HardTop.DisplayText”很简单。我在该页面上没有看到任何表明这是推荐方法的内容。也许是,也许不是;这只是我不会采取的方法,让我们同意不同意。
  • Mike,它不会创建地图,它只是将数据放在一个稍微不同的地方,一个难以序列化且无法约束的地方(后者是枚举的主要目的)。如果这是一个性能问题,那么通过生成反向查找表很容易解决;否则,我看不出这是如何“滥用”语言的,以这种方式注释枚举是非常普遍的。
【解决方案3】:

你为什么不创建一个带有静态成员的类型,比如 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 时,枚举与此类结构非常相似。

...枚举支持字段...

.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

【讨论】:

  • 有趣的方法,但是,如果您要使用 DataContractSerializer 序列化 Car 对象,这将毫无用处。 DataMember 在任何静态成员上都会被忽略。因此,您永远无法在客户端上设置屋顶样式。枚举工作得更好。我需要可以在 WCF 中使用的对象。扩展和属性不会序列化,但它们仅用于服务器端。
  • Car 的成员不会是静态的。
  • 可以理解,但是 RoofStyle 的所有属性都是静态的,因此在客户端上,您将获得没有成员的 RoofStyle 类。客户不会知道 RoofStyles 是什么。
  • 您需要再次检查。显示和值不是静态的。只有“枚举”样式值是静态的。
  • 顺便说一句。您可能想要覆盖 GetHashCode()Equals(...),以便反序列化的实例等于静态值。
【解决方案4】:

这是我用于枚举类的基类:

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

【讨论】:

  • 好的,这在功能上等同于问题中的第一个代码 sn-p(带有修饰成员的枚举)。不过,最难的部分是什么 - 序列化它并映射到不同的成员类型?
  • @Aaronaught:我继续发布了我的一个类,它使枚举类更易于编写。我主要将它与 NHibernate 一起使用,但该模型适用于任何枚举。
  • 很公平。在复杂性方面对我来说看起来差不多,但它肯定是一个有效的替代方案。 +1。
  • @Aaronaught:我看到的唯一显着区别是缺少自定义属性。因此,可以说在StringValue 旁边添加新值并不那么复杂。此外,在属性解决方案中,消费者负责隐藏缓存。不是主要的缺点,但它确实使系统不那么独立。感谢您的热烈讨论。
【解决方案5】:

我认为它没有问题 - 实际上,我也是这样做的。通过这个,我实现了枚举的详细程度,并且可以定义当我使用枚举来请求数据时如何翻译枚举,例如。 RequestTarget.Character 将产生“char”。

【讨论】:

    【解决方案6】:

    不能说我曾经见过这种方式,但消费者代码相对简单,所以我可能会喜欢使用它。

    对我来说唯一突出的是消费者可能不得不处理空值 - 这可能会被删除。如果您可以控制属性(从它的声音可以控制),那么永远不会出现 GetDisplayText 或 GetStringValue 返回 null 的情况,因此您可以删除

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

    替换成

    return attribs[0].StringValue;
    

    为了简化消费者代码的接口。

    【讨论】:

    • 唯一的原因是这些扩展方法扩展了所有枚举。因此,如果要使用一个没有这些自定义属性的枚举,它会抛出异常而不是返回 null,这是期望的。
    • 返回调用枚举的值而不是 null 不是更好的选择吗(对于 String 和 Display)?
    【解决方案7】:

    恕我直言,设计很扎实,而且会奏效。 但是,反射往往有点慢,所以如果在紧密循环中使用这些方法,可能会减慢整个应用程序的速度。

    您可以尝试将返回值缓存到 Dictionary&lt;RoofStyle, string&gt; 中,这样它们只会反映一次,然后从缓存中获取。

    类似这样的:

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

    【讨论】:

    • 字典缓存不会破坏自定义属性的目的吗?我必须制作多个字典来保存字符串值、显示文本等。我​​知道在 4.0 中我可以缓存一个元组,这肯定会提供一个很好的缓存解决方案。当我们迁移到 4.0 时,我可能会整合它。谢谢
    【解决方案8】:

    我知道这个问题已经得到解答,但不久前我发布了以下代码片段on my personal blog,它演示了使用扩展方法伪造 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
        }
    }
    

    【讨论】:

    • 我喜欢这种方法,但它对我的需求不够通用。我必须为我拥有的每个 Enum 编写一个单独的扩展类,并且有很多。我肯定会使用这种方法,并且会在适用时使用它。感谢分享,非常干净,非常适合扩展方法使用。
    猜你喜欢
    • 2014-11-18
    • 2017-05-03
    • 2014-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-17
    • 2013-01-10
    • 2014-08-09
    相关资源
    最近更新 更多