小弧光的博客

公众号:小弧光黑板报

导航

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。

posted on 2022-11-09 11:55  小弧光  阅读(49)  评论(0编辑  收藏  举报