【问题标题】:Are .NET enum types actually mutable value types?.NET 枚举类型实际上是可变值类型吗?
【发布时间】:2013-07-26 21:24:48
【问题描述】:

通过反射查看枚举类型的字段,我惊讶地注意到保存枚举的特定实例的实际值的“支持”实例字段不是private,就像我应该做的那样想,但是public。它也不是readonly。 (IsPublic 真,IsInitOnly 假。)

很多人认为 .NET 类型系统中的“可变”值类型是“邪恶的”,那么为什么是枚举类型(例如从 C# 代码创建)就是这样?

现在,事实证明,C# 编译器有某种魔法可以否认公共实例字段的存在(但见下文),但在例如PowerShell 你可以这样做:

prompt> $d = [DayOfWeek]::Thursday
prompt> $d
Thursday
prompt> $d.value__ = 6
prompt> $d
Saturday

value__ 字段可以写入。

现在,要在 C# 中执行此操作,我必须使用 dynamic,因为似乎使用正常的编译时成员绑定,C# 会假装 public 实例字段不存在。当然要使用dynamic,我们将不得不使用枚举值的装箱

这是一个 C# 代码示例:

// create a single box for all of this example
Enum box = DayOfWeek.Thursday;

// add box to a hash set
var hs = new HashSet<Enum> { box, };

// make a dynamic reference to the same box
dynamic boxDyn = box;

// see and modify the public instance field
Console.WriteLine(boxDyn.value__);  // 4
boxDyn.value__ = 6;
Console.WriteLine(boxDyn.value__);  // 6 now

// write out box
Console.WriteLine(box);  // Saturday, not Thursday

// see if box can be found inside our hash set
Console.WriteLine(hs.Contains(box));  // False

// we know box is in there
Console.WriteLine(object.ReferenceEquals(hs.Single(), box));  // True

我认为 cmets 不言自明。我们可以通过public 字段改变枚举类型DayOfWeek 的实例(可以是来自BCL 程序集或“自制”程序集的任何枚举类型)。由于实例在hashtable中,mutation导致hash码发生变化,导致实例在mutation后在错误的“bucket”中,HashSet&lt;&gt;无法运行。

.NET的设计者为什么选择将枚举类型的实例字段设为public

【问题讨论】:

  • 我想知道您对理想实现的想法。你愿意分享吗?
  • 其实有一种更简单的方法可以证明枚举类型是可变的,无需装箱或动态:gist.github.com/thomaslevesque/6100447
  • @KenKin 好吧,最重要的是,我的想法是保存当前实例实际值的实例字段应该是private。在我看来,它的名称是否包含“有趣”字符(C# 中的标识符名称中不可能出现的字符)并不重要。
  • @ThomasLevesque 有了它,您甚至可以“变异”真正不可变的类型,如 DateTime(更改私有字段 ulong dateData)甚至所谓的原始类型,如 int(更改私有字段 @ 987654341@)。您需要做的唯一更改是在GetField 调用中指定BindingFlags.Instance | BindingFlags.NonPublic。但我不认为你可以通过反射改变任何东西是一个问题。这就是反思的本质。 (尽管__makeref C# 关键字很有趣,以避免按值复制装箱。)我上面给出的示例仅因为 value__ 是公开的。
  • @JeppeStigNielsen,说得好。我忘了反射可以让你改变只读字段...

标签: c# .net powershell enums immutability


【解决方案1】:

对于不熟悉枚举如何在幕后生成的读者,让我尝试理解这个相当令人困惑的问题。 C#代码:

enum E { A, B }

成为IL

.class private auto ansi sealed E extends [mscorlib]System.Enum
{
  .field public specialname rtspecialname int32 value__
  .field public static literal valuetype E A = int32(0x00000000)
  .field public static literal valuetype E B = int32(0x00000001)
} 

或者,再次用 C# 重写,枚举等效于以下伪 C#:

struct E : System.Enum
{
    public int value__;
    public const E A = 0;
    public const E B = 1;
}

问题是:为什么魔法领域value__是公开的?

我没有参与这个设计决定,所以我必须做出有根据的猜测。我有根据的猜测是:如果字段不是公共的,你如何初始化结构的实例?

您创建了一个构造函数,然后必须调用它,这为抖动提供了工作,而该工作的性能成本能为您带来什么?如果答案是“它为我赢得了运行时间,防止自己做一些我一开始就不应该做的愚蠢和危险的事情,而且我必须非常努力地去做”,那么我向你表明这不是一个令人信服的成本效益比。

由于实例在hashtable中,由于mutation导致hash码发生变化,导致实例在mutation后在错误的“桶”中,HashSet无法正常工作。

这比“如果你这样做时感到疼痛,那么停止这样做”这一行已经过了几英里。

【讨论】:

  • 我对此知之甚少,无法判断使用单线构造函数来完成这项工作是否会产生性能成本,但我想你是对的,解释是与性能相关的。我认为,该设计之所以成功,是因为 C# 编译器中的这种魔力使它无法“看到”公共领域。如果value__ 出现在智能感知中并且可以被写入(无需使用dynamic“非常努力地工作”),很多人会写入该字段,这将改变枚举的“实用”语义.
  • 而且有点奇怪的是,非动态类型的编译时绑定忽略了value__ 字段,而dynamic 的运行时绑定选择并使用该字段而不抛出。
  • 最后一点让我印象深刻。
  • 有趣。我在这里的许多问题都试图展示一个错误。如果只有神奇的实例字段的名称包含&lt;&gt; 字符,我们将无法仅使用boxDyn.nameOfField 访问它。
  • 您的解释似乎有道理,但我认为它不正确。 var d = DayOfWeek.Monday 编译为 ldc.i4.1; stloc.0,而 (int)d 编译为 ldloc.0。所以该字段根本不被访问:编译器生成的代码与枚举与 int 相同。
猜你喜欢
  • 1970-01-01
  • 2013-01-11
  • 2018-10-18
  • 1970-01-01
  • 2018-03-06
  • 1970-01-01
  • 1970-01-01
  • 2012-04-26
  • 1970-01-01
相关资源
最近更新 更多