详解C#枚举高级战术
文章开头先给大家出一道面试题:
在设计某小型项目的数据库(假设用的是MySQL)时,如果给用户表(User)添加一个字段(Roles)用来存储用户的角色,你会给这个字段设置什么类型?提示:要考虑到角色在后端开发时需要用枚举表示,且一个用户可能会拥有多个角色。
映入你脑海的第一个答案可能是:varchar类型,用分隔符的方式来存储多个角色,比如用1|2|3或1,2,3来表示用户拥有多个角色。当然如果角色数量可能超过个位数,考虑到数据库的查询方便(比如用INSTR或POSITION来判断用户是否包含某个角色),角色的值至少要从数字10开始。方案是可行的,可是不是太简单了,有没有更好的方案?更好的回答应是整型(int、bigint等),优点是写SQL查询条件更方便,性能、空间上都优于varchar。但整型毕竟只是一个数字,怎么表示多个角色呢?此时想到了二进制位操作的你,心中应该早有了答案。且保留你心中的答案,接着看完本文,或许你会有意外的收获,因为实际应用中可能还会遇到一连串的问题。为了更好的说明后面的问题,我们先来回顾一下枚举的基础知识。
枚举基础
枚举类型的作用是限制其变量只能从有限的选项中取值,这些选项(枚举类型的成员)各自对应于一个数字,数字默认从0开始,并以此递增。例如:
publicenumDays { Sunday,Monday,Tuesday,//... }
其中Sunday的值是0,Monday是1,以此类推。为了一眼能看出每个成员代表的值,一般推荐显示地将成员值写出来,不要省略:
publicenumDays { Sunday=0,Monday=1,Tuesday=2,//... }
C#枚举成员的类型默认是int类型,通过继承可以声明枚举成员为其它类型,比如:
publicenumDays:byte { Monday=1, Tuesday=2, Wednesday=3, Thursday=4, Friday=5, Saturday=6, Sunday=7 }
枚举类型一定是继承自byte、sbyte、short、ushort、int、uint、long和ulong中的一种,不能是其它类型。下面是几个枚举的常见用法(以上面的Days枚举为例):
//枚举转字符串 stringfoo=Days.Saturday.ToString();//"Saturday" stringfoo=Enum.GetName(typeof(Days),6);//"Saturday" //字符串转枚举 Enum.TryParse("Tuesday",outDaysbar);//true,bar=Days.Tuesday (Days)Enum.Parse(typeof(Days),"Tuesday");//Days.Tuesday //枚举转数字 bytefoo=(byte)Days.Monday;//1 //数字转枚举 Daysfoo=(Days)2;//Days.Tuesday //获取枚举所属的数字类型 Typefoo=Enum.GetUnderlyingType(typeof(Days)));//System.Byte //获取所有的枚举成员 Arrayfoo=Enum.GetValues(typeof(MyEnum); //获取所有枚举成员的字段名 string[]foo=Enum.GetNames(typeof(Days));
另外,值得注意的是,枚举可能会得到非预期的值(值没有对应的成员)。比如:
Daysd=(Days)21;//不会报错 Enum.IsDefined(typeof(Days),d);//false
即使枚举没有值为0的成员,它的默认值永远都是0。
varz=default(Days);//0
枚举可以通过Description、Display等特性来为成员添加有用的辅助信息,比如:
publicenumApiStatus { [Description("成功")] OK=0, [Description("资源未找到")] NotFound=2, [Description("拒绝访问")] AccessDenied=3 } staticclassEnumExtensions { publicstaticstringGetDescription(thisEnumval) { varfield=val.GetType().GetField(val.ToString()); varcustomAttribute=Attribute.GetCustomAttribute(field,typeof(DescriptionAttribute)); if(customAttribute==null){returnval.ToString();} else{return((DescriptionAttribute)customAttribute).Description;} } } staticvoidMain(string[]args) { Console.WriteLine(ApiStatus.Ok.GetDescription());//"成功" }
上面这些我认为已经包含了大部分我们日常用到的枚举知识了。下面我们继续回到文章开头说的用户角色存储问题。
用户角色存储问题
我们先定义一个枚举类型来表示两种用户角色:
publicenumRoles { 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):
publicclassUser { publicintId{get;set;} publicRolesRoles{get;set;} } connection.Query( "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#中你可以对枚举进行任意位逻辑运算,比如要把角色从某个枚举变量中移除:
varfoo=Roles.Admin|Roles.Member; varbar=foo&~Roles.Admin;
这就解决了文章前面提到的用整型来存储多角色的问题,不论数据库还是C#语言,操作上都是可行的,而且也很方便灵活。
枚举的Flags特性
下面我们提供一个通过角色来查询用户的方法,并演示如何调用,如下:
publicIEnumerableGetUsersInRoles(Rolesroles) { _logger.LogDebug(roles.ToString()); _connection.Query ( "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] publicenumRoles { Admin=1, Member=2 }
加上这个Flags特性后,我们再来调试GetUsersInRoles(Rolesroles)方法时,roles参数的值就会显示为Admin|Member了。简单来说,加不加Flags的区别是:
varroles=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] publicenumRoles { 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] publicenumRoles { Admin=1, Member=2, Manager=4, Operator=8 }
不过在定义值的时候要在心中小小计算一下,如果你想懒一点,可以用下面这种“位移”的方法来定义:
[Flags] publicenumRoles { Admin=1<<0, Member=1<<1, Manager=1<<2, Operator=1<<3 }
一直往下递增编值即可,阅读体验好,也不容易编错。两种方式是等效的,常量位移的计算是在编译的时候进行的,所以相比不会有额外的开销。
总结
本文通过一道小小的面试题引发一连串对枚举的思考。在小型系统中,把用户角色直接存储在用户表是很常见的做法,此时把角色字段设为整型(比如int)是比较好的设计方案。但与此同时,也要考虑到一些最佳实践,比如使用Flags特性来帮助更好的调试和日志输出。也要考虑到实际开发中的各种潜在问题,比如多个枚举值进行或(‘|')运算与成员值发生冲突的问题。
到此这篇关于C#枚举高级战术的文章就介绍到这了,更多相关C#枚举内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。