带有 NSubstitute 的 LINQPad 中的奇怪行为

Strange behaviour in LINQPad with NSubstitute

我在 LINQPad 4 (v.4.57.02) 中编写了以下示例来演示尝试使用 NSubstitute (v1.9.2.0) 来模拟一个非虚拟类型的愚蠢行为 属性 :

void Main()
{
    var foo = Substitute.For<Foo>();
    foo.Alarm.Returns(2);    
    foo.Alarm.Dump();
}

public class Foo
{
    public Foo()
    {
        Console.WriteLine("Foo ctor called.");
    }

    public virtual int Alarm
    {
        get; set;
    }
}

此代码按预期工作并给出以下输出:

Foo ctor called.
2

现在,当我编辑代码以删除 Alarm 属性 上的 virtual 修饰符时,我希望看到一个 NSubstitute.Exceptions.CouldNotSetReturnDueToNoLastCallException 异常,其中包含智慧:

If you substituted for a class rather than an interface, check that the call to your substitute was on a virtual/abstract member. Return values cannot be configured for non-virtual/non-abstract members.

然而,第一次我运行修改后的代码我得到:

Foo ctor called.
0

在随后的 运行 中,我得到了预期的异常。

现在我怀疑 LINQPad 管理 AppDomain 的方式以及 NSubstitute 的 Castle 代理的工作方式有一些有趣的事情发生 - 但我不知道是什么。举起手来,我只是没有时间深入研究这个问题,想知道是否还有其他人有明确的解释,因为了解 LINQPad 执行环境中的陷阱会让人感到欣慰。

如果您开始打开 ​​LINQPad 的新实例并且 运行 没有 virtual 成员的代码,它将立即失败并出现预期的错误。

所以这是我对正在发生的事情的猜测。第一次代码是 运行 并且 virtual 成员 NSubstitute 的状态如下所示:

var foo = Substitute.For<Foo>();
foo.Alarm         // 1. last call is foo.Alarm
   .Returns(2);   // 2. make foo.Alarm return `2`. Clear last call.
foo.Alarm         // 3. last call is foo.Alarm
   .Dump();       // 4. extension method -- doesn't clear last call

NSubstitute 静态存储对替代品的最后一次调用,因此它会一直挂起直到应用程序域消失。当您修改代码以再次删除 virtual 和 运行 时,步骤 2 中的 .Returns(2) 会找到上一个 运行 的步骤 3 中的最后一次调用,将其存根相应地,然后清除最后一次调用。由于非虚拟成员,没有进一步的调用被记录,因此后续 运行s 失败并出现预期错误。