开源 - Ideal库 - 枚举扩展设计思路及实现难点(三)
今天想和大家分享关于枚举扩展设计思路和在实现过程中遇到的难点。

01、设计思路
设计思路说起来其实也很简单,就是通过枚举相关信息:枚举值、枚举名、枚举描述、枚举项、枚举类型,进行各种转换,通过一个信息获取其他信息。比如通过枚举项获取枚举描述、通过枚举类型获取枚举名称-枚举描述键值对用于下拉列表等等。
主要包括以下几类转换实现:
(1)通过枚举值转成:枚举、枚举名称、举描述;
(2)通过枚举名称转成:枚举、枚举值、枚举描述;
(3)通过枚举描述转成:枚举、枚举值、枚举名称;
(4)通过枚举项转成:枚举值、枚举描述;
(5)通过枚举类型转成:枚举值-枚举名称、枚举值-枚举描述、枚举名称-枚举值、枚举名称-枚举描述、枚举描述-枚举值、枚举描述-枚举名称等键值对集合;
02、实现难点
在实现过程中的确遇到一些难点以及需要注意的小细节。
1、枚举名称转枚举中的小细节
在实现枚举名称转枚举的过程中遇到了一个小坑。
枚举本身提供了Enum.TryParse方法用于把枚举名称字符串或者枚举值字符串转为枚举,因此我们选用了此方法实现枚举名称转枚举。但是我们自己这个接口设计目标是把枚举名称转为枚举,因此需要排除误传枚举值字符串的情况。
方案:当转换成功后,我们还需要调用Enum.IsDefined方法检查枚举名称字符串是否是有效的枚举名称字符串,如果不是枚举中现有的有效枚举项,还需要考虑是否为位标志组合情况。
代码如下:
//根据枚举名称转换成枚举,转换失败则返回空
public static TEnum? ToEnumByName<TEnum>(this string name)
where TEnum : struct, Enum
{
//转换成功则返回结果,否则返回空
if (Enum.TryParse<TEnum>(name, out var result))
{
//检查是否为有效的枚举名称字符串,
if (Enum.IsDefined(typeof(TEnum), name))
{
//返回枚举
return result;
}
else
{
//计算是否为有效的位标志组合项
var isValidFlags = IsValidFlagsMask<ulong, TEnum>(result.ToEnumValue<ulong>());
//如果是有效的位标志组合项则返回枚举,否则返回空
return isValidFlags ? result : default(TEnum?);
}
}
//返回空
return default;
}
2、枚举描述转枚举中的小细节
在实现枚举描述转枚举的过程中对于带位标志枚举处理需要特别小心。我们知道位标志枚举有个特性是枚举项之间可以通过位操作进行组合得到一个有效的枚举项,而且这个枚举项还不存在于当前定义的枚举中。这就要求我们必须处理好组合情况。
方案:因此可以按照以下步骤进行处理。
(1)优先处理当枚举不带位标志的情况;
(2)再处理带位标志的情况;
位标志组合是通过英文逗号[,]拼接的,因此还要考虑到如果一个描述字符串中带有英文逗号[,],但是本身又不是组合的情况。
(3)先把描述当作不是组合情况,先处理一遍,如果转换成功则结束转换;
(4)如果是组合的情况,则计算出组合的每项枚举值,通过位操作得到其对应的枚举项;
而在组合的情况中还需要考虑,如果有组合的项时无效的情况,则这个枚举描述转换应该标记为无效。
(5)判断组合的每一项都是正确的,否则返回空;
(6)将通过组合计算出来的枚举项转为枚举;
因为通过枚举值转枚举过程中还需要明确枚举值类型才行,因此最后一步还需要根据枚举类型实际的基础类型调用不同的转换方法。
具体代码如下:
//根据枚举描述转换成枚举,转换失败返回空
public static TEnum? ToEnumByDesc<TEnum>(this string description)
where TEnum : struct, Enum
{
var type = typeof(TEnum);
var info = GetEnumTypeInfo(type);
//不是位标志枚举的情况处理
if (!info.IsFlags)
{
return ToEnumDesc<TEnum>(description);
}
//是位标志枚举的情况处理
//不是组合位的情况,本身可能就包含[,]
var tenum = ToEnumDesc<TEnum>(description);
if (tenum.HasValue)
{
return tenum;
}
//如果不包含[,],则直接返回
if (!description.Contains(','))
{
return default;
}
//是组合位的情况
var names = description.Split(',');
var values = Enum.GetValues(type);
//记录有效枚举描述个数
var count = 0;
ulong mask = 0L;
//变量枚举所有项
foreach (var name in names)
{
foreach (Enum value in values)
{
//取枚举项描述与目标描述相比较,相同则返回该枚举项
if (value.ToEnumDesc() == name)
{
//有效枚举个数加1
count++;
//将枚举值转为long类型
var valueLong = Convert.ToUInt64(value);
// 过滤掉负数或无效的值,规范的位标志枚举应该都为非负数
if (valueLong >= 0)
{
//合并枚举值至mask
mask |= valueLong;
}
break;
}
}
}
//如果两者不相等,说明描述字符串不是一个有效组合项
if (count != names.Length)
{
return default;
}
var underlyingType = Enum.GetUnderlyingType(type);
if (underlyingType == typeof(byte))
{
return ((byte)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(sbyte))
{
return ((sbyte)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(short))
{
return ((short)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(ushort))
{
return ((ushort)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(int))
{
return ((int)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(uint))
{
return ((uint)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(long))
{
return ((long)mask).ToEnumByValue<TEnum>();
}
else if (underlyingType == typeof(ulong))
{
return mask.ToEnumByValue<TEnum>();
}
return default;
}
3、枚举转枚举值中的小细节
我们知道在定义枚举的时候可以指定枚举值类型为以下八种类型sbyte、byte、short、ushort、int、uint、long、ulong,因此所有涉及到返回枚举值的方法,我们要考虑到支持不同类型的枚举值类型返回。
同时我们也知道相同的方法名和入参,并不能通过不同的返回类型区分出不同的重载方法。
方案:因此我们可以通过泛型的方法支持不同类型的枚举值类型返回。
4、通过枚举值转换的小细节
通过上面我们知道枚举值类型有八种,因此涉及到枚举值转换出其他信息是需要同时兼容这八种类型。
要实现这个功能,可以用上面提到的泛型,如果使用泛型我们可以使用struct类型来限制枚举值泛型TValue,因为struct可以覆盖枚举值的八种类型,但是也引发了另一个问题就是float、double、DateTime都是struct类型,这样就导致这些类型在编辑器中也可以点出ToEnumByValue等相关方法。
作为一个公共封装方法,这种方式显然是对用户不友好的。
因此我们选择另外一种方式:重载方法来实现,通过封装方法多写一些代码来实现对用户友好调用。

5、如何高效返回键值对数据
在通过枚举类型转成各种键值对集合时,有些方法需要用到反射,因此如何高效的获取这些信息就变成重中之重。
最直接的想法是直接把每种枚举类型对应的键值对集合直接缓存起来,下次用到的时候直接从缓存中获取即可。
但是仔细详细,一共有3个数据:枚举值、枚举名称、枚举描述。两两组合有6种情况,也就是要6个缓存存储12个数据,这样其实就相当于浪费了3倍的空间。
当然我们可以把这三个值存一份,然后在需要的是直接组合获取,但是我们仔细分析一些是否有必要把所有数据都缓存下来。
枚举值会涉及到类型转换,枚举名称涉及调用ToString方法,枚举描述需要用到反射。这其中最好性能的非枚举描述不可,因此枚举描述肯定需要缓存,枚举值和枚举名称可以考虑缓存。
假如我们只缓存枚举描述,那么枚举值和枚举名称在使用的时候直接转换,那么我们执行要一份记录枚举描述的缓存以及一份记录枚举类型对应其所有枚举项的缓存即可。另外因为枚举对应的是否带位标志标记以及掩码可以同时记录下来。
代码如下:
public static partial class EnumExtension
{
//枚举类型基础信息
private sealed class EnumTypeInfo
{
//是否是位标志
public bool IsFlags { get; set; }
//枚举掩码
public ulong Mask { get; set; }
//枚举项集合
public List<Enum> Items { get; set; } = [];
}
//存储枚举项对应的描述
private static readonly ConcurrentDictionary<Enum, string> _descs = new();
//存储枚举相关信息
获取枚举名称-枚举描述键值对代码如下,首先获取枚举类型基础信息,然后把枚举项集合转为键值对集合。
//获取枚举名称+枚举描述
public static Dictionary<string, string> ToEnumNameDescs(this Type type)
{
//根据type获取枚举类型基础信息
var info = GetEnumTypeInfo(type);
//通过枚举项集合转为目标类型
return info.Items.ToDictionary(r => r.ToString(), r => r.ToEnumDesc());
}
6、如何识别一个枚举值是否为有效的位标志组合
如何识别一个枚举值是否是一个有效的位标志组合,可以说是整个代码中最难的部分了,其实前面也有提到,主要应用掩码的思想,上一章节已经详解讲解了这里就不在赘述了。
稍晚些时候我会把库上传至Nuget,大家可以直接使用Ideal.Core.Common。
注:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Ideal
开源 - Ideal库 - 枚举扩展设计思路及实现难点(三)的更多相关文章
- 【转】.NET(C#):浅谈程序集清单资源和RESX资源 关于单元测试的思考--Asp.Net Core单元测试最佳实践 封装自己的dapper lambda扩展-设计篇 编写自己的dapper lambda扩展-使用篇 正确理解CAP定理 Quartz.NET的使用(附源码) 整理自己的.net工具库 GC的前世与今生 Visual Studio Package 插件开发之自动生
[转].NET(C#):浅谈程序集清单资源和RESX资源 目录 程序集清单资源 RESX资源文件 使用ResourceReader和ResourceSet解析二进制资源文件 使用ResourceM ...
- JS表格分页组件:fupage的设计思路和详细使用方法(未来考虑开源,争取在2015年)
一.背景 之前在秒针工作的时候,某js高级project师写了非常多自己的组件.当中一套是分页组件.叫做st-grid. 只是在我看来,bug太多.我常常给他反馈bug,我也不清楚为啥 ...
- JS表格分页组件:fupage的设计思路和具体用法(未来考虑开源,争取在2015年)
一.背景 之前在秒针工作的时候,某js高级工程师写了很多自己的组件,其中一套是分页组件,叫做st-grid.不过在我看来,bug太多,我经常给他反馈bug,我也不清楚为啥别人没有发现. ...
- YYCache 设计思路
iOS 开发中总会用到各种缓存,最初我是用的一些开源的缓存库,但到总觉得缺少某些功能,或某些 API 设计的不够好用.YYCache (https://github.com/ibireme/YYCac ...
- 【老司机经验】CC2530&STM8S105二合一嵌入式学习板设计思路与经验分享
CC2530&STM8S105二合一嵌入式学习板设计思路与经验分享 1.缘起 这些年来一直在其他公司的实验箱和别人的开发板上进行教学与开发工作,总是觉得功能设计不那么合意.心里突然冒出个 ...
- 基于Docker一键部署大规模Hadoop集群及设计思路
一.背景: 随着互联网的发展.互联网用户的增加,互联网中的数据也急剧膨胀.每天产生的数据量数以万计,本地文件系统和单机CPU已无法满足存储和计算要求.Hadoop分布式文件系统(HDFS)是海量数据存 ...
- IM开发基础知识补课(七):主流移动端账号登录方式的原理及设计思路
1.引言 在即时通讯网经常能看到各种高大上的高并发.分布式.高性能架构设计方面的文章,平时大家参加的众多开发者大会,主题也都是各种高大上的话题——什么5G啦.AI人工智能啦.什么阿里双11分分钟多少万 ...
- MASA Framework - 整体设计思路
源起 年初我们在找一款框架,希望它有如下几个特点: 学习成本低 只需要学.Net每年主推的技术栈和业务特性必须支持的中间件,给开发同学减负,只需要专注业务就好 个人见解:一款好用的框架应该是补充,而不 ...
- OpenStack 通用设计思路 - 每天5分钟玩转 OpenStack(25)
API 前端服务 每个 OpenStack 组件可能包含若干子服务,其中必定有一个 API 服务负责接收客户请求. 以 Nova 为例,nova-api 作为 Nova 组件对外的唯一窗口,向客户暴露 ...
- 第三篇:Retrofit SDK的设计思路
2016-05-08 15:24:03 Retreofit毫无疑问是一个优美的开源框架,有轻量级.耦合性低.扩展性好.灵活性高的特点,那么Retrofit的设计者们到底是怎么样做到这些的呢?我希望能够 ...
随机推荐
- Node.js 使用
创建 Node 项目 npm init -y # 初始化 Node 项目 package.json 文件 这个文件记录了项目的相关信息. { "name": "hello ...
- C#项目—模拟考试
C#模拟考试软件 开发了一个<模拟考试>的小软件,此小软件练习的目的主要是为了体会编程思想,深度理解高内聚.低耦合,掌握编程思维逻辑的大招,告别垃圾代码,重点体会编程之美,练习时长30分钟 ...
- 一文轻松搞定 tarjan 算法(二)(附带 tarjan 题单)
完结篇:tarjan 求割点.点双连通分量.割边(桥)(附 40 道很好的 tarjan 题目). 上一篇(tarjan 求强连通分量,缩点,求边双) tarjan 求割点 还是求强联通分量的大致思路 ...
- 微软RDL远程代码执行超高危漏洞(CVE-2024-38077)漏洞检测排查方式
漏洞名称:微软RDL远程代码执行超高危漏洞(CVE-2024-38077) CVSS core: 9.8 漏洞描述: CVE-2024-38077 是微软近期披露的一个极其严重的远程代码执行漏洞. ...
- C# HttpClient 基本使用方式(一)
.NetCore主要提供了HttpWebRequest,WebClient,HttpClient这三种访问web的方式,其中HttpWebRequest,WebClient都在官方被标注为已过时,如果 ...
- Coze插件发布!PDF转Markdown功能便捷集成,打造你的专属智能体
近日,TextIn开发的PDF转Markdown插件正式上架Coze平台. 在扣子搜索"pdf转markdown",或在Coze平台搜索"pdf2markdown&quo ...
- BOM – 操作 Query
前言 ASP.NET Core 写过一篇关于操作 query 的 ASP.NET Core – 操作 Uri 和 Query. 前端偶尔也会需要做出 query. URLSearchParams 之前 ...
- QT原理与源码分析之如何开发一个自定义的绘图设备和QT绘图引擎?
简介 本文将介绍如何自定义QT绘图设备类和如何自定义QT绘图引擎类. 目录 QT绘图设备抽象类QPaintDevice QT绘图引擎抽象类QPaintEngine 自定义绘图设备类 自定义绘图引擎类 ...
- 字节跳动的多平台绽放秘诀 | Flutter 开发者故事
字节跳动旗下运营着一系列成功的用户产品.企业应用以及服务,覆盖信息.教育.娱乐等不同领域.随着产品阵容的不断发展,传统的原生双平台开发已经难以满足团队更高效.更灵活.更精美,以及更多样的产品研发需求. ...
- 线段树 ----洛谷p3372
问题描述: 已知一个数列,对数列进行两种操作:1,对数列某个区间中的所有数加d:2,查询数列某区间的区间和 输入: 第一行输入两个整数n和m,分别代表数列中元素个数和对数列的操作次数,第二行输入n个用 ...