开源 - Ideal库 - 常用枚举扩展方法

开源 - Ideal库 - 常用枚举扩展方法(一)

今天和大家享一些关于枚举操作相关的常用扩展方法。

我们平时用的比较多的是正常枚举,同时还有加[Flags]特性的位标志枚举,因此以下所有扩展方法同时适用正常枚举以及位标志枚举。

我们首先定义两种枚举用于下面扩展方法测试适用,代码如下:

//正常枚举
internal enum StatusEnum
{
    [Description("正常")]
    Normal = 0,
    [Description("待机")]
    Standby = 1,
    [Description("离线")]
    Offline = 2,
    Online = 3,
}
//位标志枚举
[Flags]
internal enum TypeFlagsEnum
{
    [Description("Http协议")]
    Http = 1,
    [Description("Udp协议")]
    Udp = 2,
    [Description("Http协议,Udp协议")]
    HttpAndUdp = 3,
    [Description("Tcp协议")]
    Tcp = 4,
}

01、根据枚举名称转换成枚举

该方法接收枚举名称字符串,并转为对应枚举,转换失败则返回空。

首先会校验字符串是否为整数值类型字符串,如果是则直接返回空,因为枚举名称不可能是整数类型字符串。

然后调用TryParse方法进行转换,具体代码如下:

//根据枚举名称转换成枚举,转换失败则返回空
public static T? ToEnumByName<T>(this string name) where T : struct, Enum
{
    //如果为整数类型字符串,则直接返回空
    if (int.TryParse(name, out _))
    {
        return default;
    }
    //转换成功则返回结果,否则返回空
    if (Enum.TryParse<T>(name, out var result))
    {
        return result;
    }
    return default;
}

下面我们对其进行详细的单元测试,分别针对正常枚举和位标志枚举两种情况测试,具体用例如下:

(1) 正常枚举名称字符串,成功转换成枚举;

(2) 不存在的枚举名称字符串,返回空;

(3) 整数类型的字符串,返回空;

(4) 正常位标志枚举名称字符串,成功转换成枚举;

(5) 不存在的位标志枚举名称字符串,返回空;

(6) 正常的位标志枚举名称组合字符串,成功转换成枚举;

(7) 不存在的位标志枚举名称组合字符串,返回空;

位标志枚举名称组合字符串是指两个枚举项组合的情况,这也是位标志枚举特色,不了解位标志枚举的可以自行先补充一下相关知识点。

具体代码如下:

[Fact]
public void ToEnumByName()
{
    //正常枚举名称字符串,成功转换成枚举
    var status = "Standby".ToEnumByName<StatusEnum>();
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的枚举名称字符串,返回空
    var isStatusNull = "StandbyNull".ToEnumByName<StatusEnum>();
    Assert.Null(isStatusNull);
    //整数类型的字符串,返回空
    var isStatusNullInt = "3".ToEnumByName<StatusEnum>();
    Assert.Null(isStatusNullInt);
    //正常位标志枚举名称字符串,成功转换成枚举
    var flags = "HttpAndUdp".ToEnumByName<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.HttpAndUdp, flags);
    //不存在的位标志枚举名称字符串,返回空
    var isFlagsNull = "HttpAndUdpNull".ToEnumByName<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
    //正常的位标志枚举名称组合字符串,成功转换成枚举
    var flagsGroup = "Http,Tcp".ToEnumByName<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.Http | TypeFlagsEnum.Tcp, flagsGroup);
    //不存在的位标志枚举名称组合字符串,返回空
    var isFlagsGroupNull = "Http,Tcp,Null".ToEnumByName<TypeFlagsEnum>();
    Assert.Null(isFlagsGroupNull);
}

02、根据枚举名称转换成枚举或默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举值,具体代码如下:

