问题 具有循环依赖性的静态字段的反射GetValue返回null


注意:以下代码实际上可以正常工作,但显示了我自己的解决方案中失败的方案。有关更多信息,请参阅本文的底部。

有了这些课程:

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();
}

获取字段 One 和 Two

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或带有上述代码的简单单元测试类中,一切正常。但在我的解决方案中,我有一些单元测试想要使用这些字段的所有实例, GetValue 可以正常返回父类型的字段,但是如果父字段应该具有子类型的实例,则它们总是提供 null! (如果这发生在这里,最终的清单就是 { One, null } 代替 { One, Two }。)测试类与两种类型(每种类型都在自己的文件中)处于不同的项目中,但我暂时制作了 一切 上市。我已经删除了一个断点并检查了我可以检查的所有内容,并完成了相同的操作 fieldInfos[1].GetValue(null) 在一个Watch表达式中,它确实返回null,尽管我的主类中有一行与第二行完全相同 MainType 以上。

哪里不对?如何获取子类型字段的所有值?他们甚至可以在没有错误的情况下返回null?

根据理论,由于某些原因,由于通过反射访问,子类型的类不是静态构造的,我试过

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

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

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

附加信息

在一堆摆弄之后,代码开始工作了。现在它不再起作用了。我正在努力复制触发代码开始工作的内容。

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

问题再现可用

您可以通过下载此操作来播放我的方案的工作(失败)修剪版本 github问题的一些最小的工作示例

调试单元测试。当异常发生时,直到你到达GlossaryHelper.cs的第20行,并且可以看到返回值 GetGlossaryMembers 在里面 Locals 标签。您可以看到索引3到12为空。


5921
2017-10-21 17:48


起源

@Fredou标签已添加。 - ErikE
@Fredou是的,对不起,添加到帖子的末尾。 - ErikE
@Fredou我刚刚在我的精简项目中升级到4.6.1,在那里我试图产生问题的最小重现。 - ErikE
最后一个问题,32位还是64位? - Fredou
@Fredou平台目标是“任何CPU”,它是Windows的64位版本,不知道正在生成什么类型​​的EXE。 - ErikE


答案:


问题

该问题与Reflection无关,而是两个静态字段初始值设定项之间的循环依赖关系及其执行顺序。

请考虑以下代码段:

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 已经执行了(好吧,不是真的,它当前正在执行,但被认为是),它只返回当前字段值(null 那一刻)然后存储在 MainType.Two 并将通过该字段的进一步访问请求返回。

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

怎么修

如果可能,请避免此类静态字段依赖性。使用静态只读 性能 代替。它们为您提供完全控制,还允许您消除字段重复(目前您有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),因此没有基础构造函数调用引起的副作用。


14
2017-11-01 08:47



感谢您的答复。这绝对是一个有用的答案。我已经添加了一个github链接到这个问题,所以你可以玩实际的场景。 - ErikE
我确信如果不是全部的话,我会给予你大部分/大部分赏金;我只是在等着看是否有其他人有其他信息(例如继续使用字段的非连接方式)以及一些更一般的指导方针和背景(计算机科学理论?)来帮助我避免将来出现这样的陷阱。 - ErikE
静态初始化是一件艰难的事情。 Jon Skeet有一个 话题 在他的书中,我很确定你还能找到其他资源。但总的来说它不可靠,你无法从类中控制它,这打破了封装。如果您可以强制代码的用户始终首先调用强制的方法 MainType 静态初始化(在app启动时或其他东西),然后你可以继续使用字段,或者如果字段没有交叉依赖等,但是现在创建一个静态只读属性... - Ivan Stoev
......非常简单 - 基本上相同数量的代码,它们应该是首选。顺便说一句,谢谢你编辑答案!干杯。 - Ivan Stoev


我有类似的问题。问题是我实现了类的静态字段,并通过反射尝试使用它的值。它在我的调试解决方案中运行良好,但在我的生产环境中无效。 问题是Release配置中的编译器发现从不使用此静态方法并删除无法访问的代码。 要解决此问题,您应该删除Optimize code flag。


0
2017-11-01 04:16



不幸的是,那不是它。这在调试模式下失败,并且未设置Optimize标志。 - ErikE