C#と閉パッケージ

11649 ワード

テキストリンク:http://kb.cnblogs.com/page/111231/
 
まず説明したいのは、このような悪い心理状態(例えば中国語の技術書)があるが、全体的には、国内の技術者は他人を分かち合い、教えるのが好きで、これは私の個人的な感じと前に庭で見た友达の感じとは正反対だ.個人は実は国内の多くの技術のネットユーザーはすべてとても熱心で、言語の問題の同じ技術のホットスポットのため少し国外に遅れているかもしれませんが、いくつかの成熟したあるいは基礎の概念はすべてとても細かい中国語の紹介を見つけることができて、特に閉鎖についてです.その字面解釈は確かに迂回しているので、この名詞を解釈しようとする学生はできるだけ自分が最も分かりやすいと思っている方法で説明しています.おしゃべりが遠くなったので、ここではC#言語で閉包を説明しましょう.
実際に閉パッケージといえば、変数の役割ドメインと変数のライフサイクルを先に挙げなければなりません.
C#の中で、変数の作用域は3種類あって、1つはクラスに属して、私達はよくfieldと言います;2つ目は関数に属し、一般的に局所変数と呼ばれています.もう1つは、実は関数に属していますが、その作用範囲はもっと小さく、関数の局所的なコードフラグメントに属しています.これは同様に局所変数と呼ばれています.この3つの変数のライフサイクルは、基本的には、各変数が格納されたオブジェクトに属し、すなわち、変数が格納されたオブジェクトに従って生成され、消滅することを一言で説明することができます.3つの役割ドメインに対応して、クラス内の変数はクラスのインスタンス化に伴って生成され、クラスオブジェクトのリソース回収に伴って消滅する(もちろん、ここでは非インスタンス化staticおよびconstオブジェクトは含まれない).関数(またはコードフラグメント)の変数も、関数(またはコードフラグメント)呼び出しの開始に伴って生成され、関数(またはコードフラグメント)呼び出しの終了に伴ってGCによって自動的に解放され、内部変数のライフサイクルは先進的な特性を満たす.
では、ここには例外はありますか?
答えはありますが、この点を言う前に、もう一つの名詞をあげなければなりません.C#はMSバージョンのJavaだと言われていますが、これは.NET 1.0ではそう言えるかもしれませんが、2.0以降はC#がjavaではないことを誇りに思っています.この依頼には大きな功績があります.JavaとC#を使ったことがある人やWinFormプログラムを書いてみたときにすべて手書きでコードを実装した人は、同じclickイベントではJavaで匿名クラスを無端にセットしなければならないが、c#では匿名依頼を書いたオブジェクトタイプを表示することなく、関数名+=をイベントの後に直接表示することができると感じている.コンパイラがこの仕事を手伝ってくれるので、3.0以降のバージョンでは、マイクロソフトが依頼した使い方がより洗練されています.簡潔なLamdaでも分かりやすいLINQでも、依頼に由来しています.
依頼は私たちが今日話す閉鎖と何の関係があるのかと聞くかもしれません.
C#,Java,JavaScript,Ruby,Pythonという言語は異なり,C#とJavaの世界では原子オブジェクトがクラス(もちろんstructと基本変数もある)であり,多くの動的言語の関数ではなく,クラスをインスタンス化し,変数をインスタンス化することができるが,直接newの関数を直接newすることはできないことを知っている.すなわち,表面的にはjsのように関数をインスタンス化して伝達することはできない.これもJava 7がパッケージを閉じるまでJava特性に遅れて加わった理由です.しかしC#にとってこれらは表象にすぎず、私がC#を学んだばかりの頃、最も多くの解釈依頼を見たのは、依頼ですね.C++の関数ポインタに相当します.この言葉は漠然としているが,確かに一定の道理があり,依頼,特に匿名依頼という層のオブジェクトのパッケージを通じて,関数をオブジェクトとして伝達できない制限を突破することができる.
ここではやはり閉鎖と依頼の関係について話していないようですが、いいでしょう.私はうるさいので、概念から話します.
閉パッケージとは、実際に使用される変数がその役割ドメインから離れているが、役割ドメインとコンテキスト関係があるため、現在の環境で上記の環境で定義された関数オブジェクトを引き続き使用することができる.
拗ねて、プログラマー、やはり例で説明したほうが理解しやすいです.
まず、最も簡単なJavaScriptでよく見られる閉パッケージの例を示します.
function f1(){
  var n = 999;
  return function(){
      alert(n); // 999
        return n;
  }
}
var a = f1();
alert(a());

