閉鎖とは何ですか?


155

時々「クロージャ」が言及されているのを見て、それを調べてみましたが、Wikiには理解できる説明がありません。誰かがここで私を助けることができますか?


あなたが知っている場合のJava / C#が、このリンクは、ヘルプ-ことを願っていますhttp://www.developerfusion.com/article/8251/the-beauty-of-closures/
グルシャン

1
閉鎖は理解しにくいです。そのウィキペディアの記事の最初の文にあるすべてのリンクをクリックして、最初にそれらの記事を理解してみてください。
ザック


3
ただし、クロージャとクラスの基本的な違いは何ですか?さて、パブリックメソッドが1つだけのクラス。
biziclop

5
@biziclop:クラスでクロージャをエミュレートできます(Java開発者がしなければならないことです)。ただし、通常は作成するのに多少冗長性が少ないため、気にしているものを手動で管理する必要はありません。(ハードコアリスパーは同様の質問をしますが、適切には他の結論に到達します-クロージャーがある場合、言語レベルのオブジェクト指向サポートは不要です)。

回答:


141

(免責事項:これは基本的な説明です。定義に関しては、少し簡略化しています)

クロージャーを考える最も簡単な方法は、変数として保存できる関数(「ファーストクラス関数」と呼ばれる)です。これは、作成されたスコープにローカルな他の変数にアクセスする特別な機能を持ちます。

例(JavaScript):

var setKeyPress = function(callback) {
    document.onkeypress = callback;
};

var initialize = function() {
    var black = false;

    document.onclick = function() {
        black = !black;
        document.body.style.backgroundColor = black ? "#000000" : "transparent";
    }

    var displayValOfBlack = function() {
        alert(black);
    }

    setKeyPress(displayValOfBlack);
};

initialize();

機能1に割り当てられているdocument.onclickとはdisplayValOfBlack閉鎖されています。どちらもブール変数を参照していることがわかりますblackが、その変数は関数の外部で割り当てられています。ためblackである関数が定義された範囲に局所、この変数へのポインタが保存されます。

これをHTMLページに配置する場合:

  1. クリックして黒に変更します
  2. [Enter]を押して「true」を表示します
  3. もう一度クリックすると、白に戻ります
  4. [false]を表示するには[Enter]を押します

これは、両方が同じ blackにアクセスでき、ラッパーオブジェクトなしで状態を格納するために使用できることを示しています。

への呼び出しsetKeyPressは、関数が他の変数と同じように渡される方法を示すことです。スコープ閉鎖に保存さは、まだ関数が定義されたものです。

クロージャーは、特にJavaScriptおよびActionScriptでイベントハンドラーとして一般的に使用されます。クロージャーを適切に使用すると、オブジェクトラッパーを作成せずに、変数をイベントハンドラーに暗黙的にバインドできます。ただし、不注意に使用すると、メモリリークが発生します(メモリ内の大きなオブジェクト、特にDOMオブジェクトを保持する未使用のイベントハンドラーのみがガベージコレクションを防止する場合など)。


1:実際には、JavaScriptのすべての関数はクロージャーです。


3
私はあなたの答えを読んでいたとき、私の頭の中に電球が点灯するのを感じました。とても有難い!:)
ジェイ

1
blackは関数内で宣言されているので、スタックが巻き戻されると破壊されませんか?
ギャブリン

1
@gablin、それはクロージャーを持つ言語についてユニークです。ガベージコレクションを使用するすべての言語はほぼ同じように機能します。オブジェクトへの参照が保持されなくなると、オブジェクトは破棄されます。JSで関数が作成されるたびに、ローカルスコープは、その関数が破棄されるまでその関数にバインドされます。
ニコール

2
@gablin、それは良い質問です。彼ら— できないとは思わない。しかし、私はガベージコレクションを作成しました。これは、JSが使用するものであり、「black関数内で宣言されているので破棄されない」と言ったときに参照しているようだからです。また、関数でオブジェクトを宣言し、それを別の場所にある変数に割り当てると、そのオブジェクトには他の参照があるため保持されることも覚えておいてください。
ニコール

1
Objective-C(およびclangの下のC)は、ガベージコレクションのないブロックを基本的にサポートしています。ランタイムサポートとメモリ管理に関する手動の介入が必要です。
quixoto

68

クロージャは基本的にオブジェクトを見るための別の方法です。オブジェクトは、1つ以上の関数がバインドされたデータです。クロージャーは、1つ以上の変数がバインドされている関数です。少なくとも実装レベルでは、この2つは基本的に同じです。本当の違いは、どこから来たのかということです。

