2010-11-21 193 views
39

我試圖創建一個函數,它可以創建一個Action來增加傳入的任何整數。但是,我的第一次嘗試給了我一個錯誤「不能使用ref或out參數在匿名方法體內「。C#不能在一個匿名方法體內使用ref或out參數

public static class IntEx { 
    public static Action CreateIncrementer(ref int reference) { 
     return() => { 
      reference += 1; 
     }; 
    } 
} 

我明白爲什麼編譯器不喜歡這樣,但儘管如此,我想有一個優雅的方式來提供一個很好的增量工廠,可以指向任何整數。我看到要做到這一點的唯一方法是類似於以下內容:

public static class IntEx { 
    public static Action CreateIncrementer(Func<int> getter, Action<int> setter) { 
     return() => setter(getter() + 1); 
    } 
} 

但當然,這是更多的來電者使用的痛苦;要求調用者創建兩個lambda表達式,而不是僅傳入一個引用。有沒有更優雅的方式提供這種功能,或者我只需要使用兩個lambda選項?

+2

這是一個簡單的例子嗎?爲什麼不使用x ++?爲什麼這個類的另一個類增加狀態 – Gishu 2010-11-21 02:47:04

+2

@Gishu是的,這是一個簡化的例子;較大的用例很難解釋,但這一切都歸結爲創建一個可以對值類型執行操作的Action工廠。 – 2010-11-21 02:55:13

回答

24

這是不可能的。

編譯器會將匿名方法使用的所有局部變量和參數轉換爲自動生成的閉包類中的字段。

CLR不允許ref類型存儲在字段中。

例如,如果您在本地變量中傳遞值類型,例如ref參數,則該值的生存期將超出其堆棧幀。

30

好吧,我發現,它實際上是可能的指針,如果在不安全的情況下:

public static class IntEx { 
    unsafe public static Action CreateIncrementer(int* reference) { 
     return() => { 
      *reference += 1; 
     }; 
    } 
} 

然而,垃圾收集器可以用這個移動垃圾收集過程中的參考肆虐,作爲以下指示:

class Program { 
    static void Main() { 
     new Program().Run(); 
     Console.ReadLine(); 
    } 

    int _i = 0; 
    public unsafe void Run() { 
     Action incr; 
     fixed (int* p_i = &_i) { 
      incr = IntEx.CreateIncrementer(p_i); 
     } 
     incr(); 
     Console.WriteLine(_i); // Yay, incremented to 1! 
     GC.Collect(); 
     incr(); 
     Console.WriteLine(_i); // Uh-oh, still 1! 
    } 
} 

可以通過將變量固定到內存中的特定點來解決此問題。這可以通過添加下面的構造函數來完成:

public Program() { 
     GCHandle.Alloc(_i, GCHandleType.Pinned); 
    } 

這令來自各地的移動對象的垃圾收集器,所以這正是我們要尋找的。然而,你必須添加一個析構函數來釋放這個pin,並且在整個對象的整個生命週期中它會分割內存。並不是那麼容易。這在C++中會更有意義,C++中的東西不會四處移動,資源管理就是當然,但在C#中並不那麼重要,因爲所有這些都應該是自動的。

所以看起來像故事的寓意是,只需將該成員的int包裝在引用類型中並完成它即可。

(是的,這是我問它之前的工作方式,但只是想弄清楚是否有辦法擺脫我所有的成員變量,並使用常規整數。)

+4

針對使用不安全上下文的託管模式的有趣解決方法+1。 – 2010-11-21 06:35:48

+0

不需要GCHandle.Alloc - 只需擴展固定語句大括號即可。 – 2012-05-21 20:04:20

+0

@TA先生這只是一個例子,表明GC可能會搞砸了。任何有用的'incr'函數都可能會從創建它的方法中返回,或者將它作爲成員變量添加到持久對象中,這顯然會留下「固定」範圍。 – 2012-05-21 21:04:43

2

它可能是運行時的一個有用功能,允許創建帶有機制的變量引用以防止其持久性;這樣的特徵將允許索引器的行爲像一個數組(例如,所以Dictionary> < Int32,Point>可以通過「myDictionary [5] .X = 9;」)來訪問)。我認爲如果這樣的引用不能被向下轉換爲其他類型的對象,也不能被用作字段,也不能被引用自己傳遞(因爲任何地方可以存儲這樣的引用可能會在引用之前超出範圍本身會)。不幸的是,CLR不提供這樣的功能。

要實現你要做的事情,需要在閉包中使用引用參數的任何函數的調用者調用者必須在閉包內包裝它想要傳遞給這樣一個函數的任何變量。如果有一個特殊的聲明表示將以這種方式使用參數,那麼編譯器可能會實現所需的行爲。也許在.net 5.0編譯器中,但我不確定這將是多麼有用。

順便說一句,我的理解是Java中的閉包使用了按值的語義,而.net中的閉包是通過引用。我可以理解一些偶然使用的引用語義,但默認使用引用似乎是一個可疑的決定,類似於使用默認的by-reference參數 - 通過VB6傳遞VB版本的語義。如果想在創建一個委託來調用一個函數時捕獲一個變量的值(例如,如果希望委託在創建委託時使用X的值調用MyFunction(X)),那麼最好使用lambda有額外的臨時工,或者僅僅使用委託工廠而不用Lambda表達式會更好。

+1

是的,Eric Lippert寫了一篇關於這個的博客。 http://blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx。實際上我的代碼依賴於這種行爲。爲OpenGL循環保留一個framecount而不需要一個framecount成員變量。我更喜歡C++規範,允許使用語義。 – 2010-11-21 06:21:02

+1

@Dax:有些情況下必須傳遞引用語義。然而,參數傳遞也是如此。這並不意味着通過引用應該是默認的。順便說一句,我不知道利用單元素數組替換引用閉包變量會有什麼優點和缺點,允許爲需要不同閉包變量組合的匿名方法創建不同的類(避免一些GC問題)。 – supercat 2010-11-21 17:19:19

相關問題