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

时间:2012-02-07 15:02:59

标签: c# .net clr

我有一个.NET应用程序,它使用一个定义某种方法的程序集(.dll):

    public void DoSomething()
    {
        // Do work
    }

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

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

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

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

为什么这种改变会破坏代码?

5 个答案:

答案 0 :(得分:16)

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

  

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

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

假设你有方法:

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

你有一个电话

int x;
x = M1(123);

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

  • 在临时池上为x。
  • 分配空间
  • 在堆栈上按123
  • 调用M1。
  • 在堆栈上按1。 Stack现在是1,123
  • 在堆栈中添加前两个东西。这会弹出并推送结果。 Stack现在是124。
  • 返回来电者
  • 筹码仍是124。
  • 将堆栈上的值存储到x的临时存储中。这会弹出堆栈,因此堆栈现在是空的。

假设你现在这样做:

M1(345);

会发生什么? 同样的事情

  • 在堆栈上按345
  • 调用M1。
  • 在堆栈上按1。 Stack现在是1,345
  • 在堆栈中添加前两个东西。这会弹出并推送结果。 Stack现在是346。
  • 返回来电者
  • 堆栈仍然是346。

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

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

现在假设你打电话

M2(456);

会发生什么?

  • 将456推入堆栈
  • 调用M2。
  • M2做到了。当它返回调用者时,堆栈为空,因为它返回无效。
  • 现在堆栈已空,所以不要弹出任何东西。

现在你明白为什么将一个方法从void返回值返回是一个突破性的变化? 每个调用者现在必须从堆栈中弹出未使用的值。使用数据执行 nothing 仍需要将其从堆栈中清除。如果你没有弹出那个值,你就是错误的堆栈; CLR要求堆栈在每个语句的开头为空,以确保不会发生这种错位。

答案 1 :(得分:2)

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

无论该值是否被消耗都没关系 - 运行时无法找到void YourClass::DoSomething(),甚至没有尝试查找string YourClass::DoSomething()

如果可以进行此类更改,则可能会通过导致堆栈下溢/溢出而轻易炸毁运行时。

答案 2 :(得分:1)

因为您已经更改了方法签名。

当外部代码需要定位方法时,需要确保调用的内容是正确的。它在编译时将一些此类信息存储为签名 - 签名信息包括返回类型(无论它是否实际在任何地方使用)。

就CLR而言,具有void返回类型的方法不再存在 - 因此MissingMethodException

答案 3 :(得分:1)

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

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

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

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

答案 4 :(得分:0)

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