オブジェクト指向プログラミングでは、事前にメンバー変数とそのメソッド(メンバー関数)を定義してオブジェクトクラスを宣言し、そのクラスのインスタンスを作成します。各インスタンスには、コンストラクターによって初期化されたメンバーデータのコピーが付属しています。次に、オブジェクト型の変数を取得し、それをデータとして渡します。これは、データとしての性質に焦点が当てられているためです。

一方、クロージャーでは、オブジェクトはオブジェクトクラスのように事前に定義されておらず、コード内のコンストラクター呼び出しによってインスタンス化されていません。代わりに、クロージャーを別の関数内の関数として記述します。クロージャは、外部関数のローカル変数のいずれかを参照でき、コンパイラはそれを検出し、これらの変数を外部関数のスタックスペースからクロージャの隠しオブジェクト宣言に移動します。次に、クロージャー型の変数を取得します。基本的に内部のオブジェクトですが、関数としての性質に焦点が当てられているため、関数参照として渡します。


3
+1:良い答え。クロージャーは1つのメソッドのみを持つオブジェクトとして、また任意のオブジェクトは一般的な基礎データ(オブジェクトのメンバー変数)上のクロージャーのコレクションとして見ることができます。これら2つのビューは非常に対称的だと思います。
ジョルジオ

3
非常に良い答えです。それは実際に閉鎖の洞察を説明しています。
ロボアレックス

1
@Mason Wheeler:閉鎖データはどこに保存されますか?関数のようなスタックで?または、オブジェクトのようなヒープ内にありますか?
ロボアレックス

1
@RoboAlex:ヒープ内。これは関数のように見えるオブジェクトだからです。
メイソンウィーラー

1
@RoboAlex:クロージャーとそのキャプチャデータの保存場所は実装によって異なります。C ++では、ヒープまたはスタックに格納できます。
ジョルジオ

29

クロージャーという用語は、コード(ブロック、関数)が、コードのブロックが定義されている環境によって閉じられた(つまり、値にバインドされた)自由変数を持つことができるという事実に由来します。

たとえば、Scala関数の定義を見てみましょう。

def addConstant(v: Int): Int = v + k

関数本体には、2つの名前(変数)がvあり、k2つの整数値を示します。名前vは、関数の引数として宣言されているため、バインドaddConstantされています(v関数の呼び出し時に値が割り当てられることがわかっている関数宣言を確認することにより)。関数にはバインドされている値(および方法)の手がかりが含まれていないため、名前kは関数に対して自由です。addConstantk

次のような呼び出しを評価するには:

val n = addConstant(10)

k値を割り当てる必要があります。これkは、定義されているコンテキストで名前が定義されている場合にのみ発生しますaddConstant。例えば:

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  def addConstant(v: Int): Int = v + k

  values.map(addConstant)
}

今、我々が定義されていることがaddConstant文脈でk定義されている、addConstantとなっている閉鎖すべての自由変数が今しているので、閉じ(値にバインド):addConstantそれは関数であるかのように呼び出され、周りに渡すことができます。kクロージャーが定義されると自由変数が値にバインドされるのに対しv、クロージャーが呼び出されると引数変数がバインドされることに注意してください。

したがって、クロージャは基本的に、コンテキストによってバインドされた後、自由変数を介して非ローカル値にアクセスできる関数またはコードブロックです。

多くの言語では、クロージャを一度だけ使用すると、匿名にすることができます。例えば

def increaseAll(values: List[Int]): List[Int] =
{
  val k = 2

  values.map(v => v + k)
}

自由変数のない関数は、(自由変数の空のセットを持つ)クロージャーの特殊なケースであることに注意してください。同様に、匿名関数匿名クロージャの特殊なケースです。つまり、匿名関数は自由変数のない匿名クロージャです。


このジャイブは、ロジックの閉じた式と開いた式でうまくいきます。ご回答有難うございます。
RainDoctor 14

@RainDoctor:自由変数は、論理式とラムダ計算式で同様に定義されます:ラムダ式のラムダは、自由/バインド変数の論理式の数量詞のように機能します。
ジョルジオ14

9

JavaScriptの簡単な説明:

var closure_example = function() {
    var closure = 0;
    // after first iteration the value will not be erased from the memory
    // because it is bound with the returned alertValue function.
    return {
        alertValue : function() {
            closure++;
            alert(closure);
        }
    };
};
closure_example();

alert(closure)は、以前に作成されたの値を使用しますclosure。返されたalertValue関数の名前空間は、closure変数が存在する名前空間に接続されます。関数全体を削除すると、closure変数の値は削除されますが、それまでは、alertValue関数は常にvariableの値を読み書きできますclosure