このコードをC#コードに翻訳すると、次のようになります.
public class TCloser
{
    public Func<int> T1()
    {
        var n = 999;
        return () =>
        {
            Console.WriteLine(n);
            return n;
        };
    }
}

class Program
{
    static void Main()
    {
        var a = new TCloser();
        var b = a.T1();
        Console.WriteLine(b());
    }
}

上記のコードから,変数nは実際には関数T 1に属する局所変数であり,本来のライフサイクルは関数T 1の呼び出し終了に伴って解放されるべきであるが,ここでは戻りの依頼bで呼び出すことができ,ここでは閉パケットが示す威力である.T 1が返す匿名依頼を呼び出すコードセグメントにnを用いたため,コンパイラから見ればこれらはすべて合法的である.返す依頼bと関数T 1にはコンテキスト関係がある,すなわち匿名依頼bはその存在する関数やクラス内の局所変数の使用を許可するため,コンパイラは一連の動作(具体的な動作は後述する)を行う.bで呼び出された関数T 1の局所変数を自動的に閉じ、その局所変数が新たな作用範囲を満たすようにする.
したがって、.NETの閉パッケージを見るとjsのように理解できます.返される匿名関数オブジェクトは関数T 1で生成されるため、T 1に属する属性に相当します.T 1のオブジェクトレベルを1つ上に上げるとよく理解できます.ここでは、T 1はクラスであり、返される匿名オブジェクトはT 1の属性に相当します.属性については、T 1が格納したオブジェクトT 1の他の属性または方法を呼び出し、T 1が格納したオブジェクトTCloser内部の他の属性を包むことができます.この匿名関数が他のオブジェクトに呼び出されると、コンパイラは、匿名関数によって使用されるメソッドT 1のローカル変数のライフサイクルを自動的に引き上げ、匿名関数のライフサイクルと同じように閉じるという.
この返される委任に含まれる変数nは、コンパイラが何らかの方法でこの委任オブジェクトの同じオブジェクトに対する付与値を隠すだけだと言えるかもしれません.では、次の2つの方法を比較します.
public class TCloser
{
    public Func<int> T1()
    {
        var n = 999;
        Func<int> result = () =>
        {
            return n;
        };
        n = 10;
        return result;
    }

    public dynamic T2()
    {
        var n = 999;
        dynamic result = new { A = n };
        n = 10;
        return result;
    }
    static void Main()
    {
        var a = new TCloser();
        var b = a.T1();
        var c = a.T2();
        Console.WriteLine(b());
        Console.WriteLine(c.A);
    }
} 

最終出力結果は何ですか?答えは10と999で,閉パケットの特性上,ここで匿名関数で用いられる変数が実際のT 1の変数であるのに対し,匿名オブジェクトresultのAは初期化時に変数nの値が与えられただけで,nではないので,後のnが変化してもAは変化しない.これこそ閉包の魔力だ.   
NET自体が関数オブジェクトをサポートしていないことに気づくかもしれませんが、このような特性はどこから来たのでしょうか.答えはコンパイラで、ILコードを見るとわかります.
まずC#コードを示します.
public class TCloser
{
    public Func<int> T1()
    {
        var n = 10;
        return () =>
        {
            return n;
        };
    }

    public Func<int> T4()
    {
        return () =>
        {
            var n = 10;
            return n;
        };
    }
}