//根据枚举名称转换成枚举,转换失败则返回默认枚举
public static T ToEnumOrDefaultByName<T>(this string name, T defaultValue) where T : struct, Enum
{
    //调用根据枚举名称转换成枚举方法
    var result = name.ToEnumByName<T>();
    if (result.HasValue)
    {
        return result.Value;
    }
    //转换失败则返回默认值
    return defaultValue;
}

因为该方法调用了上一个方法,因此我们就简单测试一下,转换成功返回正确的值,转换失败则返回默认值,具体代码如下:

[Fact]
public void ToEnumOrDefaultByName()
{
    //正常枚举名称字符串,成功转换成枚举
    var status = "Standby".ToEnumOrDefaultByName(StatusEnum.Normal);
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的枚举名称字符串,返回指定默认值
    var statusDefault = "StandbyNull".ToEnumOrDefaultByName(StatusEnum.Standby);
    Assert.Equal(StatusEnum.Standby, statusDefault);
}

03、根据枚举描述转换成枚举

该方法接收枚举描述字符串,并转为对应枚举,转换失败则返回空,其中如果枚举项没有描述则以枚举名称代替,具体代码如下:

//根据枚举描述转换成枚举,转换失败返回空
public static T? ToEnumByDesc<T>(this string description) where T : struct, Enum
{
    //首先获取枚举所有项
    foreach (Enum value in Enum.GetValues(typeof(T)))
    {
        //取枚举项描述与目标描述相比较,相同则返回该枚举项
        if (value.ToEnumDesc() == description)
        {
            return (T)value;
        }
    }
    //未查到匹配描述则返回默认值
    return default;
}

其中ToEnumDesc方法下文会详细讲解。

我们针对该方法进行以下五种情况进行单元测试:

(1) 正常枚举描述字符串,成功转换成枚举;

(2) 如果枚举项没有枚举描述,则枚举名称字符串,成功转换成枚举;

(3) 不存在的枚举描述字符串,返回空;

(4) 正常位标志枚举描述字符串,成功转换成枚举;

(5) 不存在的位标志枚举描述字符串转换,返回空;

具体代码如下:

[Fact]
public void ToEnumByDescription()
{
    //正常枚举描述字符串,成功转换成枚举
    var status = "待机".ToEnumByDesc<StatusEnum>();
    Assert.Equal(StatusEnum.Standby, status);
    //如果枚举项没有枚举描述,则枚举名称字符串,成功转换成枚举
    var statusNotDesc = "Online".ToEnumByDesc<StatusEnum>();
    Assert.Equal(StatusEnum.Online, statusNotDesc);
    //不存在的枚举描述字符串,返回空
    var isStatusNull = "待机无".ToEnumByDesc<StatusEnum>();
    Assert.Null(isStatusNull);
    //正常位标志枚举描述字符串,成功转换成枚举
    var flags = "Http协议,Udp协议".ToEnumByDesc<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.HttpAndUdp, flags);
    //不存在的位标志枚举描述字符串转换,返回空
    var isFlagsNull = "Http协议Udp协议".ToEnumByDesc<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
}

04、根据枚举描述转换成枚举或默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举值,其中如果枚举项没有描述则以枚举名称代替,具体代码如下:

//根据枚举描述转换成枚举,转换失败返回默认枚举
public static T? ToEnumOrDefaultByDesc<T>(this string description, T defaultValue) where T : struct, Enum
{
    //调用根据枚举描述转换成枚举方法
    var result = description.ToEnumByDesc<T>();
    if (result.HasValue)
    {
        return result.Value;
    }
    //未查到匹配描述则返回默认值
    return defaultValue;
}

同样的我们进行简单的单元测试:

[Fact]
public void ToEnumOrDefaultByDesc()
{
    //正常枚举描述字符串,成功转换成枚举
    var status = "待机".ToEnumOrDefaultByDesc(StatusEnum.Offline);
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的枚举描述字符串,返回指定默认值
    var statusDefault = "待机无".ToEnumOrDefaultByDesc(StatusEnum.Offline);
    Assert.Equal(StatusEnum.Offline, statusDefault);
}