このコードを実行すると、最初の反復で値0がclosure変数に割り当てられ、関数が次のように書き換えられます。

var closure_example = function(){
    alertValue : function(){
        closure++;
        alert(closure);
    }       
}

また、関数を実行するためalertValueにローカル変数が必要なのでclosure、以前に割り当てられたローカル変数の値に自身をバインドしますclosure

そして、closure_example関数を呼び出すたびに、バインドされているclosureため、変数の増分値を書き出しますalert(closure)

closure_example.alertValue()//alerts value 1 
closure_example.alertValue()//alerts value 2 
closure_example.alertValue()//alerts value 3
//etc. 

おかげで、私はコードをテストしませんでした=)すべてが今は大丈夫のようです。
ムハ

5

「クロージャ」とは、本質的に、ローカル状態とコードをパッケージにまとめたものです。通常、ローカル状態は周囲の(字句)スコープから取得され、コードは(本質的に)内部関数であり、外部に返されます。クロージャーは、内部関数が認識するキャプチャーされた変数と内部関数のコードの組み合わせです。

残念ながら、なじみがないため、説明するのは少し難しいのです。

私が過去に使った成功例の1つは、「私たちが「本」と呼んでいるものが部屋の閉まりの中にあると想像してください。「本」とは、TAOCP 、それはドレスデンファイルの本のコピーです。したがって、あなたがどの閉鎖をしているかに応じて、コードは「本をくれ」という結果になります。


あなたはこれを忘れました:あなたの答えでen.wikipedia.org/wiki/Closure_(computer_programming)
-S.Lott

3
いいえ、私は一貫してそのページを閉じないことを選択しました。
-Vatine

「状態と関数」:staticローカル変数を持つC関数をクロージャと見なすことができますか?Haskellのクロージャには状態が関係していますか?
ジョルジオ

2
Haskellの@Giorgio Closuresは、定義されているレキシカルスコープの引数に近づいている(信じている)ので、「はい」と言います(ただし、Haskellに精通していません)。静的変数を使用するAC関数は、せいぜい非常に限定されたクロージャーです(staticローカル変数を使用して、1つの関数から複数のクロージャーを作成できるようにしたい場合は、1つだけにします)。
バティーン

静的変数を持つC関数はクロージャーではないと思うので、意図的にこの質問をしました。静的変数はローカルで定義され、クロージャー内でのみ認識され、環境にアクセスしません。また、100%確信はありませんが、逆の方法でステートメントを作成します。クロージャーメカニズムを使用して異なる関数を作成します(関数はクロージャー定義+その自由変数のバインディングです)。
ジョルジオ

5

「状態」の概念を定義せずに、クロージャが何であるかを定義することは困難です。

基本的に、関数を第一級の値として扱う完全な字句スコープを持つ言語では、特別なことが起こります。私が次のようなことをする場合:

function foo(x)
return x
end

x = foo

変数xは参照するだけでなく、最後に戻ったときに残っていfunction foo()た状態も参照しfooます。本当の魔法は、fooそのスコープ内でさらに定義された他の関数があるときに起こります。独自のミニ環境のようなものです(「通常」と同様に、グローバル環境で機能を定義します)。

機能的には、C ++(C?)の 'static'キーワードと同じ問題の多くを解決できます。これは、複数の関数呼び出しを通してローカル変数の状態を保持します。ただし、関数はファーストクラスの値であるため、同じ原理(静的変数)を関数に適用することに似ています。クロージャは、保存される関数全体の状態のサポートを追加します(C ++の静的関数とは関係ありません)。

関数を最初のクラス値として扱い、クロージャーのサポートを追加することは、同じ関数の複数のインスタンスをメモリに保持できることも意味します(クラスと同様)。これが意味することは、関数内でC ++静的変数を処理するときに必要となるように、関数の状態をリセットすることなく同じコードを再利用できることです(これについて間違っているかもしれません)。

Luaのクロージャーサポートのテストを次に示します。

--Closure testing
--By Trae Barlow
--

function myclosure()
    print(pvalue)--nil
    local pvalue = pvalue or 10
    return function()
        pvalue = pvalue + 10 --20, 31, 42, 53(53 never printed)
        print(pvalue)
        pvalue = pvalue + 1 --21, 32, 43(pvalue state saved through multiple calls)
        return pvalue
    end
end

x = myclosure() --x now references anonymous function inside myclosure()

x()--nil, 20
x() --21, 31
x() --32, 42
    --43, 53 -- if we iterated x() again

