文章开头先看一道题:

在设计某小型项目的数据库(假设用的是 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
}

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

实战转换

using System.Reflection.Emit;
using System.Collections.Generic;
using System.Runtime.Intrinsics.X86; // 测试1: 前端数据{1,4}项,表示列表的第1和第4项被选中 -> 传递给后端 -> 计算状态值为9存储到数据库
int value = GetEnumValueFromIndexs(new[] { 1, 4 });
System.Console.WriteLine(value);
// 测试2: 从数据库查询状态值为9 -> 生成被选列表索引{1,4} -> 前端显示
var indexs = GetEnumIndexsFromValue(value);
System.Console.WriteLine(string.Join(',', indexs));
// 测试3: 前端数据{"语文","物理"},表示列表被选中 -> 传给后端 -> 计算状态值为9存储到数据库
value = GetEnumValueFromNames<课程>(new string[] { "语文","物理" });
System.Console.WriteLine(value);
// 测试4: 从数据库查询状态值为9 -> 生成名称列表 {"语文","物理"} -> 前端显示
System.Console.WriteLine(string.Join(',', GetEnumNamesFromValue<课程>(value))); /// <summary>
/// 根据状态值生成对应的枚举索引值列表
/// </summary>
/// <param name="value">状态值,如:5</param>
/// <returns>枚举索引值列表,如:[1,3]</returns>
List<int> GetEnumIndexsFromValue(int value)
{
List<int> result = new List<int>();
if ((value & 1) == 1) result.Add(1);
for (int i = 1; i < 100; i++)
{
int tmp = value >> i;
if (tmp == 0) break;
if ((tmp & 1) == 1) result.Add(i + 1);
}
return result;
} /// <summary>
/// 根据枚举索引值列表计算状态值
/// </summary>
/// <param name="indexs">枚举索引值列表,如:[1,3]</param>
/// <returns>状态值,如:5</returns>
int GetEnumValueFromIndexs(int[] indexs)
{
int result = 0;
foreach (int item in indexs)
{
if (item > 0) result += 1 << (item - 1);
}
return result;
} /// <summary>
/// 根据枚举名称列表计算状态值
/// </summary>
/// <param name="names">枚举名称列表</param>
/// <typeparam name="T">枚举类型</typeparam>
/// <returns>状态值</returns>
int GetEnumValueFromNames<T>(string[] names) where T : Enum
{
int result = 0;
foreach (string name in names)
{
result += (int)Enum.Parse(typeof(T), name);
}
return result;
} /// <summary>
/// 根据状态值生成枚举名称列表
/// </summary>
/// <param name="value">状态值</param>
/// <typeparam name="T">枚举类型</typeparam>
/// <returns>枚举名称列表</returns>
List<string> GetEnumNamesFromValue<T>(int value)
{
var str = Enum.Parse(typeof(T), value.ToString()).ToString();
List<string> result = new List<string>();
if (!string.IsNullOrEmpty(str))
{
result = str.Replace(" ", "").Split(',').ToList();
}
return result;
} [Flags]
enum 课程
{
语文 = 1,
数学 = 2,
英语 = 4,
物理 = 8,
化学 = 16
}

运行结果:

9
1,4
9
语文,物理

总结

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

