2015-11-24 33 views
15

我正在調查一些奇怪的對象的生命週期的問題,工作在C#編譯器的這個非常令人費解的行爲傳來:這個閉包組合行爲是一個C#編譯器錯誤嗎?

請看下面的測試類:

class Test 
{ 
    delegate Stream CreateStream(); 

    CreateStream TestMethod(IEnumerable<string> data) 
    { 
     string file = "dummy.txt"; 
     var hashSet = new HashSet<string>(); 

     var count = data.Count(s => hashSet.Add(s)); 

     CreateStream createStream =() => File.OpenRead(file); 

     return createStream; 
    } 
} 

編譯器生成以下內容:

internal class Test 
{ 
    public Test() 
    { 
    base..ctor(); 
    } 

    private Test.CreateStream TestMethod(IEnumerable<string> data) 
    { 
    Test.<>c__DisplayClass1_0 cDisplayClass10 = new Test.<>c__DisplayClass1_0(); 
    cDisplayClass10.file = "dummy.txt"; 
    cDisplayClass10.hashSet = new HashSet<string>(); 
    Enumerable.Count<string>(data, new Func<string, bool>((object) cDisplayClass10, __methodptr(<TestMethod>b__0))); 
    return new Test.CreateStream((object) cDisplayClass10, __methodptr(<TestMethod>b__1)); 
    } 

    private delegate Stream CreateStream(); 

    [CompilerGenerated] 
    private sealed class <>c__DisplayClass1_0 
    { 
    public HashSet<string> hashSet; 
    public string file; 

    public <>c__DisplayClass1_0() 
    { 
     base..ctor(); 
    } 

    internal bool <TestMethod>b__0(string s) 
    { 
     return this.hashSet.Add(s); 
    } 

    internal Stream <TestMethod>b__1() 
    { 
     return (Stream) File.OpenRead(this.file); 
    } 
    } 
} 

原始類包含兩個lambda:s => hashSet.Add(s)() => File.OpenRead(file)。第一個關閉局部變量hashSet,第二個關閉局部變量file。但是,編譯器會生成一個包含hashSetfile的封閉實現類<>c__DisplayClass1_0。因此,返回的CreateStream委託包含並保持對hashSet對象的引用,該對象在返回時應該可用於GC一次TestMethod

在我遇到此問題的實際場景中,一個非常實際的(即> 100mb)對象被錯誤地包含在內。

我的具體問題是:

  1. 這是一個錯誤?如果不是,爲什麼這種行爲被認爲是可取的?

更新:

的C#5規格7.15.5.1說:

當外變量由匿名函數引用,則 外變量到所述已被匿名的 功能捕獲。通常,局部變量的生命週期限於 執行與其關聯的塊或語句 (第5.1.7節)。但是,捕獲的外部變量的生存期至少延長至 ,直到從 創建的委託或表達式樹變爲符合垃圾回收條件。