結果:

nil
20
31
42

トリッキーになる可能性があり、おそらく言語によって異なりますが、Luaでは関数が実行されるたびに状態がリセットされるようです。これを言うのは、myclosure関数/状態に(アクセスする匿名関数ではなく)直接アクセスすると、上記のコードの結果が異なるため、pvalue10にリセットされるためです。しかし、x(匿名関数)を介してmyclosureの状態にアクセスするpvalueと、メモリ内のどこかに存在していることがわかります。私はそれにもう少しあると思う、おそらく誰かが実装の性質をよりよく説明できるでしょう。

PS:C ++ 11(以前のバージョンのもの以外)をなめているのか分からないので、これはC ++ 11とLuaのクロージャーの比較ではないことに注意してください。また、LuaからC ++へのすべての「描画された線」は、静的変数とクロージャーが100%同じではないため、類似しています。同様の問題を解決するために時々使用される場合でも。

上記のコード例では、不明なことは、匿名関数と高次関数のどちらがクロージャーと見なされるかです。


4

クロージャは、状態が関連付けられている関数です。

perlでは、次のようなクロージャーを作成します。

#!/usr/bin/perl

# This function creates a closure.
sub getHelloPrint
{
    # Bind state for the function we are returning.
    my ($first) = @_;a

    # The function returned will have access to the variable $first
    return sub { my ($second) = @_; print  "$first $second\n"; };
}

my $hw = getHelloPrint("Hello");
my $gw = getHelloPrint("Goodby");

&$hw("World"); // Print Hello World
&$gw("World"); // PRint Goodby World

C ++で提供される新しい機能を見ると。
また、現在の状態をオブジェクトにバインドすることもできます。

#include <string>
#include <iostream>
#include <functional>


std::function<void(std::string const&)> getLambda(std::string const& first)
{
    // Here we bind `first` to the function
    // The second parameter will be passed when we call the function
    return [first](std::string const& second) -> void
    {   std::cout << first << " " << second << "\n";
    };
}

int main(int argc, char* argv[])
{
    auto hw = getLambda("Hello");
    auto gw = getLambda("GoodBye");

    hw("World");
    gw("World");
}

2

簡単な関数を考えてみましょう:

function f1(x) {
    // ... something
}

この関数は、他の関数内にネストされていないため、トップレベル関数と呼ばれます。JavaScript関数はすべて「スコープチェーン」と呼ばれるオブジェクトのリストに関連付けられます。このスコープチェーンは、オブジェクトの順序付きリストです。これらの各オブジェクトはいくつかの変数を定義します。

トップレベル関数では、スコープチェーンは単一のオブジェクト、グローバルオブジェクトで構成されます。たとえば、f1上記の関数には、すべてのグローバル変数を定義する単一のオブジェクトを持つスコープチェーンがあります。(ここでの「オブジェクト」という用語はJavaScriptオブジェクトを意味するものではなく、JavaScriptが変数を「ルックアップ」できる変数コンテナーとして機能する実装定義オブジェクトにすぎないことに注意してください。)

この関数が呼び出されると、JavaScriptは「アクティベーションオブジェクト」と呼ばれるものを作成し、それをスコープチェーンの先頭に配置します。このオブジェクトには、すべてのローカル変数が含まれます(たとえば、xここ)。したがって、スコープチェーンには2つのオブジェクトがあります。1つはアクティベーションオブジェクトで、その下はグローバルオブジェクトです。

2つのオブジェクトが異なる時間にスコープチェーンに配置されることに注意してください。グローバルオブジェクトは、関数が定義されたとき(つまり、JavaScriptが関数を解析して関数オブジェクトを作成したとき)に配置され、関数が呼び出されるとアクティベーションオブジェクトに入ります。

だから、私たちは今これを知っています:

  • すべての関数にはスコープチェーンが関連付けられています
  • 関数が定義されるとき(関数オブジェクトが作成されるとき)、JavaScriptはその関数とともにスコープチェーンを保存します
  • トップレベル関数の場合、スコープチェーンには関数定義時にグローバルオブジェクトのみが含まれ、呼び出し時に追加のアクティベーションオブジェクトが追加されます

入れ子関数を扱うとき、状況は興味深いものになります。それでは、作成しましょう:

function f1(x) {

    function f2(y) {
        // ... something
    }

}

ときにf1定義されます、我々はそれが唯一のグローバルオブジェクトを収容するためのスコープチェーンを取得します。