05、根据枚举名称转换成枚举值

该方法接收枚举名字字符串,并转为对应枚举值,转换失败则返回空,具体代码如下:

//根据枚举名称转换成枚举值,转换失败则返回空
public static int? ToEnumValueByName<T>(this string name) where T : struct, Enum
{
    //调用根据枚举名称转换成枚举方法
    var result = name.ToEnumByName<T>();
    if (result.HasValue)
    {
        return Convert.ToInt32(result.Value);
    }
    //转换失败则返回空
    return default;
}

我们对其进行以下五种情况做详细的单元测试:

(1) 正常枚举名称字符串,成功转换成枚举值;

(2) 不存在的枚举名称字符串,返回空;

(3) 正常位标志枚举名称字符串,成功转换成枚举值;

(4) 正常的位标志枚举名称组合字符串,成功转换成枚举值;

(5) 不存在的位标志枚举Int值转换则返回空;

具体代码如下:

[Fact]
public void ToEnumValueByName()
{
    //正常枚举名称字符串,成功转换成枚举值
    var status = "Standby".ToEnumValueByName<StatusEnum>();
    Assert.Equal(1, status);
    //不存在的枚举名称字符串,返回空
    var isStatusNull = "StandbyNull".ToEnumValueByName<StatusEnum>();
    Assert.Null(isStatusNull);
    //正常位标志枚举名称字符串,成功转换成枚举值
    var flags = "HttpAndUdp".ToEnumValueByName<TypeFlagsEnum>();
    Assert.Equal(3, flags);
    //正常的位标志枚举名称组合字符串,成功转换成枚举值
    var flagsGroup = "Http,Udp".ToEnumValueByName<TypeFlagsEnum>();
    Assert.Equal(3, flagsGroup);
    //不存在的位标志枚举名称字符串,返回空
    var isFlagsNull = "HttpUdp".ToEnumValueByName<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
}

06、根据枚举名称转换成枚举值或默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举值,具体代码如下:

//根据枚举名称转换成枚举值,转换失败则返回默认枚举值
public static int ToEnumValueOrDefaultByName<T>(this string name, int defaultValue) where T : struct, Enum
{
    //根据枚举名称转换成枚举值
    var result = name.ToEnumValueByName<T>();
    if (result.HasValue)
    {
        return result.Value;
    }
    //转换失败则返回默认值
    return defaultValue;
}

我们进行简单的单元测试,具体代码如下:

[Fact]
public void ToEnumValueOrDefaultByName()
{
    //正常枚举名称字符串,成功转换成枚举值
    var status = "Standby".ToEnumValueOrDefaultByName<StatusEnum>(2);
    Assert.Equal(1, status);
    //不存在的枚举名称字符串,返回指定默认值
    var statusDefault = "StandbyNull".ToEnumValueOrDefaultByName<StatusEnum>(2);
    Assert.Equal(2, statusDefault);
}

07、根据枚举名称转换成枚举描述

该方法接收枚举名字字符串,并转为对应枚举描述,转换失败则返回空,其中如果枚举项没有描述则以枚举名称代替,具体代码如下:

//根据枚举名称转换成枚举描述,转换失败则返回空
public static string? ToEnumDescByName<T>(this string name) where T : struct, Enum
{
    //调用根据枚举名称转换成枚举方法
    var result = name.ToEnumByName<T>();
    if (result.HasValue)
    {
        //转为枚举描述
        return result.Value.ToEnumDesc();
    }
    //转换失败则返回空
    return default;
}

因为该方法内部都是调用现有方法,因此做个简单单元测试,具体代码如下:

