【问题标题】:Why is the order of the enums important when specifying circular enum values in .NET 5?为什么在 .NET 5 中指定循环枚举值时枚举的顺序很重要?
【发布时间】:2021-10-07 17:46:24
【问题描述】:

我正在使用枚举循环引用将一些现有枚举归结为更少的值。 它适用于我的问题,因为它适用于过渡时期,在历史中确实会出现旧的枚举值,但不会创建具有过时值的新条目。

我不是在寻找替代方法,但我偶然发现了这个奇怪的问题,其中枚举的顺序会以意想不到的方式影响序列化值。

我有这个枚举:

    public enum CivilStatusEnum
    {
        None = 0,
        Married = 1,
        Cohabiting = Married,
        Alone = 3,
        DivorcedOrSeparated = Alone,
        WidowOrWidower = Alone,
    }

我指定了“DivorcedOrSeparated”= 单独 (3)。现在,当我将枚举转换为字符串时,我得到值“DivorcedOrSeparated”。

Console.PrintLine(CivilStatusEnum.Alone.ToString());

输出:

DivorcedOrSeparated

我有一个示例代码,其中包含测试及其预期结果。如您所见,测试失败。但是如果我改变枚举的顺序,测试就会解析。

    [Theory]
    [InlineData(CivilStatusEnum.Alone, "Alone")]
    [InlineData(CivilStatusEnum.DivorcedOrSeparated, "Alone")]
    [InlineData(CivilStatusEnum.WidowOrWidower, "Alone")]
    [InlineData(CivilStatusEnum.None, "None")]
    [InlineData(CivilStatusEnum.Married, "Married")]
    [InlineData(CivilStatusEnum.Cohabiting, "Married")]
    public void Test(CivilStatusEnum input, string expected)
    {
        var text = input.ToString();
        text.Should().Be(expected);
    }

我似乎无法找到一个合理的解释来解释为什么顺序对 tostring 和 serilization 很重要。

这是 .NET 5 中的错误,还是我遗漏了什么? 如果这种行为是故意的,它如何确定哪个枚举名称将是 tostring 的输出?

谢谢你:)

【问题讨论】:

  • 存储的不是枚举文本(“Alone”),而是它的值(3)。那么你的意思是哪个值 3 ?独自一人、离婚或分居,还是寡妇或鳏夫?
  • 附带说明:长期以来,我也将枚举视为“可以从中选择一个的文本值集”。但归根结底,枚举只是整数数据类型的一个很好的包装器。而且您的代码应该依赖于该底层数据类型,而不是它们的视觉表示。例如,如果您将整数转换为枚举类型。即使实际的整数可能甚至不是有效的变体,结果也会看起来像枚举变体之一。枚举只是不知道如何处理这个问题。相信枚举文本表示可能会产生很大的误导。
  • 我知道枚举是值是 int 而不是字符串。但是我们谈论的是一个具有公共 API 和大量需要可用的历史数据的现有系统,所以我不能不付出很大的努力就改变方法。我只是想知道在调用 ToString() 时它如何确定要返回的字符串。

标签: c# enums .net-5


【解决方案1】:

Enum.ToString 执行二分查找。

确实,ToString 调用InternalFormat,后者调用GetEnumName。该方法在 EnumInfo.Values 返回的数组中执行二进制搜索。

我假设数组是按照底层值的递增顺序填充的(否则二进制搜索将不起作用),并且如果它们相等,则按照在源代码中声明值的顺序填充。这使得搜索结果依赖于声明的顺序。

为了说明这种二分搜索的效果,请考虑以下两个enum 定义:

enum Test1 { A = 0, B = 0, C = 0 }
enum Test2 { A = 0, B = 0, C = 0, D = 0, E = 0 }

Test1.A.ToString() 的结果是什么?注意Test1.A 的值为0。二分查找将首先考虑列表中间的元素,即B,其值为0。该值等于我们正在搜索的值,因此Test1.A.ToString() 返回"B"。如果找到的值高于正在搜索的值,则搜索将在列表的下半部分继续。如果找到的值低于正在搜索的值,则搜索将在列表的上半部分继续。

枚举中的所有常量也是如此,因为它们都具有相同的值。因此,Test1.C.ToString() 将同样返回 "B"

同样,Test2.A.ToString() 返回 "C",正如预期的那样。

但是请注意,虽然这种行为在当前版本的 .NET 中似乎是可预测的,但它是未定义的,并且可能会在未来的版本中发生变化。

这不是 .NET 5 中的错误。毕竟,以下两种情况都不可能成立:

CivilStatusEnum.Alone.ToString() ==  "Alone"
CivilStatusEnum.DivorcedOrSeparated.ToString() == "DivorcedOrSeparated"

原因当然是CivilStatusEnum.Alone == CivilStatusEnum.DivorcedOrSeparated

以下是the documentation 对此的评价:

如果多个枚举成员具有相同的基础值,并且您尝试根据其基础值检索枚举成员名称的字符串表示形式,则您的代码不应对该方法将返回哪个名称做出任何假设。

【讨论】:

  • 从我的测试来看,顺序似乎不是由源代码中的顺序决定的,至少不是以预期的方式。 “Alone”是第一个声明的并且仍然是所有 ToString(),在任何值为 3 的枚举上都将返回“DivorcedOrSeparated”。这就是让我困惑的地方。如果我重新排序我的枚举,它将返回另一个值 fx "WidowOrWidower" ,我只是不知道它如何确定要返回哪个值。从您发现的文档中,这听起来确实像一个错误,他们只是选择通过添加免责声明来忽略它:)
  • 不是因为“Alone”是第一个出现在源代码中的,才会找到“Alone”。如果这是一个简单的线性搜索,情况就是这样,但事实并非如此。这是一个二分查找。例如,如果您在最后添加更多值,您可能会看到其他结果。
  • @Jeanette 我添加了一个示例来说明二分查找的效果。
【解决方案2】:

我知道你说过你不是在寻找替代方法,但另一种方法是使用标志来避免这种问题:

[Flags]
public enum CivilStatusEnum
{
    None = 0,
    Married = 1,
    Cohabiting = 3, //Married | 2
    Alone = 4,
    DivorcedOrSeparated = 12, //Alone | 8
    WidowOrWidower = 20, //Alone | 16
}

然后ToString 将产生正确的答案,您可以使用以下代码检查某人是否已婚或单身:

bool IsMarried(CivilStatusEnum e){
    return ((int)e&1) == 1;
}

bool IsAlone(CivilStatusEnum e){
    return ((int)e&4) == 4;
}

【讨论】:

  • 为什么在 IsAlone 中需要 e!=0?如果 && 的右侧为真,则第一个也是如此。我错过了什么吗?
  • @GianPaolo 是的,你是对的。当我开始编写那段代码时,我正在考虑另一种方法。现已修复,谢谢!
猜你喜欢
  • 1970-01-01
  • 2012-08-31
  • 2012-01-30
  • 2020-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-13
相关资源
最近更新 更多