web-dev-qa-db-ja.com

閉鎖とは何ですか?

「クロージャー」という言葉がときどき見られるので、調べてみましたが、Wikiでわかりません。誰かがここで私を助けてくれませんか?

157
gablin

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

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

例(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.onclickdisplayValOfBlackはクロージャです。どちらもブール変数blackを参照していますが、その変数は関数の外部で割り当てられています。 blackは、関数が定義されたスコープに対してローカルローカルであるため、この変数へのポインターは保持されます。

これをHTMLページに配置すると、次のようになります。

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

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

setKeyPressの呼び出しは、関数を変数と同じように渡す方法を示すことです。クロージャに保存されているscopeは、関数が定義されたものです。

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


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

141
Nicole

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

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

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

69
Mason Wheeler

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

たとえば、Scala関数の定義:

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

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

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

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を定義したので、addConstantclosureになっています。 closed(値にバインド):addConstantを呼び出して、関数のように渡すことができます。自由変数kは、クロージャがdefinedの場合に値にバインドされますが、引数変数vは、クロージャがinvokedの場合にバインドされます。

したがって、クロージャは基本的には関数またはコードブロックであり、これらがコンテキストによってバインドされた後、フリー変数を通じて非ローカル値にアクセスできます。

多くの言語で、一度だけクロージャを使用する場合は、それを作成できますanonymous

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

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

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

29
Giorgio

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関数は変数closure

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

_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. 
_
9
Muha

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

不慣れなため、残念ながら説明が難しいものの1つです。

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

5
Vatine

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

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

_function foo(x)
return x
end

x = foo
_

変数xfunction 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がリセットされるため、pvalue関数/状態に直接アクセスした場合(無名関数が返すのではなく)の場合、上記のコードの結果とは異なるためです。 10に;しかし、x(無名関数)を介してmyclosureの状態にアクセスすると、pvalueがメモリ内のどこかに存在し、正常に動作していることがわかります。私はそれにはもう少しあると思います、おそらく誰かが実装の性質をよりよく説明することができます。

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

上記のコード例では、無名関数と高次関数のどちらがクロージャと見なされているかがわかりません。

5
Trae Barlow

クロージャーは、関連する状態を持つ関数です。

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");
}
4
Martin York

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

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

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

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

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

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

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

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

入れ子関数を扱うと状況は面白くなります。それで、作成しましょう:

function f1(x) {

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

}

f1が定義されると、グローバルオブジェクトのみを含むスコープチェーンが取得されます。

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

f2が呼び出されると、yを含む独自のアクティベーションオブジェクトが取得され、すでにf1のアクティベーションオブジェクトとグローバルオブジェクトが含まれているスコープチェーンに追加されます。

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

スコープチェーンのしくみについては理解できましたが、クロージャーについてはまだ触れていません。

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

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

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

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

上記の例では、f2からf1を返さないため、f1の呼び出しが返されると、そのアクティベーションオブジェクトがスコープチェーンから削除され、ガベージコレクションが行われます。しかし、次のようなものがあった場合:

function f1(x) {

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

    return f2;
}

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

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

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

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

2
treecoder