[Fact]
public void ToEnumDescByName()
{
    //正常位标志枚举名称字符串,成功转换成枚举描述
    var flags = "HttpAndUdp".ToEnumDescByName<TypeFlagsEnum>();
    Assert.Equal("Http协议,Udp协议", flags);
    //正常的位标志枚举名称组合字符串,组合项存在,成功转换成枚举描述
    var flagsGroup = "Http,Udp".ToEnumDescByName<TypeFlagsEnum>();
    Assert.Equal("Http协议,Udp协议", flagsGroup);
    //正常的位标志枚举名称组合字符串,组合项不存在,成功转换成枚举描述
    var flagsGroup1 = "Http,Tcp".ToEnumDescByName<TypeFlagsEnum>();
    Assert.Equal("Http协议,Tcp协议", flagsGroup1);
}

08、根据枚举名称转换成枚举描述或默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举描述,具体代码如下:

//根据枚举名称转换成枚举描述,转换失败则返回默认枚举描述
public static string ToEnumDescOrDefaultByName<T>(this string name, string defaultValue) where T : struct, Enum
{
    //调用根据枚举名称转换成枚举描述方法
    var result = name.ToEnumDescByName<T>();
    if (!string.IsNullOrWhiteSpace(result))
    {
        return result;
    }
    //转换失败则返回默认枚举描述
    return defaultValue;
}

做个简单单元测试,具体代码如下:

[Fact]
public void ToEnumDescOrDefaultByName()
{
    //正常枚举名称字符串,成功转换成枚举描述
    var status = "Standby".ToEnumDescOrDefaultByName<StatusEnum>("测试");
    Assert.Equal("待机", status);
    //不存在的枚举名称字符串,返回指定默认枚举描述
    var statusDefault = "StandbyNull".ToEnumDescOrDefaultByName<StatusEnum>("测试");
    Assert.Equal("测试", statusDefault);
}

稍晚些时候我会把库上传至Nuget,大家可以直接使用Ideal.Core.Common。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Ideal

 

2024-11-14 13:55:23【出处】:https://www.cnblogs.com/hugogoos/p/18542907

=======================================================================================

开源 - Ideal库 - 常用枚举扩展方法(二)

书接上回,今天继续和大家享一些关于枚举操作相关的常用扩展方法。

今天主要分享通过枚举值转换成枚举、枚举名称以及枚举描述相关实现。

我们首先修改一下上一篇定义用来测试的正常枚举,新增一个枚举项,代码如下:

//正常枚举
internal enum StatusEnum
{
    [Description("正常")]
    Normal = 0,
    [Description("待机")]
    Standby = 1,
    [Description("离线")]
    Offline = 2,
    Online = 3,
    Fault = 4,
}

01、根据枚举值转换成枚举

该方法接收枚举值作为参数,并转为对应枚举,转换失败则返回空。

枚举类Enum中自带了两种转换方法,其一上篇文章使用过即Parse,这个方法可以接收string或Type类型作为参数,其二为ToObject方法,接收参数为整数类型。

因为枚举值本身就是整数类型,因此我们选择ToObject方法作为最终实现,这样就避免使用Parse方法时还需要把整数类型参数进行转换。

同时我们通过上图可以发现枚举值可能的类型有uint、ulong、ushort、sbyte、long、int、byte、short八种情况。

因此下面我们以int类型作为示例,进行说明,但同时考虑到后面通用性、扩展性,我们再封装一个公共的泛型实现可以做到支持上面八种类型。因此本方法会调用一个内部私有方法,具体如下:

//根据枚举值转换成枚举,转换失败则返回空
public static T? ToEnumByValue<T>(this int value) where T : struct, Enum
{
    //调用根据枚举值转换成枚举方法
    return ToEnumByValue<int, T>(value);
}

而内部私有方法即通过泛型实现对多种类型支持,我们先看代码实现再详细讲解,具体代码如下:

//根据枚举值转换成枚举,转换失败则返回空
private static TEnum? ToEnumByValue<TSource, TEnum>(this TSource value)
    where TSource : struct
    where TEnum : struct, Enum
{
    //检查整数值是否是有效的枚举值并且是否是有效位标志枚举组合项
    if (!Enum.IsDefined(typeof(TEnum), value) && !IsValidFlagsMask<TSource, TEnum>(value))
    {
        //非法数据则返回空
        return default;
    }
    //有效枚举值则进行转换
    return (TEnum)Enum.ToObject(typeof(TEnum), value);
}