これで、 f1呼び出されると、スコープチェーンf1がアクティベーションオブジェクトを取得します。このアクティベーションオブジェクトには、変数xf2関数である変数が含まれています。そして、それf2が定義されつつあることに注意してください。したがって、この時点で、JavaScriptはの新しいスコープチェーンも保存しf2ます。この内部関数用に保存されたスコープチェーンは、現在有効なスコープチェーンです。現在有効なスコープチェーンは、f1のものです。したがって、f2のスコープチェーンはf1現在のスコープチェーンです。これには、のアクティベーションオブジェクトf1とグローバルオブジェクトが含まれます。

ときにf2呼ばれて、それはそれ自身のアクティベーションオブジェクトを含む取得だy、すでにのアクティベーションオブジェクトが含まれ、そのスコープチェーンに追加f1し、グローバルオブジェクトを。

内に別のネストされた関数が定義されている場合f2、そのスコープチェーンには、定義時に3つのオブジェクト(2つの外部関数のアクティブ化オブジェクト2つ、グローバルオブジェクト)、呼び出し時に4つが含まれます。

そのため、スコープチェーンの仕組みは理解できましたが、クロージャについてはまだ説明していません。

関数の変数が解決される関数オブジェクトとスコープ(変数バインディングのセット)の組み合わせは、コンピューターサイエンスの文献ではクロージャーと呼ばれます-JavaScriptデイビッドフラナガンによる決定版ガイド

ほとんどの関数は、関数が定義されたときに有効だった同じスコープチェーンを使用して呼び出され、クロージャーが関係していることは実際には問題ではありません。クロージャは、定義されたときに有効だったものとは異なるスコープチェーンの下で呼び出されたときに興味深いものになります。これは、ネストされた関数オブジェクトが定義された関数から返されるときに最もよく起こります。

関数が戻ると、そのアクティベーションオブジェクトはスコープチェーンから削除されます。ネストされた関数がなかった場合、アクティベーションオブジェクトへの参照はなくなり、ガベージコレクションが行われます。ネストされた関数が定義されている場合、それらの各関数はスコープチェーンへの参照を持ち、そのスコープチェーンはアクティベーションオブジェクトを参照します。

ただし、これらのネストされた関数オブジェクトが外部関数内に残っている場合は、参照されているアクティベーションオブジェクトとともに、それら自体がガベージコレクションされます。ただし、関数がネストされた関数を定義してそれを返すか、それをどこかのプロパティに保存する場合、ネストされた関数への外部参照があります。ガベージコレクションされず、それが参照するアクティベーションオブジェクトもガベージコレクションされません。

私たちの上の例では、我々は返さないf2からf1にするときのコールしたがって、f1戻り、そのアクティベーションオブジェクトは、そのスコープチェーンとごみ収集から削除されます。しかし、次のようなものがある場合:

function f1(x) {

    function f2(y) {
        // ... something
    }

    return f2;
}

ここで、返さf2れるのは、のアクティベーションオブジェクトを含むスコープチェーンを持つため、f1ガベージコレクションされません。この時点で、を呼び出すとf2、から出ていてもf1の変数にアクセスできxますf1

したがって、関数はスコープチェーンを保持し、スコープチェーンには外部関数のすべてのアクティベーションオブジェクトが含まれていることがわかります。これが閉鎖の本質です。JavaScriptの関数は「レキシカルスコープ」と呼ばれます。つまり、呼び出されたときにアクティブだったスコープではなく、定義されたときにアクティブだったスコープを保存します。

プライベート変数の近似、イベント駆動型プログラミング、部分的なアプリケーションなどのクロージャーを含む強力なプログラミング手法がいくつかあります

また、これはすべて、クロージャーをサポートするすべての言語に適用されることに注意してください。たとえば、PHP(5.3 +)、Python、Rubyなど。


-1

クロージャーは、コンパイラーの最適化です(別名、構文糖?)。一部の人々は、これを貧しい人のオブジェクトとも呼んでいます。

Eric Lippertの回答を参照してください:(以下の抜粋)

コンパイラは次のようなコードを生成します。

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はどちらも、ほぼ同じ方法でクロージャーを作成します。


この答えはCWです。なぜなら、私はエリックの素晴らしい答えに値するものではないからです。あなたが適切と思うようにそれを賛成してください。HTH
goodguys_activate

3
-1:あなたの説明はC#の根底にあります。クロージャーは多くの言語で使用されており、これらの言語の構文上の砂糖以上のものであり、機能と状態の両方を網羅しています。
マーティンヨーク

1
いいえ、クロージャは単なる「コンパイラの最適化」でも構文上の砂糖でもありません。-1
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.