细说枚举

枚举是 C# 中最有意思的一部分,大部分开发人员只了解其中的一小部分,甚至网上绝大多数的教程也只讲解了枚举的一部分。那么,我将通过这篇文章向大家具体讲解一下枚举的知识。我将从大家都了解的部分开始讲解,然后再讲解大家所不知道的或者了解很少的部分。

零、基础知识

枚举是由开发人员声明的一种 值类型 ,它在编译时就声明了一种 具名常量值 。使用枚举可以使我们的代码简单易读,我们先来看一下两个代码段:

// 代码段 1
void Method(int country)
{
    switch (country)
    {
        case 0:
            // more code
            break;
        case 1:
            // more code
            break;
        case 2:
            // more code
            break;
        case 3:
            // more code
            break;
        default:
            // more code 
            break;
    }
}

// 代码段 2
void Method(Country country)
{
    switch (country)
    {
        case Country.CN:
            // more code
            break;
        case Country.JP:
            // more code
            break;
        case Country.UK:
            // more code
            break;
        case Country.USA:
            // more code
            break;
        default:
            // more code 
            break;
    }
}

从上面的两个代码段我们可以看到两者有明显的区别。第一段代码中的 case 值我们几乎完全不知道代表了什么是什么意思,但是第二段代码我们使用了枚举,通过 case 值马上就可以知道所要表达的意思。同样利用枚举值替代布尔值也可以改善代码的可读性,例如我们要开发控制台灯打开关闭的程序,代码可以这么写 LightOperating(True) ,但是这种代码我们无法看出具体要干什么,现在我们将代码改动一下 LightOperating(Light.On) 。经过修改代码就很容易看出所要表达的意思。

  1. 枚举定义与取值
    定义枚举有两种方式,分别是普通方式和自定义方式。不管使用哪种方式都需要用的关键字 enum 来标识这个类型为枚举类型,并且枚举值都是作为整数常量来实现的。下面我们就来看一下这两种方式怎么定义枚举的。普通方式是我们经常用到的,也是默认的方式。这种方式很简单,代码如下:

    enum Country
    {
        CN,
        UK,
        JP,
        USA
    }
    

    在上面的代码段中我们定义了一个国家枚举,第一个枚举值对应的整数常量是 0 ,第二个枚举值对应的整数常量是 1 ,以此类推后面的枚举值分别对应的整数常量是 23 。但是在部分情况下我们需要自定义枚举值对应的整数常量,这个时候我们就需要用到自定义的方式。自定义方式又称为为枚举值显式赋值,它的方法如下所示:

    enum Country
    {
        CN = 3,
        UK,
        JP = 70,
        USA = 67
    }
    

    我们在代码中将第一个枚举值对应的整数常量设置为了 3 ,这时第二个枚举值的整数常量就不是 1 了,而是 4 ,因为当枚举值没有显示赋值时,将会按照上一个枚举值对应的整数值加 1 来作为自己本身对应的整数值。最后两个枚举值因为显式赋值了因此对应的整数值就是所赋值的数值。
    枚举取值也很简单,只需要 枚举名.枚举值 即可,例如 Country.UK

    Tip:这里我提几点建议:

    • 枚举值的名称不应包含枚举名称;
    • 枚举名称应以单数的形式出现(除了属性)。
  2. 枚举的类型
    到目前为止我们定义枚举类型使用的基础类型 int 类型,但是枚举不仅仅可以使用 int 类型,还可以使用除了 char 类型之外的所有基础类型。我们可以使用继承语法来指定其他类型。

    enum Country:short
     {
         CN = 3,
         UK,
         JP = 70,
         USA = 67
     }
    

    上面代码中我们显式定义了枚举所使用的基础类型为 short 。这里虽然使用了继承语法但是并没有建立继承关系,所有的枚举基类都是 System.Enum ,这些类都是密封类,无法从现有的枚举类型派生出新的成员。
    对于枚举类型的变量,值不限于声明中命名的值,因此值能转换成基础类型,那么就能转换为枚举类型。之所以这么设计是因在以后的 API 中有很大的可能在不破换老版本的同时为枚举添加新的值。但是这其中也存在一个缺陷,枚举允许在运行时分配未知的值,对于这一点我们在开发时需要考虑到。并且在后期向枚举中添加新的枚举值时应将其添加到所有枚举值的后面,或者显示指定枚举值对应的数值,这样才能避免因添加新值导致枚举类型中的枚举值对应的数值改变。

    Tip:在开发中我们应该尽量使用 int 作为枚举的基础类型,除非因性能问题或互操作方面的考虑时才会考虑使用较小的类型。

一、枚举转换

