在 .NET 中,即使对象的构造函数永远不会 运行,终结器也可以是 运行 吗?

In .NET, can a finalizer be run even if an object's constructor never ran?

我知道在 .NET 中,终结器是 运行 即使对象是部分构造的(例如,如果从其构造函数中抛出异常),但是当构造函数从未 运行 有吗?

背景

我有一些 C++/CLI 代码可以有效地执行以下操作(我不认为这是 C++/CLI 特定的,但这是我准备好的情况):

try {
   ClassA ^objA = FunctionThatReturnsAClassA();
   ClassB ^objB = gcnew ClassB(objA); // ClassB is written in C# in a referenced project
   ...
}
catch (...) {...}

我有一个 100% 可重复的案例,如果从 FunctionThatReturnsAClassA() 中抛出异常,然后触发 GC(似乎由再次 运行ning 这段代码可靠地触发,但等待a while also works), ClassB 的终结器被调用。

现在,通过跟踪输出我可以确认 ClassB 的构造函数不是 运行ning(这当然是您所期望的)。因此,objB 显然在满足调用其构造函数的先决条件(即从 FunctionThatReturnsAClassA() 收集结果)之前就已分配并添加到终结器列表中。

这只发生在调试器之外的优化发布版本中 运行。我可以做很多小的改变,导致终结器不是 运行ning——例如在两个语句之间插入另一个方法调用,或者(我认为很明显)将 "gcnew ClassB" 移动到returns 对象的单独函数。

在我看来,gcnew 语句的分配部分以某种方式被重新排序并且 运行 在前一个语句之前,但是这种重新排序并未反映在生成的 MSIL 代码中(打败了我最初的假设,即这只是另一个 C++/CLI 代码生成错误)。此外,比较 "buggy" 状态和任何 "fixed" 状态之间生成的 MSIL 代码显示没有意外的结构变化。

我也在调试器中查看了生成的 x86 代码,到目前为止它看起来并不奇怪,但我还没有深入分析它,无论如何我无法在调试器中重现这种行为所以我不能 100% 确定我从调试器获得的代码与显示奇怪行为的代码相同。

所以它可能是一个 MSIL->x86 代码生成怪癖,或者它可能是一个处理器指令重新排序(前者似乎更有可能,但我还没有通过更努力地尝试在内存中获取准确的代码来确认行为发生——这是我的下一步)。

问题

那么,在 .NET 中将对象的分配与对该对象的构造函数调用分开并重新排序是否有效(因为没有更好的术语)?

正如评论中所述,答案是 "Yes"——如果构造函数没有 运行 或没有完成,则终结器可以 运行。但是,如果未发生分配(这与构造函数调用无关),则终结器不能 运行。

现已确认这是一个 JIT 优化错误:https://github.com/dotnet/coreclr/issues/2478