この2つの返される匿名関数の唯一の違いは,返される依頼で変数nの役割ドメインが異なることであり,T 1では変数nがT 1に属し,T 4ではnが匿名関数自体に属する.しかし、ILコードを見ると、この中の大きな違いがわかります.
.method public hidebysig instance class [mscorlib]System.Func`1 T1() cil managed{
    .maxstack 3
    .locals init (
        [0] class ConsoleApplication1.TCloser/<>c__DisplayClass1 CS$<>8__locals2,
        [1] class [mscorlib]System.Func`1 CS$1$0000)
    L_0000: newobj instance void ConsoleApplication1.TCloser/<>c__DisplayClass1::.ctor()
    L_0005: stloc.0
    L_0006: nop
    L_0007: ldloc.0
    L_0008: ldc.i4.s 10
    L_000a: stfld int32 ConsoleApplication1.TCloser/<>c__DisplayClass1::n
    L_000f: ldloc.0
    L_0010: ldftn instance int32 ConsoleApplication1.TCloser/<>c__DisplayClass1::b__0()
    L_0016: newobj instance void [mscorlib]System.Func`1::.ctor(object, native int)
    L_001b: stloc.1
    L_001c: br.s L_001e
    L_001e: ldloc.1
    L_001f: ret
}
 
.method public hidebysig instance class [mscorlib]System.Func`1 T4() cil managed
{
    .maxstack 3
    .locals init (
        [0] class [mscorlib]System.Func`1 CS$1$0000)
    L_0000: nop
    L_0001: ldsfld class [mscorlib]System.Func`1 ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
    L_0006: brtrue.s L_001b
    L_0008: ldnull
    L_0009: ldftn int32 ConsoleApplication1.TCloser::b__3()
    L_000f: newobj instance void [mscorlib]System.Func`1::.ctor(object, native int)
    L_0014: stsfld class [mscorlib]System.Func`1 ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
    L_0019: br.s L_001b
    L_001b: ldsfld class [mscorlib]System.Func`1 ConsoleApplication1.TCloser::CS$<>9__CachedAnonymousMethodDelegate4
    L_0020: stloc.0
    L_0021: br.s L_0023
    L_0023: ldloc.0
    L_0024: ret
}

ILコードを見ると、T 1では、関数が返す匿名の依頼に対して構築されたクラスがnewobj instance void ConsoleApplication 1.TCloser/<>c_DisplayClass 1::.ctor()ですが、T 4では、まだ普通のFunc依頼で、レベルがクラスレベルになっただけです.
では、T 1で宣言されたクラスc__を見てみましょう.DisplayClass 1はどこが神聖ですか.
.class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1
    extends [mscorlib]System.Object{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed{}
    .method public hidebysig instance int32 b__0() cil managed{}
    .field public int32 n
}

C#では、パッケージを閉じるのはコンパイラが遊ぶ手口にすぎず、.NETオブジェクトのライフサイクルのルールから離れていないことが分かったでしょう.役割ドメインを変更する必要がある変数は、返されるクラスに直接カプセル化され、クラスの属性nとなり、変数nがここで返されるクラスの属性となっているため、変数のライフサイクルが関数T 1呼び出しの終了に伴って終了しないことが保証される.
ここを見て皆さんはC#閉包の経緯を大体知っていると思います.C#では、閉パッケージはクラス内の他の属性やメソッドと同様であり、原則として次のレイヤが前のレイヤ定義の各種設定をスムーズに呼び出すことができるが、前のレイヤは次のレイヤ設定にアクセスする能力を備えていない.すなわち,クラス内のメソッドの変数はクラス内のすべての属性とメソッドに自由にアクセスでき,閉パッケージはその上位層であるメソッドの様々な設定にアクセスできる.ただし、クラスはメソッドのローカル変数にアクセスできません.同様に、メソッドは内部で定義された匿名関数で定義されたローカル変数にもアクセスできません.
これはまさにC#の中の閉パッケージで、Java言語の依頼を超えて閉パッケージの第一歩の基礎を築き、その後、様々な文法糖とコンパイラを通じて現在.NET世界で全面的に開花しているLamdaとLINQを実現し、より簡潔で優雅なコードを書くことができます.