运维开发网

C枚举的高级应用

运维开发网 https://www.qedev.com 2022-04-26 14:53 出处:网络
这篇文章介绍了C#枚举的高级应用,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

这篇文章介绍了C#枚举的高级应用,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

在文章的开头,我会给你一个面试问题:

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

你想到的第一个答案可能是:varchar类型,使用分隔符存储多个角色,如1|2|3或1,2,3表示用户有多个角色。当然,如果角色的数量可能超过个位数,考虑到数据库查询的方便性(比如用INSTR或POSITION来确定用户是否包含某个角色),角色的值至少要以数字10开头。这个方案是可行的,但并不太简单。有更好的吗?比较好的答案应该是整数(int,bigint等。).好处是写SQL查询条件更方便,性能上优于varchar,空。但是integer毕竟只是一个数字。怎么能代表多个角色呢?至此,想到二进制位运算,你脑子里应该就有答案了。并且把答案记在脑子里,然后看完这篇文章,也许你会有意想不到的收获,因为在实际应用中可能会遇到一系列的问题。为了更好的解释下面的问题,我们先来复习一下枚举的基础知识。

枚举基础

枚举的功能是限制其变量从有限数量的选项中取值。这些选项(枚举类型的成员)每个都对应一个数字。默认情况下,该数字从0开始,随0增加。例如:

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

其中,星期日的值为0,星期一的值为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

枚举可以通过描述和显示等功能为成员添加有用的辅助信息,例如:

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}

这样,如果用户既有管理员角色又有成员角色,用户表的角色字段应该包含3。这就是问题所在。此时,如何编写所有具有Admin角色的用户的SQL?对于基础程序员来说,这个问题很简单,用位运算符逻辑and(lsquo;amprsquo)来查询。

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

类似地,要查询具有两种角色的用户,SQL语句应该写成这样:

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

这个SQL语句在C#中的查询是这样的(为了简单起见,这里用的是Dapper):

public class User{ public int Id { get; set; } public Roles Roles { get; set; }}connection.Querylt;Usergt;( "SELECT * FROM `User` WHERE `Roles` amp; @roles = @roles;", new { roles = Roles.Admin | Roles.Member });

相应的,在C#中,判断一个用户是否有某个角色,可以这样判断:

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

同样,在C#中,您可以对枚举执行任意位逻辑运算,例如从枚举变量中移除角色:

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

这解决了本文前面提到的使用integer存储多个角色的问题。无论数据库还是C#语言,操作都是可行且方便的。

枚举的 Flags 特性

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

public IEnumerablelt;Usergt; GetUsersInRoles(Roles roles){ _logger.LogDebug(roles.ToString()); _connection.Querylt;Usergt;( "SELECT * FROM `User` WHERE `Roles` amp; @roles = @roles;", new { roles });}// 调用_repository.GetUsersInRoles(Roles.Admin | Roles.Member);

角色的价值。管理员|角色。成员是3。因为roles枚举类型中没有定义值为3的字段,所以方法中的Roles参数显示3。这些信息对我们调试或打印日志非常不友好。在方法内部,我们不知道这个3代表什么。为了解决这个问题,C#枚举有一个非常有用的特性:FlagsAtrribute。

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

添加了Flags特性后,当我们再次调试GetUsersInRoles(Roles roles)方法时,Roles参数的值将显示为Admin|Member。简单地说,添加或不添加标志的区别在于:

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

我觉得给枚举加标志应该算是C#编程的一个最佳实践,尽量在定义枚举的时候加标志。

解决枚举值冲突:2 的幂

到目前为止,列举角色的类型似乎都没问题,但是如果要添加一个角色:Mananger会怎么样呢?根据数值递增的规则,Manager的值应该设置为3。

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

可以把Manager的值设置为3吗?显然不能,因为Admin和Member的按位OR运算(即Admin | Member)的值也是3,也就是说你两个角色都有,这和Manager是冲突的。那如何设置值避免冲突呢?因为它是二进制逻辑运算ldquo或者rdquo如果与成员值有冲突,那么用逻辑运算或的规则来解决它。我们知道ldquo或者rdquo运算的逻辑是,只要两边都有1,结果就是1。例如,1|1和1|0都产生1,只有0|0产生0。那么我们必须避免任何两个值在同一位置出现1。根据二进制全2比1的特点,只需要保证所有枚举值都是2的幂即可。例如:

1: 000000012: 000000104: 000001008: 00001000

如果以后再增加,就是16,32,64...,其中所有值都不会与成员的任何值冲突,无论它们是如何相加的。这就解决了问题,所以我们必须像这样定义角色枚举的值:

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

但是,在定义值时,您应该在心中进行一点计算。如果你想偷懒,可以用下面的ldquoRdquo方法来定义:

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

一直可以向下增加数值,阅读体验好,不容易出错。两种方法是等价的,常量位移的计算是在编译时进行的,所以比较起来不会有额外的开销。

总结

本文通过一个小小的面试问题引出一系列关于枚举的思考。在小型系统中,将用户角色直接存储在用户表中是非常常见的。此时将角色字段设置为整数(如int)是较好的设计方案。但同时,我们也应该考虑一些最佳实践,例如使用标志来帮助更好地调试和日志输出。还要考虑实际开发中的各种潜在问题,比如多个枚举值或者(lsquo| rsquo)运算与成员值冲突。

这就是本文的全部内容。希望对大家的学习有帮助,也希望大家能支持一下

0

精彩评论

暂无评论...
验证码 换一张
取 消