枚举转换主要涉及到了枚举与枚举的转换、枚举与数字和字符串的转换。

  1. 枚举之间转换
    首先我要说明的是在 C# 中不支持不同枚举数组之间的直接转换,所以如果想要实现不同枚举数组之间的转换我们可以利用 CLR 宽松的赋值兼容性这一特点来进行转换,需要转换的两个枚举必须具有相同的基础类型。同样,我们通过一个例子来看一下具体实现方法。
    static void Main(string[] args)
     {
         CountryAllName[] can = (CountryAllName[])(Array)new Country[4];
     }
    enum Country
     {
         CN,
         UK,
         JP,
         USA
     }
     enum CountryAllName
     {
         China,
         UnitedKingdom,
         Japan,
         UnitedStates
     }
    
    在使用这种方法时有可能会出现意外的错误或结果,并且相关开发规范中并没有说这种方式每次都起作用,因此我不建议这么使用,除非在一些极端场景中。
  2. 枚举和字符串之间转换
    枚举转换为字符串可以直接使用 ToString() 方法, 枚举值 ToString 后会直接输出枚举值标识符的字符串形式,例如 Country.CN.ToString() 得到的结果是字符串 CN 。当然,你也可以利用 Enum.GetNamesEnum.GetName 方法来获取。下面我简单来讲解一下这两个方法的使用。
    • GetNames
      GetNames 方法需要传入一个枚举类型,返回值是一个字符串数组。例如需要获取到 Country 的第二个国家,那么就可以这么来写 Enum.GetNames(typeof(Country))[1],返回结果是 UK 。
    • GetName
      GetName 方法返回的是一个字符串,这个字符串就是需要获取的指定枚举值的字符串形式。同样我们获取第二个国家,Enum.GetName(typeof(Country),1) ,返回的值同样是 UK 。
      字符串转换为枚举也很简单,同样用到了 Enum 基类的一个静态方法 Parse ,例如我们将 JP 转换为枚举 Country 的枚举值可以这么做 (Country)Enum.Parse(typeof(Country),"JP") 。这里有一点需要注意,TryParse 方法是在 .net 4.0 才出现的,因此如果要在 .net 4.0 以下版本中将字符串转换为枚举时,需要进行恰当的错误处理防止字符串不存在与枚举类型中的枚举值中。

    Tip:字符串向枚举转换不可本地化,如果必须本地化,就必须是那些对上层用户不可见的消息。因此在实际开发中应该尽量避免枚举和字符串之间的转换。

  3. 枚举和数字之间转换
    枚举转换为数字我们可以使用强转,例如 (int)Country.CN 返回结果是 0 。从数字转换为枚举我们有两种方法,一种是使用强转,另一种是使用 Enum 的静态方发 ToObject
    • 强转
      强转就比较简单了,Country country = (Country)2
    • ToObject
      ToObject 方法需要传入枚举类型和需要转换的数字,例如 Country country = (Country)Enum.ToObject(typeof(Country),2)
  4. 注意
    字符串转换为枚举和数字转换为枚举都必须先进行判断所要转换的值是否包含在枚举中,判断的方法也很简单只需要调用 Enum 的静态方法 IsDefined 即可,例如我要将 0 和 HK 转换为枚举,代码如下:
    Type type = typeof(Country);
     if(Enum.IsDefined(type,0))
     {
         Enum.ToObject(type, 0);
     }
     if(Enum.IsDefined(type,"HK"))
     {
         Enum.Parse(typeof(Country), "HK");
     }
    
    上述代码中只有 0 会成功转换为枚举值 CN ,因为 0 所对应的枚举值是 CN ,而 HK 并没有在枚举中。

三、标志与属性

这一小节我们来讲解一下标志与属性,标志和属性属于在开发中用的比较少,并且大部分程序员了解的也不多。

  1. 标志
    在开发中有时我们希望能对枚举进行组合使用来表示复合值,那么这时我们就需要定义标志枚举了,标志枚举的名称为复数形式,代表了一个标志的集合。一般我们会使用按位或操作符链接枚举值,使用 HasFlags 方法或者按位与操作符来判断特定的位是否存在。比较经典的标志枚举是位于 System.IO 命名空间中的 FileAttributes 标志枚举,它列出了文件的所有属性,比如只读、隐藏、所在磁盘等等,它所包含的所有枚举值皆可相互组合,例如一个文件既是隐藏文件又是只读文件。定义标志枚举的方法如下:
    [Flags]
    enum WeekDays
     {
         Monday = 1,
         Tuesday = 2,
         Wednesday = 4,
         Thursday = 8,
         Friday = 16,
         Saturday = 32,
         Sunday = 64
     }
    
    在上面的代码中你会发现一个规律,每个枚举值对应的整数值都是 2的n次方,这是为什么呢。在标志枚举中要求多个枚举值相互组合后的结果不能包含在标志枚举中,并且基于按位运算的特性可以很方便的使用位运算符来计算一个枚举值是否包含了另外一个枚举值,这在权限系统中相当有用。
  2. 属性
    枚举值上同样也可以使用属性,例如我们需要打印输出枚举值的中文名,我们就可以通过属性的形式进行设置,首先我们需要定义一个属性:
    public class EnumChineseAttribute : Attribute
     {
         private string m_strDescription;
         public EnumChineseAttribute(string chineseName)
         {
             m_strDescription = chineseName;
         }
    
         public string Description
         {
             get { return m_strDescription; }
         }
     }
     enum Country
     {
         [EnumChinese("中国")]
         CN,
         [EnumChinese("英国")]
         UK,
         [EnumChinese("日本")]
         JP,
         [EnumChinese("美国")]
         USA
     }
     static void Main(string[] args)
     {
         Country country = Country.CN;
         FieldInfo fieldInfo = country.GetType().GetField("CN");
         object[] attribArray = fieldInfo.GetCustomAttributes(false);
         EnumChineseAttribute attrib = (EnumChineseAttribute)attribArray[0];
         Console.WriteLine(attrib.Description);
         Console.Read();
     }
    
    通过上面的代码我们就能获取到 CN 对应的中文名称了,这段代码并没有进行进一步优化,在实际项目中必须进行封装和优化。

四、小结

这篇文章主要讲解了枚举相关的知识,内容有点琐碎,但是在实际开发中还是比较实用的。文章中我所提到的要点和规定在实际开发中已经经过验证,各位读者可以直接拿来使用。

本文由博客一文多发平台 OpenWrite 发布!

posted @ 2020-02-18 11:51  ProgramerCat  阅读(388)  评论(1编辑  收藏  举报