为啥我可以通过反射在字段初始化后重写静态只读字段?

Posted

技术标签:

【中文标题】为啥我可以通过反射在字段初始化后重写静态只读字段?【英文标题】:Why I can rewrite static readonly field after field initiaization by reflection?为什么我可以通过反射在字段初始化后重写静态只读字段? 【发布时间】:2020-08-14 20:33:12 【问题描述】:

我们知道静态构造函数是在第一次调用类时调用的。

我们现在有一个类:

static class Demo

    private readonly static int _intValue;

    static Demo ()
    
        _intValue = 5;
    

在访问Demo 时将调用构造函数。值 5 将分配给 _intValue 只读字段。之后,我们可以再次通过反射设置值:

typeof (Demo)
  .GetField ("_ intValue", BindingFlags.Static | BindingFlags.NonPublic)
  .SetValue (null, 567);

为什么值5 可以被覆盖?它不是已经在构造函数中初始化了吗?为什么不抛出System.FieldAccessException

在 NET Core 3.1 中测试。完整示例https://dotnetfiddle.net/4DsJ6H

更新: 我找到了一种方法来覆盖只读静态字段,即使它被请求一次。它基于通过 IL 生成的方法。实际上,除了最初的问题 - 这种解决方法是如何工作的?

https://dotnetfiddle.net/oYaf5t

【问题讨论】:

Reflection 让您可以做普通 C# 代码甚至编译后的 IL 代码通常做不到的事情。例如,您可以从其声明类型之外调用私有方法。这似乎是一个类似的案例。 @JoeSewell 为什么你没有提到它作为答案? @JoeSewell 我很感兴趣这个任务是如何详细完成的,它绕过了只读字段的限制?这是由环境提供的吗?为什么?或者这只是 .NET Core 的一个特性?例如,在 NET Framework 4.7 中,通过反射根本找不到静态只读字段。这有什么关系吗?我只是不知道要掌握什么才能更好地理解问题中描述的行为。 “在 NET Framework 4.7 中,根本不会通过反射找到静态只读字段”——这根本不是真的;而在 .net core 3.1 中,您 不能 依赖于能够通过反射获得静态 ReadOnly 字段,因为它会影响某些新的 JIT 优化(但这可能仅适用于 ref 类型(类),对于去虚拟化) 这里是@MarcGravell 的更深层次的discussion。 【参考方案1】:

反射已经打破了所有规则,包括可访问性和可变性;它实际上和unsafe 一样强大:就像unsafe 一样:如果出现问题,它是自己造成的,运行时会嘲笑你。

请注意,在 .NET Core 中,运行时有时会阻止您执行此操作,因为如果您这样做,JIT 优化将变得无效。但如果它不在这里:很好。

注意:您过去可以通过反射更改string.Empty。想象一下结局有多好:)

【讨论】:

"注意:你曾经能够更改 string.Empty" 我在用我的调度程序替换 TaskScheduler.Default 时问了上面的问题。当然,不是在产品中:) 我是否正确理解在这个简单的示例中,运行时允许覆盖只读静态字段,因为它正确地执行了 JIT?如果是这样,你能举例说明什么时候不能这样做吗? @KregHEk Sean Skelly 已经为您链接了一个。也许与您的示例是值类型有关,因此不需要(或可以)进行去虚拟化。 @KregHEk 忍不住;这是另一个带有示例的链接,这次是a SO answer。 tl; dr:与仅具有静态只读字段相比,同时具有静态构造函数 静态只读字段的组合会在设置字段时导致 IL 中的细微差异。但一定要通读它并注意“beforefieldinit”。

以上是关于为啥我可以通过反射在字段初始化后重写静态只读字段?的主要内容,如果未能解决你的问题,请参考以下文章

常量数据与只读字段

为啥我无法在 C++ 中初始化静态字段 [重复]

为啥静态字段初始化失败导致NoClassDefFoundError?

为啥静态字段没有及时初始化?

为啥 Django 中的只读表单字段是个坏主意?

C#各种字段类型对比