65.9K
CodeProject 正在变化。 阅读更多。
Home

使用反射自动测试所有 Enum 类型是否有重复值和易出错的 Flags 值

starIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIconemptyStarIcon

1.80/5 (2投票s)

2007 年 7 月 13 日

5分钟阅读

viewsIcon

30992

downloadIcon

187

使用反射自动测试所有 Enum 类型是否有重复值和易出错的 Flags 值

引言

在实际的 C#/.NET 项目中,存在大量的枚举定义。而且我发现很多时候我们需要每个枚举项都有一个与众不同的值,但事实是,开发人员很可能给多个枚举项分配相同的值。这是错误的根源。另外,更值得关注的是 [Flags] 枚举,它的值应该是 0x1、0x2、0x4 等,而不是 0x7。本文通过反射阐述了一种可重用的方法来自动检查这些与枚举定义相关的项。并且将这些规则应用于单元测试框架,例如 NUnit,非常容易。

Screenshot - nunit_test_enum.png

背景

在使用 NUnit 测试代码之前,应在 preamble 中包含 using NUnit.Framework;。并且

向 NUnit dll 添加引用:nunit-framework.dll,只需要这一个。

在 .NET 1.1 上通过 C# 进行测试

演示项目本身无法运行,只需使用 NUnit 进行测试。

动机

.NET 中的反射为许多有趣的事情打开了一扇门,事实上,本文的动机源于另一个故事

所有枚举名称的本地化

public enum Unit
{
  mm,
  cm,
  pt,
  Inch,
}
这在绘图软件中非常常见。我想将这些项本地化到字符串表中,仅仅将字符串表放在枚举旁边并不足以同步这两者。我需要确保当有人向枚举 Unit 添加新的支持单位时,我的软件会通知开发人员,否则相应的本地化将不会完成。
public enum StringID
{
  Unit_UI_mm,
  Unit_UI_cm,
  Unit_UI_pt,
  Unit_UI_Inch,
}
我使用了这两个枚举之间的命名约定。所以可以通过以下方式检查:
foreach(string unit_name in Enum.GetNames( typeof(Unit) ) )
{
  if( Enum.IsDefined( typeof(Unit), "Unit_UI_" + unit_name ) == false)
  {
    Debug.Assert(false, string.Format("need to add Localization for [{0}]", unit_name) );
  }
}

使用代码

要使用代码,只需将 EnumTester.cs 文件添加到你的项目中,然后重新编译你的程序集,再将其交给 NUnit 进行测试。

using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Collections;
using NUnit.Framework;

namespace EnumTester
{
    /// <summary>
    /// A NUnit class
    /// </summary>
    [TestFixture(Description="Test Enum definition: duplicated value, or error-prone value for [Flags]ed Enum such as 3")]
    public class EnumTest
    {
        [Test(Description="Test All the Enum types defined in this assembly")]
        public void Enum_Check_All()
        {
            Assembly assem = Assembly.GetExecutingAssembly();
            EnumTester_Common.check_assembly_enum_type( assem, EnumTest_Type.All );
        }
}

基本思想是使用反射检测程序集中的所有枚举类型,然后检查每个枚举的名称定义和值。如果你对枚举很了解,只想跳过测试,只需将以下特性添加到你的枚举或单个枚举项即可

  [Skip_Enum_CheckAttribute]
  public enum I_Know_My_Enum_Well_Even_Its_error_prone
  {
    Name,
    name, //diff from the former only by case
    s0, //The digit 0
    sO, //The upper case character between 'N' and 'P'
    others = 0, //The value same as Name
  }

  public enum MyEnum
  {
    Name,
    name, //diff from the former only by case
    [Skip_Enum_CheckAttribute]
    s0, //Check of s0 will be skipped, but checking of others still take it account into.
    sO, //The upper case character between 'N' and 'P'
    others = 0, //The value same as Name
  }
请注意,Skip_Enum_CheckAttribute 的特性后缀是可选的。

为了更精细地控制测试,我包含了一个名为

EnumTest_Type
的枚举,用于精确定义应在程序集上执行哪种类型的测试。例如,要检测值重复,请使用
  [Test(Description="Test All the Enum types defined in this assembly")]
  public void Enum_Check_All()
  {
      Assembly assem = Assembly.GetExecutingAssembly();
      EnumTester_Common.check_assembly_enum_type( assem, EnumTest_Type.Duplicated_Value );
  }

关注点

检查常量名称仅大小写不同

    internal enum Enum_Diff_Only_Case
    {
        abcdefghijklmnopqrstuvwxyz,

        ABCDEFGHIJKLMNOPQRSTUVWXYZ,
    }

检查常量名称仅数字 0 和字符 o/O 不同

    internal enum Enum_Digit_0_Upper_Char_O
    {
        Guess_digit_or_char_0,
        Guess_digit_or_char_O,
    }

检查常量名称仅数字 1 和字符 l/L 不同

