2012-02-07 105 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

相關問題