Humanizer 枚举反向解析指南EnumDehumanizeExtensions.DehumanizeTo 完整用法与匹配原理【免费下载链接】HumanizerHumanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities项目地址: https://gitcode.com/gh_mirrors/hu/Humanizer导读EnumDehumanizeExtensions是 Humanizer 中负责把人类可读的字符串反向映射回枚举值的扩展类与EnumHumanizeExtensions.Humanize()构成正反两个方向的完整闭环前者把UserType.AnonymousUser变成Anonymous user后者把Anonymous user还原为UserType.AnonymousUser。本文基于 EnumDehumanizeExtensions 的 API 参考文档 展开结合源码、缓存机制与测试用例讲解三个重载方法的使用差异、别名匹配的完整规则、OnNoMatch错误处理策略以及 AOT/裁剪场景下的注意事项读完即可在项目中安全地落地展示用字符串 ↔ 枚举值的双向转换。类概览一个静态扩展类三个重载EnumDehumanizeExtensions是一个静态扩展类定义于 src/Humanizer/EnumDehumanizeExtensions.cs声明为public static class EnumDehumanizeExtensions它只围绕一个核心方法DehumanizeTo提供三种调用形态覆盖编译期已知类型运行时才拿到类型需要自定义无匹配行为三类典型场景重载目标类型来源返回类型无匹配时的行为DehumanizeToTTargetEnum(this string input)编译期泛型TTargetEnum抛NoMatchFoundException固定DehumanizeToTTargetEnum(this string input, OnNoMatch onNoMatch)编译期泛型TTargetEnum?由OnNoMatch决定抛异常或返回 nullDehumanizeTo(this string input, Type targetEnum, OnNoMatch onNoMatch)运行时TypeSystem.Enum由OnNoMatch决定抛异常或返回 null三个重载的完整签名可在 PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt 中核对它是项目对公开 API 的批准基线任何签名变更都会被 API 审批测试拦截。泛型重载编译期类型安全的首选无参版本DehumanizeToTTargetEnum(this string)签名如下TTargetEnum必须是struct且实现System.Enum输入字符串不允许为 nullpublic static TTargetEnum DehumanizeToTTargetEnum(this string input) where TTargetEnum : struct, System.Enum;文档给出的标准示例enum UserType { AnonymousUser, RegisteredUser } Anonymous user.DehumanizeToUserType() UserType.AnonymousUser Registered user.DehumanizeToUserType() UserType.RegisteredUser AnonymousUser.DehumanizeToUserType() UserType.AnonymousUser注意第三个例子原始成员名AnonymousUser本身也是合法输入。这意味着即使输入来自Enum.GetName/ToString()无需任何预处理也能直接还原这与 EnumHumanizeTests.cs 中DehumanizeIsCaseInsensitive测试的输入集合标题、小写、句子、原始成员名是一致的。异常行为ArgumentException当TTargetEnum并非枚举类型时抛出NoMatchFoundException当没有任何枚举成员能匹配输入字符串时抛出。带OnNoMatch的版本public static System.NullableTTargetEnum DehumanizeToTTargetEnum( this string input, Humanizer.OnNoMatch onNoMatch Humanizer.OnNoMatch.ThrowsException) where TTargetEnum : struct, System.Enum;与无参版本相比返回值从TTargetEnum变为TTargetEnum?这是实现无匹配返回 null的前提。默认值仍是OnNoMatch.ThrowsException因此DehumanizeToTEnum(input)与DehumanizeToTEnum(input, OnNoMatch.ThrowsException)行为完全等价Invalid.DehumanizeToUserType(OnNoMatch.ReturnsNull) null Invalid.DehumanizeToUserType(OnNoMatch.ThrowsException) // 抛出 NoMatchFoundException底层调用链从源码看两个泛型重载最终都汇聚到同一个私有方法 DehumanizeToPrivatestatic T? DehumanizeToPrivateT(string input, OnNoMatch onNoMatch) where T : struct, Enum { var dehumanized EnumCacheT.GetDehumanized(); if (dehumanized.TryGetValue(input, out var value)) { return value; } if (onNoMatch ! OnNoMatch.ThrowsException) { return null; } throw new NoMatchFoundException($Couldnt find any enum member that matches the string {input}); }整个查字典过程是 O(1) 的字典查找没有任何逐成员线性扫描性能与匹配规则都由EnumCacheT在背后承担详见下文匹配规则与缓存机制两节。非泛型重载运行时类型的分发器当目标枚举类型在编译期不可知例如来自配置文件、反射或数据绑定时使用接受System.Type的重载public static System.Enum DehumanizeTo( this string input, System.Type targetEnum, Humanizer.OnNoMatch onNoMatch Humanizer.OnNoMatch.ThrowsException);Anonymous user.DehumanizeTo(typeof(UserType)) UserType.AnonymousUser (as Enum)返回类型是System.Enum基类调用方需要自行强转为具体类型。实现要点见 EnumDehumanizeExtensions.cs它通过缓存的MethodInfo调用MakeGenericMethod(targetEnum)把工作委托给泛型重载再通过反射Invoke。TargetInvocationException会被解包把内部真实异常如NoMatchFoundException原样抛出避免调用方拿到一层无意义的包装异常。文档明确给出的取舍该重载依赖反射类型安全性低于泛型重载只要编译期能确定类型就应该优先使用DehumanizeToTTargetEnum(this string, OnNoMatch)。匹配规则四种候选源与两条冲突仲裁原则DehumanizeTo不是简单的字符串等于成员名比较。根据 EnumCache.cs 的 AddAliases 实现每个枚举成员在构建反查字典时会注册四类候选字符串枚举成员名本身caseName成员名的 Humanize 结果caseName.Humanize()例如AnonymousUser→Anonymous userDisplayAttribute的Name、Description、ShortName三者均会注册为别名配置的description 属性取值——即通过Configurator.UseEnumDescriptionPropertyLocator指定的、位于任意属性不限于DescriptionAttribute上的字符串属性值当该属性被选定为成员的author 描述时其取值也参与反查。匹配本身大小写不敏感底层FrozenDictionarystring, T使用StringComparer.OrdinalIgnoreCase见 EnumCache.cs 与 GetDehumanized不做空白 trim—— Display name 不会被还原成功这由DehumanizeDoesNotTrimAliases测试直接验证EnumHumanizeTests.cs。别名冲突的仲裁原则文档 Remark 原文若别名发生碰撞枚举无符号数值顺序靠后的成员优先因为构建字典时按Enum.GetValues的顺序依次AddAliases后写入者覆盖先写入者同时当前的 humanized 表示优先于补充别名成员名及其 Humanize 结果在AddAliases中最先写入随后才是 Display/description 别名。这两条规则分别有对应测试DehumanizeAliasCollisionsUseExistingCachePrecedenceEnumHumanizeTests.cs验证碰撞按缓存写入顺序仲裁DehumanizeRecognizesAliasesForDuplicateEnumValuesEnumHumanizeTests.cs验证值重复的多个成员别名都能命中。OnNoMatch 与 NoMatchFoundException错误处理的两个档位OnNoMatch定义于 src/Humanizer/OnNoMatch.cs是当前仅被DehumanizeTo使用的行为开关public enum OnNoMatch { ThrowsException, // 默认行为抛出 NoMatchFoundException ReturnsNull // 返回 null 而不是抛异常 }ThrowsException默认无匹配时抛出 NoMatchFoundException其消息形如Couldnt find any enum member that matches the string {input}携带原始输入便于排查ReturnsNull无匹配时返回 null此时泛型重载的返回类型必须是TTargetEnum?这也是该重载与无参版本返回类型不同的根本原因。测试DehumanizeThrowsForEnumNoMatch与DehumanizeCanReturnNullForEnumNoMatchEnumHumanizeTests.cs分别锁定这两种行为。实战建议面向用户输入的宽松解析如搜索框、导入数据用ReturnsNull再判空面向必须成功的内部映射用默认的ThrowsException及早暴露脏数据。缓存机制FrozenDictionary 与首次构建成本反查字典并不是每次调用都重新构建。EnumCacheTsrc/Humanizer/EnumCache.cs针对每个具体枚举类型T维护静态只读的FrozenDictionarystring, T首次访问时一次性构建含成员名、Humanize 结果、Display 别名与配置 description 别名之后所有DehumanizeTo调用共享同一份不可变字典。FrozenDictionary的不可变性同时带来两个好处并发读取安全且对 AOT/裁剪后的运行环境友好。这也是反向解析与正向 Humanize共用同一份元数据来源DefaultInfo的原因——DehumanizeTo还原的正是Humanize()生成的字符串二者天然互逆。例如EnumAliasesUnderTest.RawEnumName.Humanize()输出Display description而Display description.DehumanizeToEnumAliasesUnderTest()又能还原回去见 EnumHumanizeTests.cs。AOT / 裁剪环境注意事项如果目标框架是 .NET 6请注意DehumanizeTo依赖反射与动态代码路径泛型重载通过[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)]标注类型参数向裁剪器声明需要保留枚举的公共字段确保别名注册阶段TypeOfT.GetField(caseName)可用EnumCache.cs非泛型重载使用MakeGenericMethodInvoke因此标注了[RequiresDynamicCode]与[RequiresUnreferencedCode]EnumDehumanizeExtensions.cs在 AOT 发布或启用裁剪的构建中编译器会给出相应警告测试文件中也同步标注了[RequiresDynamicCode]/[RequiresUnreferencedCode]如 EnumHumanizeTests.cs说明这是被正式纳入兼容性矩阵的已知约束。在启用 NativeAOT 的项目中使用时应优先考虑编译期可解析的泛型重载并对非泛型重载做好可能产生警告/需要额外配置的预期。实战建议与边界总结推荐用法// 1. 编译期类型已知、输入可信默认抛异常 var userType Anonymous user.DehumanizeToUserType(); // 2. 编译期类型已知、输入不可信返回 null 再判断 UserType? type rawText.DehumanizeToUserType(OnNoMatch.ReturnsNull); if (type is null) { /* 处理无效输入 */ } // 3. 运行时才知道类型 Enum value text.DehumanizeTo(runtimeEnumType); // 返回后按需强转需要留意的边界输入为 null 属于前置条件违约文档明确要求非 null匹配不做 trim来自表单的字符串建议先自行Trim()别名冲突时数值靠后的成员优先若枚举存在别名重合且顺序敏感应检查成员声明顺序目标类型必须是真正的枚举否则抛ArgumentExceptionFlags 枚举的单项别名同样参与反查dark gray→DARK_GRAY见 EnumHumanizeTests.cs但组合值如Read | Write的字符串不在反查范围内DehumanizeTo面向的是单个成员。如果需要更完整的枚举 API 图景可对照阅读正向转换文档 Humanizer.EnumHumanizeExtensions 的 API 参考、错误类型 NoMatchFoundException、行为开关 OnNoMatch以及通用的字符串反向转换 StringDehumanizeExtensions处理some string→SomeString的 PascalCase 还原与枚举反查互补。【免费下载链接】HumanizerHumanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities项目地址: https://gitcode.com/gh_mirrors/hu/Humanizer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