该方法首先验证参数合法性,验证通过直接使用ToObject方法进行转换。

参数验证首先通过Enum.IsDefined方法校验参数是否是有效的枚举项,这是因为无论是ToObject方法还是Parse方法对于整数类型参数都是可以转换成功的,无论这个参数是否是枚举中的项,因此我们需要首先排查掉非枚举中的项。

而该方法中IsValidFlagsMask方法主要是针对位标志枚举组合情况,位标志枚举特性导致即使我们枚举项中没有定义相关项,但是可以通过组合得到而且是合法的,因此我们需要对位标志枚举单独处理,具体实现代码如下:

//存储枚举是否为位标志枚举
private static readonly ConcurrentDictionary<Type, bool> _flags = new();
//存储枚举对应掩码值
private static readonly ConcurrentDictionary<Type, long> _flagsMasks = new();
private static bool IsValidFlagsMask<TSource, TEnum>(TSource source)
    where TSource : struct
    where TEnum : struct, Enum
{
    var type = typeof(TEnum);
    //判断是否为位标志枚举,如果有缓存直接获取,否则计算后存入缓存再返回
    var isFlags = _flags.GetOrAdd(type, (key) =>
    {
        //检查枚举类型是否有Flags特性
        return Attribute.IsDefined(key, typeof(FlagsAttribute));
    });
    //如果不是位标志枚举则返回false
    if (!isFlags)
    {
        return false;
    }
    //获取枚举掩码,如果有缓存直接获取,否则计算后存入缓存再返回
    var mask = _flagsMasks.GetOrAdd(type, (key) =>
    {
        //初始化存储掩码变量
        var mask = 0L;
        //获取枚举所有值
        var values = Enum.GetValues(key);
        //遍历所有枚举值,通过位或运算合并所有枚举值
        foreach (Enum enumValue in values)
        {
            //将枚举值转为long类型
            var valueLong = Convert.ToInt64(enumValue);
            // 过滤掉负数或无效的值,规范的位标志枚举应该都为非负数
            if (valueLong >= 0)
            {
                //合并枚举值至mask
                mask |= valueLong;
            }
        }
        //返回包含所有枚举值的枚举掩码
        return mask;
    });
    var value = Convert.ToInt64(source);
    //使用待验证值value和枚举掩码取反做与运算
    //结果等于0表示value为有效枚举值
    return (value & ~mask) == 0;
}

该方法首先是判断当前枚举是否是位标志枚举即枚举是否带有Flags特性,可以通过Attribute.IsDefined实现,考虑到性能问题,因此我们把枚举是否为位标志枚举存入缓存中,当下次使用时就不必再次判断了。

如果当前枚举不是位标志枚举则之间返回false。

如果是位标志枚举则进入关键点了,如何判断一个值是否为一组值或一组值任意组合里面的一个?

这里用到了位掩码技术,通过按位或对所有枚举项进行标记,达到合并所有枚举项的目的,同时还包括可能的组合情况。

这里存储掩码的变量定义为long类型,因为我们需要兼容上文提到的八种整数类型。同时一个符合规范的位标志枚举设计枚举值是不会出现负数的因此也需要过滤掉。

同时考虑到性能问题,也需要把每个枚举对于的枚举掩码记录到缓存中方便下次使用。

拿到枚举掩码后我们需要对其进行取反,即表示所有符合要求的值,此值再与待验证参数做按位与操作,如果不为0表示待验证才是为无效枚举值,否则为有效枚举值。

关于位操作我们后面找机会再单独详解讲解其中原理和奥秘。

讲解完整个实现过程我们还需要对该方法进行详细的单元测试,具体分为以下几种情况:

