具有循环依赖性的静态字段的反射 GetValue returns null

Reflection GetValue of static field with circular dependency returns null

注意:下面的代码实际上工作正常,但显示了我自己的解决方案中失败的场景。有关详细信息,请参阅此 post 的底部。

有了这些 classes:

public class MainType {
   public static readonly MainType One = new MainType();
   public static readonly MainType Two = SubType.Two;
}

public sealed class SubType : MainType {
   public new static readonly SubType Two = new SubType();
}

获取字段 OneTwo:

List<FieldInfo> fieldInfos = typeof(MainType)
   .GetFields(BindingFlags.Static | BindingFlags.Public)
   .Where(f => typeof(MainType).IsAssignableFrom(f.FieldType))
   .ToList();

最后,获取它们的值:

List<MainType> publicMainTypes = fieldInfos
   .Select(f => (MainType) f.GetValue(null))
   .ToList();

在 LinqPad 或简单的单元测试中 class 使用上述代码,一切正常。但是在我的解决方案中,我有一些单元测试想要处理这些字段的所有实例,GetValue 可以很好地处理父类型的 return 字段,但是父字段应该有实例的子类型,他们总是改为 null! (如果发生这种情况,最终列表将是 { One, null } 而不是 { One, Two }。)测试 class 与这两种类型在不同的项目中(每个都在它们自己的文件中),但是我临时做了一切public。我在其中放置了一个断点并检查了我可以检查的所有内容,并在 Watch 表达式中完成了 fieldInfos[1].GetValue(null) 的等效操作,实际上它确实 return null,尽管事实上有一行在我的主要 class 中,与上面 MainType 中的第二个完全一样。

怎么了?如何获取子类型字段的所有值?他们怎么可能 return null 而没有错误?

根据理论上可能由于某种原因子类型的 class 由于通过反射访问而未被静态构造的理论,我尝试了

System.Runtime.CompilerServices.RuntimeHelpers
  .RunClassConstructor(typeof(SubType).TypeHandle);

在开始之前位于顶部,但它没有帮助(其中 SubType 是我项目中的实际子类型 class)。

我会继续努力尝试在一个简单的案例中重现这一点,但我暂时没有想法。

附加信息

经过一堆摆弄,代码开始工作了。现在它不再工作了。我正在努力重现触发代码开始工作的内容。

注意:在 Visual Studio 2015 年使用 C# 6.0 定位 .Net 4.6.1。

问题重现可用

您可以通过下载这个 somewhat minimal working example of the problem at github.

来玩我的场景的工作(失败)精简版本

调试单元测试。出现异常时,单步执行到GlossaryHelper.cs的第20行,可以在Locals选项卡中看到GetGlossaryMembers的return值。您可以看到索引 3 到 12 为空。

我遇到了类似的问题。问题是我实现了 class 的静态字段,并通过反射尝试使用它的值。它在我的调试解决方案中运行良好,但在我的生产环境中运行不佳。 问题是发布配置中的编译器发现从未使用过此静态方法并删除了无法访问的代码。 要解决此问题,您应该删除优化代码标志。

问题

这个问题与反射无关,而是两个静态字段初始化器之间的循环依赖及其执行顺序。

考虑以下片段:

var b = MainType.Two;
var a = SubType.Two;
Debug.Assert(a == b); // Success

现在让我们交换前两行:

var a = SubType.Two;
var b = MainType.Two;
Debug.Assert(a == b); // Fail! b == null

这是怎么回事?让我们看看:

  1. 代码第一次尝试访问 SubType.Two 静态字段。
  2. 静态初始化程序触发并执行 SubType 的构造函数。
  3. 由于SubType继承自MainTypeMainType构造函数也执行并触发MainType静态初始化。
  4. MainType.Two 字段静态初始值设定项正在尝试访问 SubType.Two。由于静态初始化程序只执行一次,并且 SubType.Two 的初始化程序已经执行(好吧,不是真的,它当前正在执行,但被认为是),它只是 returns 当前字段值(null 在那一刻),然后存储在 MainType.Two 中,并将由对该字段的进一步访问请求返回。

简而言之,这种设计的正确工作实际上取决于外部访问字段的顺序,所以它有时有效,有时无效也就不足为奇了。不幸的是,这是你无法控制的。

如何修复

如果可能,请避免此类静态字段依赖项。请改用 static readonly properties。它们为您提供完全控制,还允许您消除字段重复(目前您有 2 个不同的字段,它们包含一个相同的值)。

这是没有此类问题的等效设计(使用 C#6.0):

public class MainType
{
    public static MainType One { get; } = new MainType();
    public static MainType Two => SubType.Two;
}

public sealed class SubType : MainType
{
    public new static SubType Two { get; } = new SubType();
}

当然,这需要更改您的反射代码以使用 GetProperties 而不是 GetFields,但我认为这是值得的。

更新: 另一种解决问题的方法是将静态字段移动到嵌套的抽象容器中 类:

public class MainType
{
    public abstract class Fields
    {
        public static readonly MainType One = new MainType();
        public static readonly MainType Two = SubType.Fields.Two;
    }
}

public sealed class SubType : MainType
{
    public new abstract class Fields : MainType.Fields
    {
        public new static readonly SubType Two = new SubType();
    }
}

现在两个测试都成功完成:

var a = SubType.Fields.Two;
var b = MainType.Fields.Two;
Debug.Assert(a == b); // Success

var b = MainType.Fields.Two;
var a = SubType.Fields.Two;
Debug.Assert(a == b); // Success

这是因为容器类除了被包含在之外,与静态字段类型没有关系,因此它们的静态初始化是独立的。此外,尽管它们使用继承,但它们永远不会被实例化(因为是 abstract),因此没有由基本构造函数调用引起的副作用。