2012-02-07 84 views
8

我有一个使用的组件(.DLL).NET应用程序定义一些方法:为什么添加返回类型的空隙返回方法导致MissingMethodException

public void DoSomething() 
    { 
     // Do work 
    } 

假设这种方法签名更改成包括返回类型:

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

为什么使用这个方法的代码在system.missingMethodException而失败?

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

为什么此更改会破坏代码呢?

+0

编辑我的例子。我的观点是,该方法现在返回一些东西;但以前使用它的所有代码都不会对它做任何事情。编写这样的代码是完全合法的。 – 2012-02-07 15:08:01

回答

1

因为你做了一个突破性的API更改。您没有更改基类或接口中的方法签名。

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

因此,此更改可以通过重新编译调用此方法的所有程序集来解决,但当您仅重新编译更改后的程序集并希望使用程序集将奇迹般地选取更改的方法签名时,您会得到运行时异常。情况并非如此,因为方法引用确实包含完整的方法签名和定义类型。

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

+0

谢谢。我的印象是,只有方法名称+参数类型被用于推断调用哪种方法。 – 2012-02-07 15:19:18

+1

它实际上是typeref + methodref其中方法引用包含名称,返回类型,类型arguemtns,泛型参数和调用约定 – 2012-02-07 15:26:27

+0

@liortal:返回类型不被C#用于确定要调用的方法。但是C#肯定需要知道返回类型*以在方法*的调用者中生成正确的代码。 – 2012-02-07 16:14:21

0

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

+1

从异常('MissingMethodException')我相信他描述了由另一个dll替换dll导致的运行时错误。如果是这样,那么它与C#无关。 – 2012-02-07 15:09:26

2

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

无论数值是否被消耗都无关紧要 - 运行时无法找到void YourClass::DoSomething(),它甚至不会尝试寻找string YourClass::DoSomething()

如果这种改变是可能的,那么你可以通过引起堆栈下溢/溢出来轻松地破坏运行时。

+0

+1更不用说,你不会希望它猜测,也许选择错误的方法 - 这可能会得到讨厌的快速... – Basic 2012-02-07 15:09:35

+0

我认为,只有通过查看其名称+参数来完成选择方法。 – 2012-02-07 15:12:13

+2

你以为错了! – adelphus 2012-02-07 15:13:18

1

因为您更改了方法签名。

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

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

+0

有趣。我想知道它的MSIL是什么样子(之前和之后)。 – 2012-02-07 15:10:01

14

其他答案说明你已经改变了方法的签名,因此必须重新编译调用者,这是正确的。我想我可能会添加一些关于此问题的其他信息:

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

这是完全正确的。现在,考虑这个问题:你如何编写不使用数据的代码?你似乎在假设没有使用值不需要代码,但没有使用值肯定需要代码!

假设你有方法:

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); 

会发生什么? 同样的事情

  • 堆栈
  • 调用M1上推345。
  • 将1推入堆栈。堆栈现在是1,345
  • 在堆栈中添加前两项。这会弹出并推送结果。堆栈现在是346
  • 返回给调用者
  • 协议栈仍在346

但没有指令存储在堆栈上任何地方的价值,所以我们要发出一个弹出指令:

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

现在假设你叫

M2(456); 

会发生什么?

  • 堆栈
  • 调用M2上推456。
  • M2做它的事情。当它返回给调用者时,堆栈是空的,因为它无效返回。
  • 堆栈现在是空的,所以不要弹出任何东西。

现在你明白为什么从void返回值返回的方法是一个突破性的改变? 每个呼叫者现在必须将未使用的值从堆栈中弹出。做没有与数据仍然需要清理它从堆栈。如果您没有将该值取消,您将错位堆栈; CLR要求在每个语句开始时堆栈为为空以确保不会发生这种错位。

+1

+100成就解锁 - (Eric Lippert回答了你的问题) – 2012-02-07 16:00:52

+0

平衡的堆叠是一件好事。但有时我真的想从我的调用者的堆栈中复制一些值,特别是方法句柄,以获得我的调用者的非常便宜的栈走。在IL级别,我还没有找到任何方法来做到这一点。您是否意识到这种可能性,还是必须诉诸GetStackFramesInternal? – 2012-02-07 21:50:55

+0

@AloisKraus IL评估堆栈与调用堆栈不同。评估栈更虚拟。有关更多详细信息,请参阅Eric的回答http://stackoverflow.com/a/7877736/385844。 – phoog 2012-02-08 18:46:54

相关问题