为啥将返回类型添加到 void 返回方法会导致 MissingMethodException

Posted

技术标签:

【中文标题】为啥将返回类型添加到 void 返回方法会导致 MissingMethodException【英文标题】:Why adding a return type to a void returning method causes a MissingMethodException为什么将返回类型添加到 void 返回方法会导致 MissingMethodException 【发布时间】:2012-02-07 15:02:58 【问题描述】:

我有一个 .NET 应用程序,它使用一个程序集 (.dll),该程序集定义了一些方法:

    public void DoSomething()
    
        // Do work
    

假设此方法签名更改为包含 string 返回类型:

    public string DoSomething()
    
        // Do work
        return "something";
    

为什么使用此方法的代码会在 System.MissingMethodException 上失败?

在我看来,在所有调用此方法的站点上,都没有使用返回值(因为它以前不存在)。

那么为什么这个改变会破坏代码呢?

【问题讨论】:

编辑了我的例子。我的观点是该方法现在返回了一些东西;但是以前使用它的所有代码都不会对它做任何事情。编写这样的代码是完全合法的。 【参考方案1】:

表明您已更改方法签名并因此必须重新编译调用者的其他答案是正确的。我想我可能会就你的这个问题添加一些额外的信息:

在我看来,在所有调用此方法的站点上,都没有使用返回值(因为它以前不存在)。

完全正确。现在,考虑这个问题:您如何编写不使用数据的代码?您似乎在完全错误的假设下工作,即不使用值不需要代码,但不使用值肯定需要代码!

假设你有方法:

static int M1(int y)  return y + 1; 
static void M2(int z)  ... 

你有一个电话

int x;
x = M1(123);

在 IL 级别会发生什么?以下:

在临时池上为 x 分配空间。 将 123 压入堆栈 调用 M1。 将 1 压入堆栈。堆栈现在是 1, 123 添加堆栈顶部的两个东西。这会弹出并推送结果。堆栈现在为 124。 返回调用者 堆栈仍然是 124。 将堆栈上的值存储到 x 的临时存储器中。这会弹出堆栈,因此堆栈现在是空的。

假设你现在这样做:

M1(345);

会发生什么? 同样的事情

将 345 推入堆栈 调用 M1。 将 1 压入堆栈。堆栈现在是 1, 345 添加堆栈顶部的两个东西。这会弹出并推送结果。堆栈现在是 346。 返回调用者 堆栈仍然是 346。

但是没有指令可以将值存储在任何地方,所以我们必须发出一个弹出指令:

将未使用的值从堆栈中弹出。

现在假设你打电话给

M2(456);

会发生什么?

将 456 推入堆栈 调用 M2。 M2 尽其所能。当它返回给调用者时,堆栈是空的,因为它是 void 返回。 堆栈现在是空的,所以不要弹出任何东西。

现在您明白为什么将方法从返回 void 更改为返回值是一项重大更改吗? 现在每个调用者都必须从堆栈中弹出未使用的值。对数据什么都不做仍然需要将其从堆栈中清除。如果您不弹出该值,则说明您未对齐堆栈; CLR 要求堆栈在每个语句的开头为 empty,以确保不会发生这种错位。

【讨论】:

+100 成就解锁——(Eric Lippert 回答了你的问题) 平衡的堆栈是一件好事。但有时我真的想从调用者的堆栈中复制一些值,尤其是方法句柄,以获得调用者的非常便宜的堆栈遍历。在 IL 级别,我还没有找到任何方法来做到这一点。您是否知道这种可能性,还是我必须求助于 GetStackFramesInternal? @AloisKraus IL 评估堆栈与调用堆栈不同。评估堆栈相当虚拟。有关详细信息,请参阅 Eric 的回答 ***.com/a/7877736/385844。 不错的链接。这真是个好东西。但我希望一些“文档不足”的 IL 操作码可以让我以便宜但不安全的方式到达线程堆栈。【参考方案2】:

如果不涉及反射但链接是静态的(我从描述中假设这一点),那么运行时将尝试使用确切的签名找到一个方法。这就是 CIL 的 callvirt 的工作原理。

无论是否使用该值都无关紧要 - 运行时无法找到void YourClass::DoSomething(),它甚至不会尝试查找string YourClass::DoSomething()

如果可以进行此类更改,那么您可以通过导致堆栈下溢/溢出来轻松炸毁运行时。

【讨论】:

+1 更不用说你不希望它猜测并且可能选择错误的方法 - 这可能会很快变得令人讨厌...... 我认为选择方法只是通过查看它的名称+参数来完成。 @liortal 对于 C#,方法签名(用于处理方法重载)不包括返回类型。在 CLR 中,方法签名确实包含返回类型。大概这意味着可以编写一种允许在同一类中重载int M()string M() 的语言。 C# 不是那种语言。有关详细信息,请参阅 Eric Lippert 关于该主题的帖子:***.com/a/8521944/385844【参考方案3】:

因为你做了一个破坏性的 api 更改。您确实更改了基类或接口中的方法签名。

调用者与此方法相关联。在 IL 中,方法引用不仅是对类型及其方法的引用,并且对方法有一些索引,而且调用者方法引用确实包含完整的方法签名。

因此,此更改可以通过重新编译调用此方法的所有程序集来修复,但是当您仅重新编译更改的程序集并希望使用的程序集能够神奇地获取更改的方法签名时,您会遇到运行时异常。情况并非如此,因为方法引用确实包含完整的方法签名和定义类型。

它也确实发生在我身上。您是对的,没有人可以使用返回类型,因此此更改是安全的,但您需要重新编译所有受影响的目标。

【讨论】:

谢谢。我的印象是只有方法名+参数类型用于推断调用哪个方法。 实际上是 typeref + methodref,其中方法引用确实包含名称、返回类型、类型参数、泛型参数和调用约定 @liortal:C# 不使用返回类型来确定调用哪个方法。但是 C# 肯定需要知道返回类型才能在方法的调用者中生成正确的代码【参考方案4】:

因为您更改了方法签名。

当外部代码需要定位一个方法时,它需要确保它调用的是正确的。它在编译时将其中一些信息作为签名存储——签名信息包括返回类型(不管它是否实际在任何地方使用)。

就 CLR 而言,返回类型为 void 的方法已不复存在 - 因此为 MissingMethodException

【讨论】:

有趣。我想知道它的 MSIL 是什么样的(之前和之后)。【参考方案5】:

C# 的制造者决定对方法签名应该严格。

【讨论】:

从异常 (MissingMethodException) 我相信他描述的是由于用另一个替换 dll 而导致的运行时错误。如果是这样,那么它与 C# 无关。

以上是关于为啥将返回类型添加到 void 返回方法会导致 MissingMethodException的主要内容,如果未能解决你的问题,请参考以下文章

PHP 7.1 - 为啥没有关于 void 返回值的警告?

当一个函数无返回值时,函数的类型应定义为啥

C# 异步编程

可以/为啥在返回类型中使用 char * 而不是 const char * 会导致崩溃?

为啥我无法在具有 void 返回类型的异步函数中捕获异常?

为啥当objective-c 有冲突返回类型时会发生这种情况?