简述C#枚举高级战术

文章开头先给大家出一道面试题:

在设计某小型项目的数据库(假设用的是 MySQL)时,如果给用户表(User)添加一个字段(Roles)用来存储用户的角色,你会给这个字段设置什么类型?提示:要考虑到角色在后端开发时需要用枚举表示,且一个用户可能会拥有多个角色。

映入你脑海的第一个答案可能是:varchar 类型,用分隔符的方式来存储多个角色,比如用 1|2|3 或 1,2,3 来表示用户拥有多个角色。当然如果角色数量可能超过个位数,考虑到数据库的查询方便(比如用 INSTR 或 POSITION 来判断用户是否包含某个角色),角色的值至少要从数字 10 开始。方案是可行的,可是不是太简单了,有没有更好的方案?更好的回答应是整型(int、bigint 等),优点是写 SQL 查询条件更方便,性能、空间上都优于 varchar。但整型毕竟只是一个数字,怎么表示多个角色呢?此时想到了二进制位操作的你,心中应该早有了答案。且保留你心中的答案,接着看完本文,或许你会有意外的收获,因为实际应用中可能还会遇到一连串的问题。为了更好的说明后面的问题,我们先来回顾一下枚举的基础知识。

枚举基础

枚举类型的作用是限制其变量只能从有限的选项中取值,这些选项(枚举类型的成员)各自对应于一个数字,数字默认从 0 开始,并以此递增。例如:

public enum Days
{
  Sunday, Monday, Tuesday, // ...
}

其中 Sunday 的值是 0,Monday 是 1,以此类推。为了一眼能看出每个成员代表的值,一般推荐显示地将成员值写出来,不要省略:

public enum Days
{
  Sunday = 0, Monday = 1, Tuesday = 2, // ...
}

C# 枚举成员的类型默认是 int 类型,通过继承可以声明枚举成员为其它类型,比如:

public enum Days : byte
{
  Monday = 1,
  Tuesday = 2,
  Wednesday = 3,
  Thursday = 4,
  Friday = 5,
  Saturday = 6,
  Sunday = 7
}

枚举类型一定是继承自 byte、sbyte、short、ushort、int、uint、long 和 ulong 中的一种,不能是其它类型。下面是几个枚举的常见用法(以上面的 Days 枚举为例):

// 枚举转字符串
string foo = Days.Saturday.ToString(); // "Saturday"
string foo = Enum.GetName(typeof(Days), 6); // "Saturday"
// 字符串转枚举
Enum.TryParse("Tuesday", out Days bar); // true, bar = Days.Tuesday
(Days)Enum.Parse(typeof(Days), "Tuesday"); // Days.Tuesday

// 枚举转数字
byte foo = (byte)Days.Monday; // 1
// 数字转枚举
Days foo = (Days)2; // Days.Tuesday

// 获取枚举所属的数字类型
Type foo = Enum.GetUnderlyingType(typeof(Days))); // System.Byte

// 获取所有的枚举成员
Array foo = Enum.GetValues(typeof(MyEnum);
// 获取所有枚举成员的字段名
string[] foo = Enum.GetNames(typeof(Days));

另外,值得注意的是,枚举可能会得到非预期的值(值没有对应的成员)。比如:

Days d = (Days)21; // 不会报错
Enum.IsDefined(typeof(Days), d); // false

即使枚举没有值为 0 的成员,它的默认值永远都是 0。

var z = default(Days); // 0

枚举可以通过 Description、Display 等特性来为成员添加有用的辅助信息,比如:

public enum ApiStatus
{
  [Description("成功")]
  OK = 0,
  [Description("资源未找到")]
  NotFound = 2,
  [Description("拒绝访问")]
  AccessDenied = 3
}

static class EnumExtensions
{
  public static string GetDescription(this Enum val)
  {
    var field = val.GetType().GetField(val.ToString());
    var customAttribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute));
    if (customAttribute == null) { return val.ToString(); }
    else { return ((DescriptionAttribute)customAttribute).Description; }
  }
}