C#枚举高级应用的更多相关文章

  1. C#枚举高级战术

    文章开头先给大家出一道面试题: 在设计某小型项目的数据库(假设用的是 MySQL)时,如果给用户表(User)添加一个字段(Roles)用来存储用户的角色,你会给这个字段设置什么类型?提示:要考虑到角 ...

  2. Swift enum(枚举)使用范例

    //: Playground - noun: a place where people can play import UIKit var str = "Hello, playground& ...

  3. Java开发知识之Java的枚举

    Java开发知识之Java的枚举 一丶什么是枚举 枚举可以理解为就是常量,在Java中我们定义常量.都是用 final语句. C++中都是用const关键字. 枚举跟C++概念都是一样的.就是特定的常 ...

  4. Swift 中枚举

    Swift 中枚举高级用法及实践 字数11017 阅读479 评论0 喜欢20 title: "Swift 中枚举高级用法及实践"date: 2015-11-20tags: [AP ...

  5. Swift 面向对象解析(一)

    面向对象总体概括: Swift 不仅能够面向过程编程,也能够面向对象编程(OOP).面向对象其实就是“以对象为核心”,把我们的客观世界想着是由一个个对象组成的,面向对象编程则为对象提供了属性和方法,属 ...

  6. 编写高效优雅Java程序

    面向对象 01.构造器参数太多怎么办? 如果参数很多,会导致构造方法非常多,拓展性差,代码难编写,且难以看懂. 用JavaBeans模式, get和set 一行构造编程多行代码实现,需要使用额外机制确 ...

  7. TypeScript——02——TS基本数据类型介绍和使用

    一,TS的数据类型 ES6的数据类型: 6种基本数据类型 Boolean Number String Symbol undefined null 3种引用类型 Array Function Objec ...

  8. “类型思维”之Typescript,你掌握了吗?

    (一)背景 JavaScript是一门动态弱类型语言 对变量的类型非常宽容 而且不会在这些变量和它们的调用者之间建立结构化的契约. 试想有这么几个场景: 1: 你调用一个别人写的函数,但是这个人没有写 ...

  9. typescript枚举,类型推论,类型兼容性,高级类型,Symbols(学习笔记非干货)

    枚举部分 Enumeration part 使用枚举我们可以定义一些有名字的数字常量. 枚举通过 enum关键字来定义. Using enumerations, we can define some ...

  10. OpenJDK源码研究笔记(十):枚举的高级用法,枚举实现接口,竟是别有洞天

    在研究OpenJDK,Java编译器javac源码的过程中,发现以下代码. 顿时发现枚举类竟然也有如此"高端大气上档次"的用法. 沙场点兵(用法源码) com.sun.tools. ...

随机推荐

  1. AIRIOT智慧变电站管理解决方案

    随着社会电气化进程的加速,电力需求与日俱增,变电站作为电网的关键节点,其稳定性和智能化管理水平直接关系到整个电力系统的高效运作.传统变电站管理平台难以适应现代电力系统复杂管理需求,存在如下痛点: 数据 ...

  2. pgsql安装与主从配置搭建

    一:安装环境 查看一下安装环境:cat /etc/centos-release CentOS Linux release 7.7.1908 (Core) 二:软件下载 https://www.post ...

  3. flutter开发环境的搭建

    下载flutter开发包,有670M左右. github的下载地址:https://github.com/flutter/flutter 或者官方下载地址:https://flutter.dev/do ...

  4. 这是一个基于threading可停止线程的有限容量有限并行度的python任务管理器

    这是一个可停止线程的有限容量有限并行度的任务管理器 基于:GitHub - AlitaIcon/StopableThreadJob: 可停止线程任务管理器 Quick Start 基础调用与效果 im ...

  5. 利用 Helm 在各类 Kubernetes 中安装 Rainbond

    利用 Helm 安装 Rainbond 好雨科技技术团队一直致力于让用户更方便的安装 Rainbond 这款产品. 不久前,我们刚刚推出了在一个容器中部署 Rainbond 的快速安装方式,这种方式覆 ...

  6. NOIP模拟91(多校24)

    T1 破门而入 解题思路 签到题(然而我数组开小直接变成暴力分...) 发现其实就是第一类斯特林数,然后 \(n^2\) 推就好了. 感觉可以用 NTT 优化成 \(nlogn\) ,但是好像并没有什 ...

  7. 6.26考试总结(NOIP模拟10)[入阵曲·将军令·星空]

    对于虚伪而言,真实的光明或许过于耀眼了 前言 这一次吧,考得特别烂,主要是这次考试想搞一下特殊性质,然后一不小心就因小失大弄巧成拙了下,下次注意吧.. T1 入阵曲 暴力 思路 对于这个题的话,暴力的 ...

  8. tab切换中嵌套swiper轮播

    今天在做官网的时候需要用到swiper多图轮播的功能,但是得嵌套在tab切换中,就在我把砖都搬完后,发现了个问题,就是我在进行tab切换后,发现原本设置的swiper的自动轮播竟然失效了,而且样式也是 ...

  9. 剑指Offer-60.把二叉树打印成多行(C++/Java)

    题目: 从上到下按层打印二叉树,同一层结点从左至右输出.每一层输出一行. 分析: 层次打印二叉树,在打印二叉树结点的同时,保存好结点的左右孩子,不断的重复打印,直到需要打印的数组为空即可. 程序: C ...

  10. P7897

    problem && blog 第一道正经的 Ynoi,特此写篇题解纪念一下. Algorithm 1 可以想到 \(O(nm)\) 的 DP. 我们定义 \(dp_u\) 为 \(u ...