這似乎對某種程度的解釋是開放的,並沒有明確禁止lambda捕獲它沒有引用的變量。但是,this question涵蓋了一個相關的場景,@ eric-lippert被認爲是一個錯誤。恕我直言,我看到由編譯器提供的組合閉包實現作爲一種很好的優化,但是優化不應該用於編譯器可以合理檢測的lambda,可能會超出當前棧幀的壽命。


  • 如何針對這一點,我的代碼,而放棄使用lambda表達式一起?值得注意的是,我如何在防守方面進行編碼,以便將來的代碼更改不會在同一方法中突然導致其他一些未更改的lambda開始包含它不應該包含的內容?

  • 更新:

    我提供的代碼示例是必然做作。顯然,將lambda創建重構爲單獨的方法可解決該問題。我的問題不是關於設計最佳實踐(也包括@ peter-duniho)。相反,根據TestMethod的內容,我想知道是否有辦法強制編譯器從組合閉包實現中排除createStream lambda。


    爲了記錄在案,我與VS .NET目標4.6 2015年

    +0

    他們共享相同的詞彙範圍。也許正是因爲如此。 –

    +1

    [離散匿名方法共享一個類?]的可能重複(http://stackoverflow.com/questions/3885106/discrete-anonymous-methods-sharing-a-class)。作爲一個額外的好處,這個例子非常簡單,但是*不是*做的。 – Brian

    +0

    這是「隱性封閉」的原因嗎?我想我明白現在警告好多了。我總是想知道爲什麼在某些情況下,拉姆達捕獲的東西與它無關。 –

    回答

    12

    這是一個錯誤?

    不。編譯器符合此處的規範。

    爲什麼這種行爲被認爲是可取的?

    這是不可取的。這是深深遺憾,因爲你發現了這裏,當我在2007年描述回:

    http://blogs.msdn.com/b/ericlippert/archive/2007/06/06/fyi-c-and-vb-closures-are-per-scope.aspx

    C#編譯器團隊一直認爲,因爲C#3.0中的每一個版本解決這個和它從來沒有足夠高的優先級。考慮在Roslyn github網站上輸入一個問題(如果沒有的話,可能會有)。

    我個人希望看到這個固定的;就目前來看,這是一個很大的「陷阱」。

    如何在不放棄使用lambda全部的情況下對此進行編碼?

    變量是被捕獲的東西。您可以在完成後將哈希集變量設置爲null。然後,唯一消耗的內存是變量的內存,四個字節,而不是它所指向的內存,這些內存將被收集。

    6

    我不知道在C#語言規範什麼,將決定完全是一個編譯器是如何實現匿名方法和可變捕獲。這是一個實現細節。

    該規範的作用是設置一些規則來規定匿名方法及其捕獲變量的行爲。我沒有C#6規格的副本,但這裏是從C#5規範相關的文字,「7.15.5.1捕獲的外層變量」下:

    &hellip;被捕獲的外層變量的壽命至少延長從匿名函數創建的委託或表達式樹有資格進行垃圾回收。 [重點礦]

    規範中沒有任何內容限制變量的生命週期。只需要編譯器確保變量的存在時間足夠長,以便在匿名方法需要時保持有效。

    So&hellip;

    1.這是一個錯誤?如果不是,爲什麼這種行爲被認爲是可取的?

    不是一個錯誤。編譯器符合規範。

    至於它是否被認爲是「可取的」,那是一個加載的術語。什麼是「可取的」取決於您的優先事項。也就是說,編譯器作者的一個優先事項是簡化編譯器的任務(並且這樣做可以使其運行速度更快並減少錯誤發生的機率)。在這種情況下,該特定實施方式可能被認爲是「合意的」。另一方面,語言設計者和編譯器作者都有共同的目標,即幫助程序員生成工作代碼。由於實現細節可能會干擾這一點,因此這種實現細節可能被認爲是「不合需要的」。最終,根據他們潛在的競爭目標,每個優先級如何排名都是一個問題。

    2.如何在不放棄使用lambda表達式的情況下對此進行編碼?值得注意的是,我如何在防守方面進行編碼,以便將來的代碼更改不會在同一方法中突然導致其他一些未更改的lambda開始包含它不應該包含的內容?

    很難說沒有一個設計不那麼簡單的例子。一般來說,我會說明顯的答案是「不要混合你的lambda」。在你特定的(被公認爲是人爲的)例子中,你有一種方法似乎是在做兩個完全不同的事情。由於各種原因,這通常是不被接受的,在我看來,這個例子只是增加了這個列表。

    我不知道修復「兩件不同的事情」的最佳方法是什麼,但一個明顯的選擇是至少重構該方法,以便「兩種不同的東西」方法將工作委託給另外兩種方法,每種方法都是描述性的(它具有幫助代碼自我記錄的好處)。

    例如:

    CreateStream TestMethod(IEnumerable<string> data) 
    { 
        string file = "dummy.txt"; 
        var hashSet = new HashSet<string>(); 
    
        var count = AddAndCountNewItems(data, hashSet); 
    
        CreateStream createStream = GetCreateStreamCallback(file); 
    
        return createStream; 
    } 
    
    int AddAndCountNewItems(IEnumerable<string> data, HashSet<string> hashSet) 
    { 
        return data.Count(s => hashSet.Add(s)); 
    } 
    
    CreateStream GetCreateStreamCallback(string file) 
    { 
        return() => File.OpenRead(file); 
    } 
    

    以這種方式,所捕獲的變量保持獨立。即使編譯器出於某種奇怪的原因仍然將它們都放入相同的閉包類型中,它仍然不應該導致在兩個閉包之間使用的那種類型的相同實例

    你的TestMethod()仍然有兩個不同的東西,但至少它本身不包含那兩個不相關的實現。代碼更具可讀性和更好的劃分性,這是一件好事,即使它修復了變量生存期問題。

    +0

    關於C#規範7.15.5.1,第一段開始_「當一個外部變量被匿名函數引用時,外部變量被稱爲被匿名函數」_「捕獲。然而,lambda'()=> File.OpenRead(file)'不引用外部變量'hashSet',所以'hashSet'的生命週期不應該延長到這個lambda的生命週期。關於這兩件不同的事情 - 正如你注意到的那樣,這確實是一個人爲的例子。這個問題似乎會影響任何使用捕獲lambda的方法做一些工作並創建一個長壽命的捕獲lambda。 – tg73

    +0

    @ tg73:_「所以hashSet的生命週期不應該延長這個lambda的生命週期」_ - 恕我直言,你沒有仔細閱讀規範。 _other_ lambda表達式捕獲'hashSet'變量_is_,並且規範中沒有任何內容對這些捕獲變量的生命週期放置_upper_限制。如果編譯器想要,它可以通過將變量設置爲「靜態」變量並_never_丟棄它來實現捕獲。雖然我瞭解這種行爲不便於您的使用,但完全符合規範要求。 –

    +0

    @ tg73:_「這個問題似乎影響任何創建長時間捕獲lambda的方法」 - 但只有在每個捕獲不同局部變量的方法中有兩個不相關的匿名方法時。方法應該簡單;一個足夠大的方法包含兩個獨立的具有不相關變量生命週期的邏輯,無論如何都是由於重構。恕我直言,它應該很容易解決這個問題在任何情況下,通過打破方法成小塊。我無法評論我沒見過的例子,但不能輕易做到這一點是不尋常的。 –

    相關問題