(1) 正常枚举值,成功转换成枚举;

(2) 不存在的枚举值,但是可以通过枚举项按位或合并得到,返回空;

(3) 不存在的枚举值,也不可以通过枚举项按位或合并得到,返回空;

(4) 正常位标志枚举值,成功转换成枚举;

(5) 不存在的枚举值,但是可以通过枚举项按位或合并得到,成功转换成枚举;

(6) 不存在的枚举值,也不可以通过枚举项按位或合并得到,返回空;

具体实现代码如下:

[Fact]
public void ToEnumByValue()
{
    //正常枚举值,成功转换成枚举
    var status = 1.ToEnumByValue<StatusEnum>();
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的枚举值,但是可以通过枚举项按位或合并得到,返回空
    var isStatusNull = 5.ToEnumByValue<StatusEnum>();
    Assert.Null(isStatusNull);
    //不存在的枚举值,也不可以通过枚举项按位或合并得到,返回空
    var isStatusNull1 = 8.ToEnumByValue<StatusEnum>();
    Assert.Null(isStatusNull1);
    //正常位标志枚举值,成功转换成枚举
    var flags = 3.ToEnumByValue<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.HttpAndUdp, flags);
    //不存在的枚举值,但是可以通过枚举项按位或合并得到,成功转换成枚举
    var flagsGroup = 5.ToEnumByValue<TypeFlagsEnum>();
    Assert.Equal(TypeFlagsEnum.Http | TypeFlagsEnum.Tcp, flagsGroup);
    //不存在的枚举值,也不可以通过枚举项按位或合并得到,返回空
    var isFlagsNull = 8.ToEnumByValue<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
}

02、根据枚举值转换成枚举或默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举,具体代码如下:

//根据枚举值转换成枚举,转换失败则返回默认枚举
public static T? ToEnumOrDefaultByValue<T>(this int value, T defaultValue) 
    where T : struct, Enum
{
    //调用根据枚举值转换成枚举方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回枚举
        return result.Value;
    }
    //转换失败则返回默认枚举
    return defaultValue;
}

然后我们进行一个简单单元测试,代码如下:

[Fact]
public void ToEnumOrDefaultByValue()
{
    //正常枚举值,成功转换成枚举
    var status = 1.ToEnumOrDefaultByValue(StatusEnum.Offline);
    Assert.Equal(StatusEnum.Standby, status);
    //不存在的枚举值,返回指定默认枚举
    var statusDefault = 5.ToEnumOrDefaultByValue(StatusEnum.Offline);
    Assert.Equal(StatusEnum.Offline, statusDefault);
}

03、根据枚举值转换成枚举名称

该方法接收枚举值作为参数,并转为对应枚举名称,转换失败则返回空。

实现则是通过根据枚举值转换成枚举方法获得枚举,然后通过枚举获取枚举名称,具体代码如下:

//根据枚举值转换成枚举名称,转换失败则返回空
public static string? ToEnumNameByValue<T>(this int value) where T : struct, Enum
{
    //调用根据枚举值转换成枚举方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回枚举名称
        return result.Value.ToString();
    }
    //转换失败则返回空
    return default;
}

我们进行如下单元测试:

[Fact]
public void ToEnumNameByValue()
{
    //正常枚举值,成功转换成枚举名称
    var status = 1.ToEnumNameByValue<StatusEnum>();
    Assert.Equal("Standby", status);
    //不存在的枚举值,返回空
    var isStatusNull = 10.ToEnumNameByValue<StatusEnum>();
    Assert.Null(isStatusNull);
    //正常位标志枚举值,成功转换成枚举名称
    var flags = 3.ToEnumNameByValue<TypeFlagsEnum>();
    Assert.Equal("HttpAndUdp", flags);
    //不存在的位标志枚举值,返回空
    var isFlagsNull = 20.ToEnumNameByValue<TypeFlagsEnum>();
    Assert.Null(isFlagsNull);
}

