【问题标题】:Reflection GetValue of static field with circular dependency returns null具有循环依赖的静态字段的反射GetValue返回null
【发布时间】:2016-10-21 17:48:44
【问题描述】:

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

使用这些类:

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 或带有上述代码的简单单元测试类中,一切正常。但是在我的解决方案中,我有一些单元测试想要处理这些字段的所有实例,GetValue 可以很好地返回父类型的字段,但是如果父字段应该有子类型的实例,它们总是而是给null! (如果这里发生这种情况,最终列表将是 { One, null } 而不是 { One, Two }。)测试类与这两种类型位于不同的项目中(每种类型都在自己的文件中),但我暂时制作了 一切公开。我已经在其中放置了一个断点并检查了所有我可以检查的内容,并且在 Watch 表达式中完成了与 fieldInfos[1].GetValue(null) 等效的操作,它实际上返回 null,尽管事实上我的主类中有一行完全像第二个来自上述MainType

怎么了?如何获取子类型字段的所有值?他们怎么可能在没有错误的情况下返回 null?

理论上,也许由于某种原因,由于通过反射访问,子类型的类没有被静态构造,我尝试了

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

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

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

其他信息

经过一番折腾,代码开始工作了。现在它不再工作了。我正在努力重现触发代码开始工作的原因。

注意:在 Visual Studio 2015 中使用 C# 6.0 以 .Net 4.6.1 为目标。

可重现问题

您可以通过下载此somewhat minimal working example of the problem at github 来使用我的场景的工作(失败)精简版本。

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

【问题讨论】:

  • @Fredou 标签已添加。
  • @Fredou 是的,抱歉,已添加到帖子末尾。
  • @Fredou 我刚刚在我的精简项目中升级到 4.6.1,我正在尝试对问题进行最低限度的重现。
  • 最后一个问题,32 位还是 64 位?
  • @Fredou 平台目标是“任何 CPU”,它是 Windows 的 64 位版本,但不确定正在生成什么类型​​的 EXE。

标签: c# reflection visual-studio-2015 static c#-6.0


【解决方案1】:

问题

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

考虑以下 sn-p:

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 中,并将由对该字段的进一步访问请求返回。

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

如何解决

如果可能,请避免此类静态字段依赖。请改用静态只读 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),因此不会产生由基构造函数调用引起的副作用。

【讨论】:

  • 感谢您的回复。这绝对是一个有用的答案。我已经为问题添加了一个 github 链接,以便您可以使用实际场景。
  • 我敢肯定,如果不是全部,我会奖励你大部分/大部分的赏金;我只是在等待是否有其他人有其他信息(例如继续使用字段的非技巧方法)以及一些更一般的指导方针和背景(计算机科学理论?),以帮助我在未来避免此类陷阱。
  • 静态初始化是一件很难的事情。 Jon Skeet 在他的书中有一个topic,我很确定你可以找到其他来源。但总的来说,它不可靠,您无法从类中控制它,这会破坏封装。如果您可以强制代码的用户始终首先调用强制 MainType 静态初始化的方法(在应用程序启动时或其他情况下),那么您可以继续使用字段,或者如果字段没有交叉依赖等,但是因为现在创建一个静态只读属性...
  • ... 如此简单 - 基本上相同数量的代码,它们确实应该是首选。顺便说一句,感谢您编辑答案!干杯。
【解决方案2】:

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

【讨论】:

  • 不幸的是,事实并非如此。这在调试模式下失败,并且未设置优化标志。
猜你喜欢
  • 1970-01-01
  • 2021-09-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-15
  • 1970-01-01
相关资源
最近更新 更多