    internal enum Enum_Digit_1_Lower_Char_l
    {
        Guess_digit_or_char_1, //The decimal between 0 and 2
        Guess_digit_or_char_l, //the lower case of letter L
    }

枚举的技巧、窍门和陷阱

CodeProject 上有几篇文章讨论了如何操作枚举,特别是通过反射,这里是我自己收集的部分技巧和窍门

在枚举定义中的最后一个项后面可以添加逗号。

我认为这比 C 的规则方便多了

int ia[] = {1, 2, 3, 4,  };
最后一个逗号仅在上文的结构中允许,在枚举中不允许。这个小小的改进对枚举很有意义,因为你通常希望重新组织枚举项。

项的值派生自其最新的前一个同级项

public enum WeekDay
{
  SunDay ,       //The first default value always 0
  MonDay = 2,    //You can explicitly specify a value
  TuesDay = 2,   //Even it's duplicated
  Wednesday ,    //Wensday will be 3, determined by Tuesday's value
  Thursday ,     //will be 4, determined by Wednesday
  Friday = 100,  //Ok
  Saturday,      //Will be 101
}
上面注释的规则与 C/C++ 相同。

Enum.GetNames( Type enumType) 将按定义顺序返回名称

在有多个项具有相同值的情况下,Enum.GetName(Type enumType, object value) 也将按上述顺序返回第一个项。

public enum EnumTest: int
{
  None = 0, 
  One  = 0,
}

Console.WriteLine( Enum.GetName(typeof(EnumTest), (int)0 ) ); //Get None

public enum EnumTest: int
{
  One  = 0,
  None = 0, 
}

Console.WriteLine( Enum.GetName(typeof(EnumTest), (int)0 ) ); //Get One

0 是特殊的枚举值

你可以直接为枚举变量赋值 0,其他值不允许
MyEnum val = 0;
val = 1; //Must cast

当需要强制转换时,请始终使用 (MyEnum) 语法,GetObject() as MyEnum 不允许

枚举是值类型。“as”仅适用于引用类型。这是所有值类型的一般规则,不仅限于枚举。

定义枚举的底层类型

  public enum MyColors: long
  {
      ...,
  }
至少在 C# 中,int 是默认的,大多数情况下都可以。我想再次强调,char *不是*枚举的合法底层类型。

获取枚举的底层类型,以及枚举的非托管 sizeof

Type underlying_type = MyEnum.GetUnderlyingType();

int unmanaged_size = System.Runtime.InteropServices.Marshal.SizeOf(underlying_type);

//The following line will occur an exception
int unmanaged_size = System.Runtime.InteropServices.Marshal.SizeOf( MyEnum );

即使指定了 csc /w:4,C# 编译器也不会就易出错的 [Flags] 枚举值发出任何警告。

[Flags]
public enum MyEnum
{
  First  = 0x1;
  Second = 0x3;
}

//Combine MyEnum.First | MyEnum.Second will result in the weird result in your code.
//This article will tackle this issue by auto-detect such a case.

[Effective C#] (Item 8) 的指令:将 0 设为枚举的有效值

简单地说,值类型默认初始化为 0。有关更多讨论,请参阅本书。

获取所有已定义的枚举常量

string[] all_names = Enum.GetNames( typeof(MyEnum) );
或者,也可以通过更难的方式做到同样的事情
  ArrayList all_names = new ArrayList();
  Type t = typeof(MyEnum);
  FieldInfo[] fields = t.GetFields( BindingFlags.Public | BindingFlags.Static);
  foreach(FieldInfo f in fields)
  {
     all_names.Add( f.Name );
  }

检测给定类型是否为枚举类型

  Type t = null; //From Reflection
  if( t.BaseType == typeof(Enum) ) {...}

枚举的合法底层类型

错误 CS1008:预期类型为 byte、sbyte、short、ushort、int、uint、long 或 ulong

我通过 try-error 得到了它,所以你不需要。请注意,char *不是*枚举的有效底层类型。

如何通过名称获取枚举变量的实例?

enum_val = (MyEnum_WeekDay) TypeDescriptor.GetConverter(enum_val).ConvertFrom("Sunday");
我长期以来习惯于以下方式,但不知道上述方式,幸运的是,在 Dactyl 的建议下(谢谢!),后者的速度比使用 TypeConverter 类快 3 倍。
Enum.Parse( typeof(MyEnum_WeekDay), "Sunday") ;

如何通过 int 值获取枚举变量的实例?

如果你可以在编译时访问枚举类型,那就更简单了
MyEnum val = (MyEnum)300;
Console.WriteLine( ((int)val).ToString() ); //Will output 300 if 300 is not a defined value
即使 300 不是定义的值,也不会抛出异常。当不知道编译时枚举类型时,会更困难。
Convert.ChangeType( (int)300, runtime_enum_type );
但这将导致以下异常
System.InvalidCastException: Invalid cast from System.Int32 to Client+TestEnum.
即使运行时枚举类型的底层类型恰好是 int。请注意,反向转换是可以的。
Convert.ChangeType( enum_val, typeof(long) );
即使 long 不是枚举的底层类型。也许有一个简单的方法我只是错过了(如果有,请告诉我),这是我自己的方法
    Type underlying_type = t.GetUnderlyingType();
    string enum_name =
      Enum.GetName(t, Convert.ChangeType( int_value, underlying_type ) );

    Enum.Parse(t, enum_name );
请注意,将原始十进制数转换为实际底层类型是必须的,否则将抛出以下异常
System.ArgumentException: Enum underlying type and the object must be same type or object
must be a String.  Type passed in was System.Int64; the enum underlying type was System.In
t16.
同样的规则也适用于以下方法
static Enum.IsDefined( Type enum_type, object obj)
obj 的有效类型要么是 string,要么是枚举的精确底层类型。当然,对于字符串,它应该是枚举常量。上面的片段可用于检测给定的字符串或 int/short 等是否是枚举中的已定义项。

历史

这是我第一次在 CodeProject 发帖。希望这对您有所帮助。
© . All rights reserved.