C#中使用记录的枚举类
文章为机翻
原文:https://josef.codes/enumeration-class-in-c-sharp-using-records/
其他参考:https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types
介绍
在我多年来从事的所有项目中,或多或少都使用了某种枚举类。
枚举(或简称枚举类型)是围绕整数类型的精简语言包装器。您可能希望将它们的使用限制在从一组封闭值中存储一个值时。基于大小(小、中、大)的分类就是一个很好的例子。将枚举用于控制流或更健壮的抽象可能是一种代码味道。这种类型的使用会导致带有许多控制流语句检查枚举值的脆弱代码。
相反,您可以创建启用面向对象语言的所有丰富功能的枚举类。
枚举的示例实现:
public class CardType : Enumeration
{
public static CardType Amex = new(1, nameof(Amex));
public static CardType Visa = new(2, nameof(Visa));
public static CardType MasterCard = new(3, nameof(MasterCard));
public CardType(int id, string name)
: base(id, name)
{
}
}
本周,我注意到我当前的项目也使用了Enumeration 类。他们的实现基于Microsoft 提供的以下类。
实现
微软
public abstract class Enumeration : IComparable
{
public string Name { get; private set; }
public int Id { get; private set; }
protected Enumeration(int id, string name) => (Id, Name) = (id, name);
public override string ToString() => Name;
public static IEnumerable<T> GetAll<T>() where T : Enumeration =>
typeof(T).GetFields(BindingFlags.Public |
BindingFlags.Static |
BindingFlags.DeclaredOnly)
.Select(f => f.GetValue(null))
.Cast<T>();
public override bool Equals(object obj)
{
if (obj is not Enumeration otherValue)
{
return false;
}
var typeMatches = GetType().Equals(obj.GetType());
var valueMatches = Id.Equals(otherValue.Id);
return typeMatches && valueMatches;
}
public override int GetHashCode() => Id.GetHashCode();
public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
{
var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id);
return absoluteDifference;
}
public static T FromValue<T>(int value) where T : Enumeration
{
var matchingItem = Parse<T, int>(value, "value", item => item.Id == value);
return matchingItem;
}
public static T FromDisplayName<T>(string displayName) where T : Enumeration
{
var matchingItem = Parse<T, string>(displayName, "display name", item => item.Name == displayName);
return matchingItem;
}
private static T Parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration
{
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
if (matchingItem == null)
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
return matchingItem;
}
public int CompareTo(object other) => Id.CompareTo(((Enumeration)other).Id);
}
引起我注意的一件事是GetAll方法。它使用反射来返回类的所有声明字段(上例中的所有CardTypes)。虽然它并不像看起来那么糟糕,但您不会在每次调用该GetAll
方法时支付反射价格。这是因为运行时在涉及反射和元数据时会进行缓存。您可以在此处阅读更多相关信息。
我们在工作中的实施
这是我在当前工作项目中找到的版本。除了一堆实用方法外,它看起来或多或少与 Microsoft 实现相同。
public abstract class Enumeration : IComparable
{
protected Enumeration() { Description = string.Empty; }
protected Enumeration(int value, string description)
{
if (value < -1 || value == 0)
{
throw new Exception(
$"Invalid value: {value}. Please use -1 to represent a null value and positive values otherwise.");
}
Value = value;
if (description.Length > MaxDescriptionLength)
{
throw new Exception($"Display name can be max {MaxDescriptionLength} characters");
}
Description = description;
}
public int Value { get; }
public string Description { get; }
public string GetName<T>() where T : Enumeration, new()
{
var type = typeof(T);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly);
foreach (var info in fields)
{
var instance = new T();
var locatedValue = info.GetValue(instance) as T;
if (locatedValue?.Value == Value)
{
return info.Name;
}
}
throw new Exception($"The enumeration value {Value} could not be found");
}
public override string ToString()
{
return Description;
}
public static IEnumerable<T> GetAll<T>() where T : Enumeration
{
var type = typeof(T);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly);
foreach (var info in fields)
{
if (info.GetValue(null) is T locatedValue)
{
yield return locatedValue;
}
}
}
public override bool Equals(object? obj)
{
if (!(obj is Enumeration otherValue))
{
return false;
}
var typeMatches = GetType() == obj.GetType();
var valueMatches = Value.Equals(otherValue.Value);
return typeMatches && valueMatches;
}
public override int GetHashCode()
{
return HashCode.Combine(Value);
}
public static bool Exists<T>(int value) where T : Enumeration, new()
{
var matchingItem = GetMatchingItem<T>(item => item.Value == value);
return matchingItem != null;
}
public static bool Exists<T>(string description) where T : Enumeration, new()
{
var matchingItem = GetMatchingItem<T>(item => item.Description == description);
return matchingItem != null;
}
public static T FromValue<T>(int value) where T : Enumeration
{
var matchingItem = Parse<T, int>(value, "value", item => item.Value == value);
return matchingItem;
}
public static Result<T> Parse<T>(int value) where T : Enumeration
{
var matchingItem = GetMatchingItem<T>(item => item.Value == value);
if (matchingItem == null)
{
return Result.Failure<T>(
new Error(ErrorTypes.Validation, $"'{value}' is not a valid value in {typeof(T)}"));
}
return Result.Success(matchingItem);
}
public static T FromDescription<T>(string description) where T : Enumeration
{
var matchingItem = Parse<T, string>(description, "description", item => item.Description == description);
return matchingItem;
}
private static T Parse<T, TK>(TK value, string description, Func<T, bool> predicate) where T : Enumeration
{
var matchingItem = GetMatchingItem(predicate);
if (matchingItem == null)
{
var message = $"'{value}' is not a valid {description} in {typeof(T)}";
throw new Exception(message);
}
return matchingItem;
}
public static T? GetMatchingItem<T>(Func<T, bool> predicate) where T : Enumeration
{
return GetAll<T>().FirstOrDefault(predicate);
}
public int CompareTo(object? other)
{
if (!(other is Enumeration e))
{
throw new ArgumentException("obj is not the same type as this instance");
}
return Value.CompareTo(e.Value);
}
}
这个实现有同样的GetAll
问题,它在每次调用时都使用反射。
记录执行
通过使用记录,我们可以免费获得相等检查,因为记录自动实现了IEquatable<T>接口。遗憾的是,records 没有实现IComparable接口,所以我们需要自己做。
我的想法是使Enumeration类通用,然后T
在静态构造函数中使用并使用反射来获取所有实例。然后,我会将所有实例缓存在两个字典中,一个使用Value作为键,另一个使用DisplayName作为键。这将允许非常快速的查找,接近 O(1)。
如果我们不将字典的初始化包装在Lazy类中,我们将得到运行时错误。这是因为静态构造函数将在我们实际实现的构造函数之前运行。发生这种情况时,我们不会从反射调用中获得任何值,一切都将为空。不好 :)。
public abstract record Enumeration<T> : IComparable<T> where T : Enumeration<T>
{
private static readonly Lazy<Dictionary<int, T>> AllItems;
private static readonly Lazy<Dictionary<string, T>> AllItemsByName;
static Enumeration()
{
AllItems = new Lazy<Dictionary<int, T>>(() =>
{
return typeof(T)
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(x => x.FieldType == typeof(T))
.Select(x => x.GetValue(null))
.Cast<T>()
.ToDictionary(x => x.Value, x => x);
});
AllItemsByName = new Lazy<Dictionary<string, T>>(() =>
{
var items = new Dictionary<string, T>(AllItems.Value.Count);
foreach (var item in AllItems.Value)
{
if (!items.TryAdd(item.Value.DisplayName, item.Value))
{
throw new Exception(
$"DisplayName needs to be unique. '{item.Value.DisplayName}' already exists");
}
}
return items;
});
}
protected Enumeration(int value, string displayName)
{
Value = value;
DisplayName = displayName;
}
public int Value { get; }
public string DisplayName { get; }
public override string ToString() => DisplayName;
public static IEnumerable<T> GetAll()
{
return AllItems.Value.Values;
}
public static int AbsoluteDifference(Enumeration<T> firstValue, Enumeration<T> secondValue)
{
return Math.Abs(firstValue.Value - secondValue.Value);
}
public static T FromValue(int value)
{
if (AllItems.Value.TryGetValue(value, out var matchingItem))
{
return matchingItem;
}
throw new InvalidOperationException($"'{value}' is not a valid value in {typeof(T)}");
}
public static T FromDisplayName(string displayName)
{
if (AllItemsByName.Value.TryGetValue(displayName, out var matchingItem))
{
return matchingItem;
}
throw new InvalidOperationException($"'{displayName}' is not a valid display name in {typeof(T)}");
}
public int CompareTo(T? other) => Value.CompareTo(other!.Value);
}
如果您不想使用记录,当然可以将上述方法与类一起使用。
让我们做一些基准测试!
基准
BenchmarkDotNet=v0.13.2, OS=Windows 11 (10.0.22621.521)
11th Gen Intel Core i7-11370H 3.30GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.100-rc.1.22431.12
[Host] : .NET 7.0.0 (7.0.22.42610), X64 RyuJIT AVX2
.NET 7.0 : .NET 7.0.0 (7.0.22.42610), X64 RyuJIT AVX2
Job=.NET 7.0 Runtime=.NET 7.0
| Method | Categories | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|-------------------- |----------- |----------:|---------:|---------:|----------:|------:|--------:|-------:|----------:|------------:|
| FromName_Microsoft | FromName | 148.71 ns | 2.985 ns | 5.228 ns | 146.31 ns | 1.00 | 0.00 | 0.0381 | 240 B | 1.00 |
| FromName_Ours | FromName | 108.93 ns | 1.214 ns | 1.077 ns | 108.70 ns | 0.72 | 0.03 | 0.0381 | 240 B | 1.00 |
| FromName_Record | FromName | 17.84 ns | 0.162 ns | 0.143 ns | 17.82 ns | 0.12 | 0.00 | - | - | 0.00 |
| | | | | | | | | | | |
| FromValue_Microsoft | FromValue | 192.86 ns | 3.731 ns | 5.585 ns | 192.47 ns | 1.00 | 0.00 | 0.0381 | 240 B | 1.00 |
| FromValue_Ours | FromValue | 148.31 ns | 1.598 ns | 1.335 ns | 148.00 ns | 0.77 | 0.02 | 0.0381 | 240 B | 1.00 |
| FromValue_Record | FromValue | 10.89 ns | 0.087 ns | 0.081 ns | 10.88 ns | 0.06 | 0.00 | - | - | 0.00 |
| | | | | | | | | | | |
| GetAll_Microsoft | GetAll | 240.63 ns | 4.043 ns | 3.782 ns | 239.61 ns | 1.00 | 0.00 | 0.0381 | 240 B | 1.00 |
| GetAll_Ours | GetAll | 186.35 ns | 2.347 ns | 2.196 ns | 185.49 ns | 0.77 | 0.01 | 0.0381 | 240 B | 1.00 |
| GetAll_Record | GetAll | 23.43 ns | 0.324 ns | 0.287 ns | 23.32 ns | 0.10 | 0.00 | 0.0127 | 80 B | 0.33 |
巨大的成功!记录实现比其他实现更快并且分配更少。这是因为我们使用了惰性泛型方法,它允许我们缓存所有项目,以便我们只支付一次反射成本。真的,这与使用记录没有任何关系。记录帮助我们的唯一一件事是我们需要编写更少的代码(Equals 等)。:)
所以再一次,我们在记录实现中使用的“通用方法”在“常规类实现”中同样有效。
另一个大大提高性能的事情是我们在进行 FromName 和 FromValue 查找时使用了 Dictionary。
转载请注明原文链接:https://www.cnblogs.com/itfanr/p/16873166.html
公众号:小弧光黑板报