javascript閉包 閉包如何在幕後工作?(C#)




javascript閉包 (4)

我覺得我對閉包有很好的理解,如何使用它們,以及它們什麼時候有用。 但我不明白的是他們實際上是如何在幕後的幕後工作的。 一些示例代碼:

public Action Counter()
{
    int count = 0;
    Action counter = () =>
    {
        count++;
    };

    return counter;
}

通常,如果閉包沒有捕獲{count},它的生命週期將被限定為Counter()方法,並且在它完成之後它將消除Counter()的其餘堆棧分配。 什麼時候關閉會發生什麼? 這個Counter()調用的整個堆棧分配是否存在? 它會將{count}複製到堆中嗎? 它是否從未真正在堆棧上分配,但被編譯器識別為關閉,因此總是存在於堆上?

對於這個特殊的問題,我主要關注它在C#中是如何工作的,但是不反對與支持閉包的其他語言進行比較。


Eric Lippert的回答確實很明顯。 然而,建立一個堆棧幀和捕獲如何工作的圖片會很好。 要做到這一點,有助於查看稍微複雜的示例。

這是捕獲代碼:

public class Scorekeeper { 
   int swish = 7; 

   public Action Counter(int start)
   {
      int count = 0;
      Action counter = () => { count += start + swish; }
      return counter;
   }
}

這就是我認為相同的東西(如果我們幸運的話,Eric Lippert會評論這是否真的正確):

private class Locals
{
  public Locals( Scorekeeper sk, int st)
  { 
      this.scorekeeper = sk;
      this.start = st;
  } 

  private Scorekeeper scorekeeper;
  private int start;

  public int count;

  public void Anonymous()
  {
    this.count += start + scorekeeper.swish;
  }
}

public class Scorekeeper {
    int swish = 7;

    public Action Counter(int start)
    {
      Locals locals = new Locals(this, start);
      locals.count = 0;
      Action counter = new Action(locals.Anonymous);
      return counter;
    }
}

關鍵是本地類替換整個堆棧幀,並在每次調用Counter方法時進行相應的初始化。 通常,堆棧幀包括對“this”的引用,加上方法參數以及局部變量。 (進入控制塊時,堆棧框架也會有效擴展。)

因此,我們沒有一個對應於捕獲的上下文的對象,而是每個捕獲的堆棧幀實際上有一個對象。

基於此,我們可以使用以下心智模型:堆棧幀保留在堆上(而不是堆棧上),而堆棧本身只包含指向堆上堆棧幀的指針。 Lambda方法包含指向堆棧幀的指針。 這是使用託管內存完成的,因此框架會粘在堆上,直到不再需要它為止。

顯然,編譯器可以通過僅在需要堆對象來支持lambda閉包時使用堆來實現它。

我喜歡這個模型的是它提供了“收益率回報”的綜合圖片。 我們可以想到一個迭代器方法(使用yield return),好像它的堆棧幀是在堆上創建的,而引用指針存儲在調用者的局部變量中,以便在迭代期間使用。


謝謝@HenkHolterman。 由於已經由Eric解釋過,我添加了鏈接只是為了顯示編譯器為閉包生成的實際類。 我想補充一點,C#編譯器創建顯示類可能會導致內存洩漏。 例如,在函數內部有一個由lambda表達式捕獲的int變量,另一個局部變量只保存對大字節數組的引用。 編譯器將創建一個顯示類實例,該實例將保存對變量(即int和字節數組)的引用。 但是在引用lambda之前,字節數組不會被垃圾收集。


編譯器 (與運行時相對)創建另一個類/類型。 您的閉包函數以及您關閉/提升/捕獲的任何變量將作為該類的成員在您的代碼中重寫。 .Net中的閉包實現為此隱藏類的一個實例。

這意味著你的count變量完全是一個不同類的成員,並且該類的生命週期與任何其他clr對像一樣。 在它不再生根之前,它不符合垃圾收集的條件。 這意味著只要你有一個可調用的方法引用它就不會去任何地方。


你的第三個猜測是正確的。 編譯器將生成如下代碼:

private class Locals
{
  public int count;
  public void Anonymous()
  {
    this.count++;
  }
}

public Action Counter()
{
  Locals locals = new Locals();
  locals.count = 0;
  Action counter = new Action(locals.Anonymous);
  return counter;
}

合理?

另外,您要求進行比較。 VB和JScript都以完全相同的方式創建閉包。







closures