04、根据枚举值转换成枚举名称默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举名称,具体代码如下:

//根据枚举值转换成枚举名称,转换失败则返回默认枚举名称
public static string? ToEnumNameOrDefaultByValue<T>(this int value, string defaultValue) 
    where T : struct, Enum
{
    //调用根据枚举值转换成枚举名称方法
    var result = value.ToEnumNameByValue<T>();
    if (!string.IsNullOrWhiteSpace(result))
    {
        //返回枚举名称
        return result;
    }
    //转换失败则返回默认枚举名称
    return defaultValue;
}

进行简单的单元测试,具体代码如下:

[Fact]
public void ToEnumNameOrDefaultByValue()
{
    //正常枚举值,成功转换成枚举名称
    var status = 1.ToEnumNameOrDefaultByValue<StatusEnum>("离线");
    Assert.Equal("Standby", status);
    //不存在的枚举名值,返回指定默认枚举名称
    var statusDefault = 12.ToEnumNameOrDefaultByValue<StatusEnum>("离线");
    Assert.Equal("离线", statusDefault);
}

05、根据枚举值转换成枚举描述

该方法接收枚举值作为参数,并转为对应枚举名称,转换失败则返回空。

实现则是通过根据枚举值转换成枚举方法获得枚举,然后通过枚举获取枚举描述,具体代码如下:

//根据枚举值转换成枚举描述,转换失败则返回空
public static string? ToEnumDescByValue<T>(this int value) where T : struct, Enum
{
    //调用根据枚举值转换成枚举方法
    var result = value.ToEnumByValue<T>();
    if (result.HasValue)
    {
        //返回枚举描述
        return result.Value.ToEnumDesc();
    }
    //转换失败则返回空
    return default;
}

单元测试如下:

[Fact]
public void ToEnumDescByValue()
{
    //正常位标志枚举值,成功转换成枚举描述
    var flags = 3.ToEnumDescByValue<TypeFlagsEnum>();
    Assert.Equal("Http协议,Udp协议", flags);
    //正常的位标志枚举值,组合项不存在,成功转换成枚举描述
    var flagsGroup1 = 5.ToEnumDescByValue<TypeFlagsEnum>();
    Assert.Equal("Http协议,Tcp协议", flagsGroup1);
}

06、根据枚举值转换成枚举描述默认值

该方法是对上一个方法的补充,用于处理转换不成功时,则返回一个指定默认枚举描述,具体代码如下:

//根据枚举值转换成枚举描述,转换失败则返回默认枚举描述
public static string? ToEnumDescOrDefaultByValue<T>(this int value, string defaultValue) 
    where T : struct, Enum
{
    //调用根据枚举值转换成枚举描述方法
    var result = value.ToEnumDescByValue<T>();
    if (!string.IsNullOrWhiteSpace(result))
    {
        //返回枚举描述
        return result;
    }
    //转换失败则返回默认枚举描述
    return defaultValue;
}

单元测试代码如下:

[Fact]
public void ToEnumDescOrDefaultByValue()
{
    //正常枚举值,成功转换成枚举描述
    var status = 1.ToEnumDescOrDefaultByValue<StatusEnum>("测试");
    Assert.Equal("待机", status);
    //不存在的枚举值,返回指定默认枚举描述
    var statusDefault = 11.ToEnumDescOrDefaultByValue<StatusEnum>("测试");
    Assert.Equal("测试", statusDefault);
}

稍晚些时候我会把库上传至Nuget,大家可以直接使用Ideal.Core.Common。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Ideal

 

2024-11-14 13:54:57【出处】:https://www.cnblogs.com/hugogoos/p/18545101

=======================================================================================

开源 - 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

 

2024-11-19 09:55:03【出处】:https://www.cnblogs.com/hugogoos/p/18553991

=======================================================================================

posted on 2024-11-14 13:57  jack_Meng  阅读(7)  评论(0编辑  收藏  举报

导航