static void Main(string[] args)
{
  Console.WriteLine(ApiStatus.Ok.GetDescription()); // "成功"
}

上面这些我认为已经包含了大部分我们日常用到的枚举知识了。下面我们继续回到文章开头说的用户角色存储问题。

用户角色存储问题

我们先定义一个枚举类型来表示两种用户角色:

public enum Roles
{
  Admin = 1,
  Member = 2
}

这样,如果某个用户同时拥有 Admin 和 Member 两种角色,那么 User 表的 Roles 字段就应该存 3。那问题来了,此时若查询所有拥有 Admin 角色的用户的 SQL 该怎么写呢?对于有基础的程序员来说,这个问题很简单,只要用位操作符逻辑与(‘&')来查询即可。

SELECT * FROM `User` WHERE `Roles` & 1 = 1;

同理,查询同时拥有这两种角色的用户,SQL 语句应该这么写:

SELECT * FROM `User` WHERE `Roles` & 3 = 3;

对这条 SQL 语句用 C# 来实现查询是这样的(为了简单,这里使用了 Dapper):

public class User
{
  public int Id { get; set; }
  public Roles Roles { get; set; }
}

connection.Query<User>(
  "SELECT * FROM `User` WHERE `Roles` & @roles = @roles;",
  new { roles = Roles.Admin | Roles.Member });

对应的,在 C# 中要判断用户是否拥有某个角色,可以这么判断:

// 方式一
if ((user.Roles & Roles.Admin) == Roles.Admin)
{
  // 做管理员可以做的事情
}

// 方式二
if (user.Roles.HasFlag(Roles.Admin))
{
  // 做管理员可以做的事情
}

同理,在 C# 中你可以对枚举进行任意位逻辑运算,比如要把角色从某个枚举变量中移除:

var foo = Roles.Admin | Roles.Member;
var bar = foo & ~Roles.Admin;

这就解决了文章前面提到的用整型来存储多角色的问题,不论数据库还是 C# 语言,操作上都是可行的,而且也很方便灵活。

枚举的 Flags 特性

下面我们提供一个通过角色来查询用户的方法,并演示如何调用,如下:

public IEnumerable<User> GetUsersInRoles(Roles roles)
{
  _logger.LogDebug(roles.ToString());
  _connection.Query<User>(
    "SELECT * FROM `User` WHERE `Roles` & @roles = @roles;",
    new { roles });
}

// 调用
_repository.GetUsersInRoles(Roles.Admin | Roles.Member);

Roles.Admin | Roles.Member 的值是 3,由于 Roles 枚举类型中并没有定义一个值为 3 的字段,所以在方法内 roles 参数显示的是 3。3 这个信息对于我们调试或打印日志很不友好。在方法内,我们并不知道这个 3 代表的是什么。为了解决这个问题,C# 枚举有个很有用的特性:FlagsAtrribute。

[Flags]
public enum Roles
{
  Admin = 1,
  Member = 2
}

加上这个 Flags 特性后,我们再来调试 GetUsersInRoles(Roles roles) 方法时,roles 参数的值就会显示为 Admin|Member 了。简单来说,加不加 Flags 的区别是:

var roles = Roles.Admin | Roles.Member;
Console.WriteLing(roles.ToString()); // "3",没有 Flags 特性
Console.WriteLing(roles.ToString()); // "Admin, Member",有 Flags 特性

给枚举加上 Flags 特性,我觉得应当视为 C# 编程的一种最佳实践,在定义枚举时尽量加上 Flags 特性。

解决枚举值冲突:2 的幂

到这,枚举类型 Roles 一切看上去没什么问题,但如果现在要增加一个角色:Mananger,会发生什么情况?按照数字值递增的规则,Manager 的值应当设为 3。

[Flags]
public enum Roles
{
  Admin = 1,
  Member = 2,
  Manager = 3
}

能不能把 Manager 的值设为 3?显然不能,因为 Admin 和 Member 进行位的或逻辑运算(即:Admin | Member) 的值也是 3,表示同时拥有这两种角色,这和 Manager 冲突了。那怎样设值才能避免冲突呢?既然是二进制逻辑运算“或”会和成员值产生冲突,那就利用逻辑运算或的规律来解决。我们知道“或”运算的逻辑是两边只要出现一个 1 结果就会 1,比如 1|1、1|0 结果都是 1,只有 0|0 的情况结果才是 0。那么我们就要避免任意两个值在相同的位置上出现 1。根据二进制满 2 进 1 的特点,只要保证枚举的各项值都是 2 的幂即可。比如:

1:  00000001
2:  00000010
4:  00000100
8:  00001000
再往后增加的话就是 16、32、64...,其中各值不论怎么相加都不会和成员的任一值冲突。这样问题就解决了,所以我们要这样定义 Roles 枚举的值:

[Flags]
public enum Roles
{
  Admin = 1,
  Member = 2,
  Manager = 4,
  Operator = 8
}

不过在定义值的时候要在心中小小计算一下,如果你想懒一点,可以用下面这种“位移”的方法来定义:

[Flags]
public enum Roles
{
  Admin  = 1 << 0,
  Member  = 1 << 1,
  Manager = 1 << 2,
  Operator = 1 << 3
}

一直往下递增编值即可,阅读体验好,也不容易编错。两种方式是等效的,常量位移的计算是在编译的时候进行的,所以相比不会有额外的开销。

总结

本文通过一道小小的面试题引发一连串对枚举的思考。在小型系统中,把用户角色直接存储在用户表是很常见的做法,此时把角色字段设为整型(比如 int)是比较好的设计方案。但与此同时,也要考虑到一些最佳实践,比如使用 Flags 特性来帮助更好的调试和日志输出。也要考虑到实际开发中的各种潜在问题,比如多个枚举值进行或(‘|')运算与成员值发生冲突的问题。

到此这篇关于简述C#枚举高级战术的文章就介绍到这了,更多相关C# 枚举内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C#实现获取枚举中元素个数的方法

    本文以一个简单实例讲述了C#实现获取枚举中元素个数的方法,对于C#学习来说是需要加以牢固掌握的技巧.分享给大家供大家参考之用.具体如下: 实现该功能可以使用枚举基类System.Enum的GetNames(Type t) 方法来获取某个枚举中的元素列表. 具体功能代码如下: enum EnumTest { type1, type2, } class Program { static void Main(string[] args) { EnumTest e = new EnumTest(); s

  • C# 遍历枚举类型的所有元素

    比如定义了一个错误的枚举类型 复制代码 代码如下: public enum eErrorDetailCode : int         {             登陆成功 = 0,             登出 = 1,             应用错误 = 2,             成功 = 16,             失败 = 17         } 需要引用 using System; 然后在循环中,遍历枚举对象的所有元素 复制代码 代码如下: foreach (int  m

  • C#中可枚举类型详解

    枚举是迭代一个集合中的数据项的过程. 我们经常使用的大多数集合实际上都已经实现了枚举的接口IEnumerable和IEnumerator接口,这样才能使用foreach迭代,有些是含有某种抽象了枚举细节的接口:ArrayList类型有索引,BitArray有Get方法,哈希表和字典有键和值..........其实他们都已经实现了IEnumerable和IEnumerator接口.所以一切的集合和数组都可以用IEnumerable或者IEnumerable<T>接口来定义. IEnumerabl

  • C# 获取枚举值的简单实例

    先申明一个枚举: 复制代码 代码如下: public enum Test_Enum        {            one = 1001, two = 1002, three = 1003, four = 1004, five = 1005, six = 1006, seven = 1007, eight = 1008, nine = 1009, zero = 1000        } 获取值: 复制代码 代码如下: object ojb = Enum.GetName(typeof(T

  • C#枚举数值与名称的转换实例分享

    首先建立一个枚举: 复制代码 代码如下: /// <summary>    /// 颜色    /// </summary>    public enum ColorType    {        /// <summary>        /// 红色         /// </summary>        Red, /// <summary>        /// 蓝色         /// </summary>      

  • C#枚举中的位运算权限分配浅谈

    常用的位运算主要有与(&), 或(|)和非(~), 比如: 1 & 0 = 0, 1 | 0 = 1, ~1 = 0 在设计权限时, 我们可以把权限管理操作转换为C#位运算来处理. 第一步, 先建立一个枚举表示所有的权限管理操作: 复制代码 代码如下: [Flags] public enum Permissions { Insert = 1, Delete = 2, Update = 4, Query = 8 } [Flags]表示该枚举可以支持C#位运算, 而枚举的每一项值, 我们用2的

  • C#编程中枚举类型的使用教程

    枚举类型(也称为枚举)为定义一组可以赋给变量的命名整数常量提供了一种有效的方法.例如,假设您必须定义一个变量,该变量的值表示一周中的一天.该变量只能存储七个有意义的值.若要定义这些值,可以使用枚举类型.枚举类型是使用 enum关键字声明的. enum Days { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }; enum Months : byte { Jan, Feb, Mar, Apr, May, Jun,

  • 深入理解C#中的枚举

    枚举类型是一种的值类型,它用于声明一组命名的常数. (1)枚举的声明:枚举声明用于声明新的枚举类型.访问修辞符 enum 枚举名:基础类型 复制代码 代码如下: {        枚举成员    } 基础类型必须能够表示该枚举中定义的所有枚举数值.枚举声明可以显式地声明 byte.sbyte.short.ushort.int.uint.long 或 ulong 类型作为对应的基础类型.没有显式地声明基础类型的枚举声明意味着所对应的基础类型是 int. (2)枚举成员枚举成员是该枚举类型的命名常数

  • 深入解析c#中枚举类型的定义与使用

    介绍枚举是一个指定的常数,其基础类型可以是除 Char 外的任何整型.如果没有显式声明基础类型,则使用 Int32.编程语言通常提供语法来声明由一组已命名的常数和它们的值组成的枚举. 定义默认基数从O开始,也可指定数值.enum Days { Saturday=1, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday };enum Colors { Red = 1, Green = 2, Blue = 4, Yellow = 8 }; 使用C

  • 简述C#枚举高级战术

    文章开头先给大家出一道面试题: 在设计某小型项目的数据库(假设用的是 MySQL)时,如果给用户表(User)添加一个字段(Roles)用来存储用户的角色,你会给这个字段设置什么类型?提示:要考虑到角色在后端开发时需要用枚举表示,且一个用户可能会拥有多个角色. 映入你脑海的第一个答案可能是:varchar 类型,用分隔符的方式来存储多个角色,比如用 1|2|3 或 1,2,3 来表示用户拥有多个角色.当然如果角色数量可能超过个位数,考虑到数据库的查询方便(比如用 INSTR 或 POSITION

  • 详解C#枚举高级战术

    文章开头先给大家出一道面试题: 在设计某小型项目的数据库(假设用的是 MySQL)时,如果给用户表(User)添加一个字段(Roles)用来存储用户的角色,你会给这个字段设置什么类型?提示:要考虑到角色在后端开发时需要用枚举表示,且一个用户可能会拥有多个角色. 映入你脑海的第一个答案可能是:varchar 类型,用分隔符的方式来存储多个角色,比如用 1|2|3 或 1,2,3 来表示用户拥有多个角色.当然如果角色数量可能超过个位数,考虑到数据库的查询方便(比如用 INSTR 或 POSITION

  • 详解C# 枚举高级用法之Description

    目录 基础枚举 高级点用法,反射元数据 基础枚举 namespace TestEnum { //声明 public enum Name { //默认值 boob = 0 ,依次往下排,可自定义 jackMa = 3,jackCHan那就= 4 boob, JackMa, JackChan, // Lisa, Poro } } 声明枚举,调用. 几个常用的转换不做介绍,一嗦一大堆. enum转string enum 转int string转enum 整型转enum 高级点用法,反射元数据 publ

  • C#实现常见加密算法的示例代码

    目录 前言 1. Base64编码 1.1 原理介绍 1.2 C#代码 2. 凯撒密码 2.1 原理介绍 2.2 C#代码 3. Vigenere密码 3.1 原理介绍 3.2 C#代码 4. DES 4.1 原理介绍 4.2 C#代码 5. AES 5.1 原理简述 5.2 C#代码 前言 最近项目中需要用到字符串加解密,遂研究了一波,发现密码学真的是博大精深,好多算法的设计都相当巧妙,学到了不少东西,在这里做个小小的总结,方便后续查阅. 文中关键词: 明文(P,Plaintext) 密文(C

  • C#枚举的高级应用

    文章开头先给大家出一道面试题: 在设计某小型项目的数据库(假设用的是 MySQL)时,如果给用户表(User)添加一个字段(Roles)用来存储用户的角色,你会给这个字段设置什么类型?提示:要考虑到角色在后端开发时需要用枚举表示,且一个用户可能会拥有多个角色. 映入你脑海的第一个答案可能是:varchar 类型,用分隔符的方式来存储多个角色,比如用 1|2|3 或 1,2,3 来表示用户拥有多个角色.当然如果角色数量可能超过个位数,考虑到数据库的查询方便(比如用 INSTR 或 POSITION

  • 批处理的高级运用技巧

    批处理的高级运用1 一.简单批处理内部命令  批处理的高级运用!!小技巧哦!!!  批处理大家都很熟悉吧!  下面介绍批处理的几个知识点:  1:巧用FC命令做查木马工具:  首先建立批处理文件atm.bat:写入代码:@echo off  dir c:\windows\system32\*.exe >c:??.txt  dir c:\windows\system32\*.dll >c:??.txt  2:建立批处理文件WLTS.bat 写入代码:@echo off  dir c:\windo

  • JavaScript高级程序设计(第3版)学习笔记6 初识js对象

    在房子里面可以放你想放的任意事物--如果你有足够的美学造诣,你甚至可以弄一个房中房试试--当然,为了方便管理,我们会给房子里存放的所有事物都会取上一个不重复的名字,比如医药房间里的各种药品名称.在ECMAScript中,你可以在对象中存放任意你想放的数据,同样,我们需要给存放的数据取一个名字--也就是对象的属性名,再存放各种数据.再看看ECMA-262中对象的定义:无序属性的集合,其属性可以包含简单数据类型值.对象或者函数. 进入对象,我开始有些激动了,说实话,让我想起做这系列学习笔记的最初原因

  • JavaScript高级程序设计(第三版)学习笔记1~5章

    第2章,在html中使用JavaScript Html引入外部js脚本 <script type="text/javascript" src="test.js">两个</script>之间不应放脚本,因为并不会被执行</script> <script>标签有一个defer属性可以延迟脚本执行,但是并不保证会按脚本排列顺序执行 建议:将脚本引入放在<body>标签的所有内容之后,而不放在<head>

  • 详细解析Python中__init__()方法的高级应用

    通过工厂函数对 __init__() 加以利用 我们可以通过工厂函数来构建一副完整的扑克牌.这会比枚举所有52张扑克牌要好得多,在Python中,我们有如下两种常见的工厂方法: 定义一个函数,该函数会创建所需类的对象. 定义一个类,该类有创建对象的方法.这是一个完整的工厂设计模式,正如设计模式书所描述的那样.在诸如Java这样的语言中,工厂类层次结构是必须的,因为该语言不支持独立的函数. 在Python中,类并不是必须的.只是当有相关的工厂非常复杂的时候才会显现出优势.Python的优势就是当一

  • Mssql高级注入笔记II

    一些sql扩展  xp_regaddmultistring  xp_regdeletekey 删除键名  xp_regdeletevalue 删除键值  xp_regenumkeys 枚举  xp_regenumvalues  xp_regread 对于  xp_regremovemultistring  xp_regwrite 写  xp_availablemedia 查看驱动器  xp_dirtree 看目录  xp_enumdsn ODBC数据源  xp_loginconfig 一些服务器

随机推荐