百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

C# 枚举高级战术

bigegpt 2024-08-23 11:42 2 浏览

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

在设计某小型项目的数据库(假设用的是 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 特性来帮助更好的调试和日志输出。也要考虑到实际开发中的各种潜在问题,比如多个枚举值进行或(‘|’)运算与成员值发生冲突的问题。

相关推荐

悠悠万事,吃饭为大(悠悠万事吃饭为大,什么意思)

新媒体编辑:杜岷赵蕾初审:程秀娟审核:汤小俊审签:周星...

高铁扒门事件升级版!婚宴上‘冲喜’老人团:我们抢的是社会资源

凌晨两点改方案时,突然收到婚庆团队发来的视频——胶东某酒店宴会厅,三个穿大红棉袄的中年妇女跟敢死队似的往前冲,眼瞅着就要扑到新娘的高额钻石项链上。要不是门口小伙及时阻拦,这婚礼造型团队熬了三个月的方案...

微服务架构实战:商家管理后台与sso设计,SSO客户端设计

SSO客户端设计下面通过模块merchant-security对SSO客户端安全认证部分的实现进行封装,以便各个接入SSO的客户端应用进行引用。安全认证的项目管理配置SSO客户端安全认证的项目管理使...

还在为 Spring Boot 配置类加载机制困惑?一文为你彻底解惑

在当今微服务架构盛行、项目复杂度不断攀升的开发环境下,SpringBoot作为Java后端开发的主流框架,无疑是我们手中的得力武器。然而,当我们在享受其自动配置带来的便捷时,是否曾被配置类加载...

Seata源码—6.Seata AT模式的数据源代理二

大纲1.Seata的Resource资源接口源码2.Seata数据源连接池代理的实现源码3.Client向Server发起注册RM的源码4.Client向Server注册RM时的交互源码5.数据源连接...

30分钟了解K8S(30分钟了解微积分)

微服务演进方向o面向分布式设计(Distribution):容器、微服务、API驱动的开发;o面向配置设计(Configuration):一个镜像,多个环境配置;o面向韧性设计(Resista...

SpringBoot条件化配置(@Conditional)全面解析与实战指南

一、条件化配置基础概念1.1什么是条件化配置条件化配置是Spring框架提供的一种基于特定条件来决定是否注册Bean或加载配置的机制。在SpringBoot中,这一机制通过@Conditional...

一招解决所有依赖冲突(克服依赖)

背景介绍最近遇到了这样一个问题,我们有一个jar包common-tool,作为基础工具包,被各个项目在引用。突然某一天发现日志很多报错。一看是NoSuchMethodError,意思是Dis...

你读过Mybatis的源码?说说它用到了几种设计模式

学习设计模式时,很多人都有类似的困扰——明明概念背得滚瓜烂熟,一到写代码就完全想不起来怎么用。就像学了一堆游泳技巧,却从没下过水实践,很难真正掌握。其实理解一个知识点,就像看立体模型,单角度观察总...

golang对接阿里云私有Bucket上传图片、授权访问图片

1、为什么要设置私有bucket公共读写:互联网上任何用户都可以对该Bucket内的文件进行访问,并且向该Bucket写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可...

spring中的资源的加载(spring加载原理)

最近在网上看到有人问@ContextConfiguration("classpath:/bean.xml")中除了classpath这种还有其他的写法么,看他的意思是想从本地文件...

Android资源使用(android资源文件)

Android资源管理机制在Android的开发中,需要使用到各式各样的资源,这些资源往往是一些静态资源,比如位图,颜色,布局定义,用户界面使用到的字符串,动画等。这些资源统统放在项目的res/独立子...

如何深度理解mybatis?(如何深度理解康乐服务质量管理的5个维度)

深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...

@Autowired与@Resource原理知识点详解

springIOCAOP的不多做赘述了,说下IOC:SpringIOC解决的是对象管理和对象依赖的问题,IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系...

java的redis连接工具篇(java redis client)

在Java里,有不少用于连接Redis的工具,下面为你介绍一些主流的工具及其特点:JedisJedis是Redis官方推荐的Java连接工具,它提供了全面的Redis命令支持,且...