87

私はちょうど今、関数ポインタについて学んでいます。このテーマに関する K&R の章を読んでいるときに、最初に私を襲ったのは、「ねえ、これはクロージャのようなものだ」ということでした。この仮定が根本的に間違っていることはわかっていましたが、オンラインで検索した後、この比較の分析は実際には見つかりませんでした.

では、C スタイルの関数ポインターがクロージャーやラムダと根本的に異なるのはなぜでしょうか? 私が知る限り、匿名で関数を定義するのではなく、関数ポインターがまだ定義済みの (名前付きの) 関数を指しているという事実に関係しています。

関数に関数を渡すと、関数に名前が付けられていない 2 番目のケースの方が、通常の日常的な関数が渡される最初のケースよりも強力であると見なされるのはなぜですか?

この 2 つを厳密に比較することがなぜ間違っているのか、どのように間違っているのか教えてください。

ありがとう。

4

12 に答える 12

109

ラムダ (またはクロージャー) は、関数ポインターと変数の両方をカプセル化します。これが、C# で次のことができる理由です。

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

私はそこで匿名デリゲートをクロージャーとして使用しました (構文はラムダに相当するものよりも少し明確で C に近いです)。これは、lessThan (スタック変数) をクロージャーに取り込みました。クロージャーが評価されると、lessThan (スタック フレームが破棄されている可能性があります) が引き続き参照されます。lessThan を変更すると、比較が変更されます。

int lessThan = 100;
Func<int, bool> lessThanTest = delegate(int i) {
   return i < lessThan;
};

lessThanTest(99); // returns true
lessThan = 10;
lessThanTest(99); // returns false

C では、これは違法になります。

BOOL (*lessThanTest)(int);
int lessThan = 100;

lessThanTest = &LessThan;

BOOL LessThan(int i) {
   return i < lessThan; // compile error - lessThan is not in scope
}

ただし、2 つの引数を取る関数ポインターを定義することもできます。

int lessThan = 100;
BOOL (*lessThanTest)(int, int);

lessThanTest = &LessThan;
lessThanTest(99, lessThan); // returns true
lessThan = 10;
lessThanTest(100, lessThan); // returns false

BOOL LessThan(int i, int lessThan) {
   return i < lessThan;
}

しかし、今は評価するときに 2 つの引数を渡さなければなりません。この関数ポインターを、lessThan がスコープ内にない別の関数に渡したい場合は、チェーン内の各関数に渡すか、グローバルに昇格して、手動で有効にしておく必要があります。

クロージャーをサポートするほとんどの主流言語は無名関数を使用しますが、その必要はありません。匿名関数のないクロージャーと、クロージャーのない匿名関数を持つことができます。

要約: クロージャは、関数ポインタ + キャプチャされた変数の組み合わせです。

于 2008-10-16T15:07:11.707 に答える
44

「実際の」クロージャーを使用する言語と使用しない言語の両方のコンパイラーを作成した人として、私は上記の回答のいくつかに敬意を表して同意しません。Lisp、Scheme、ML、または Haskell クロージャーは、動的に新しい関数を作成しません。代わりに、既存の関数を再利用しますが、新しい自由変数を使用して再利用します。自由変数の集まりは、少なくともプログラミング言語の理論家によって 、環境と呼ばれることがよくあります。

クロージャーは、関数と環境を含む集合体です。Standard ML of New Jersey コンパイラでは、1 つをレコードとして表しました。1 つのフィールドにはコードへのポインターが含まれ、他のフィールドには自由変数の値が含まれていました。コンパイラは、同じコードへのポインターを含む新しいレコードを割り当てることにより、新しいクロージャー (関数ではない) を動的に作成しましたが、自由変数の値は異なります。

これらすべてを C でシミュレートできますが、面倒です。次の 2 つの手法が一般的です。

  1. クロージャーが 2 つの C 変数に分割されるように、関数 (コード) へのポインターと自由変数への別のポインターを渡します。

  2. 構造体へのポインターを渡します。構造体には、自由変数の値とコードへのポインターが含まれます。

手法 #1 は、C である種のポリモーフィズムをシミュレートしようとしていて、環境の型を明らかにしたくない場合に理想的です。つまり、環境を表すために void* ポインターを使用します。例として、Dave Hanson のC Interfaces and Implementationsを見てください。手法 2 は、関数型言語のネイティブ コード コンパイラで発生するものにより似ていますが、別のよく知られた手法とも似ています。仮想メンバー関数を持つ C++ オブジェクトです。実装はほぼ同じです。

この観察は、Henry Baker の賢明な意見につながりました。

Algol/Fortran の世界の人々は、関数クロージャが将来の効率的なプログラミングにどのような用途をもたらすかを理解していないと何年も不満を漏らしていました。その後、「オブジェクト指向プログラミング」革命が起こり、今では誰もが関数クロージャを使ってプログラムを作成しています。

于 2008-12-06T04:04:36.560 に答える
8

Cでは、関数をインラインで定義できないため、実際にクロージャを作成することはできません。あなたがしているのは、いくつかの事前定義されたメソッドへの参照を渡すことだけです。匿名メソッド/クロージャーをサポートする言語では、メソッドの定義ははるかに柔軟です。

簡単に言うと、関数ポインターにはスコープが関連付けられていません(グローバルスコープを数えない限り)が、クロージャには、それらを定義するメソッドのスコープが含まれます。ラムダを使用すると、メソッドを作成するメソッドを作成できます。クロージャを使用すると、「いくつかの引数を関数にバインドし、結果としてアリティの低い関数を取得する」ことができます。(トーマスのコメントから引用)。Cではそれを行うことはできません。

編集:例を追加します(Actionscript風の構文を使用します。これが今の私の頭の中にあります):

別のメソッドを引数として取るメソッドがあるが、呼び出されたときにそのメソッドにパラメーターを渡す方法を提供していないとしますか?たとえば、渡したメソッドを実行する前に遅延が発生するメソッド(ばかげた例ですが、単純にしておきたい)などです。

function runLater(f:Function):Void {
  sleep(100);
  f();
}

ここで、runLater()を使用して、オブジェクトの処理を遅らせたいとします。

function objectProcessor(o:Object):Void {
  /* Do something cool with the object! */
}

function process(o:Object):Void {
  runLater(function() { objectProcessor(o); });
}

process()に渡す関数は、静的に定義された関数ではなくなりました。動的に生成され、メソッドが定義されたときにスコープ内にあった変数への参照を含めることができます。したがって、グローバルスコープにない場合でも、「o」と「objectProcessor」にアクセスできます。

それが理にかなっていることを願っています。

于 2008-10-16T14:47:09.813 に答える
6

クロージャ=ロジック+環境。

たとえば、次のC#3メソッドについて考えてみます。

public Person FindPerson(IEnumerable<Person> people, string name)
{
    return people.Where(person => person.Name == name);
}

ラムダ式は、ロジック(「名前の比較」)だけでなく、パラメーター(つまりローカル変数)「名前」を含む環境もカプセル化します。

これについて詳しくは、C#1、2、3を紹介するクロージャに関する私の記事をご覧ください。クロージャによって、作業が簡単になります。

于 2008-10-16T14:49:17.110 に答える
4

C では、関数ポインターを引数として関数に渡し、関数から値として返すことができますが、関数は最上位レベルにしか存在しません。関数定義を相互にネストすることはできません。外側の関数の変数にアクセスできるネストされた関数をサポートしながら、コール スタックの上下に関数ポインターを送信できるようにするには、C で何が必要になるかを考えてみてください。(この説明に従うには、関数呼び出しが C およびほとんどの同様の言語で実装される方法の基本を知っている必要があります。ウィキペディアのコール スタックエントリを参照してください。)

ネストされた関数へのポインタとはどのようなオブジェクトですか? それを呼び出すと、外部関数の変数にどのようにアクセスするのでしょうか? コードのアドレスだけではいけません。(再帰のために、一度にアクティブな外部関数のいくつかの異なる呼び出しが存在する可能性があることに注意してください。) これはfunarg 問題と呼ばれ、下向きの funargs 問題と上向きの funargs 問題の 2 つのサブ問題があります。

下向きの funargs 問題、つまり、呼び出した関数への引数として関数ポインタを「スタックの下に」送ることは、実際には C と互換性がなく、GCCネストされた関数を下向きの funargs としてサポートします。GCC では、ネストされた関数へのポインターを作成すると、実際には、静的リンク ポインターを設定し、静的リンク ポインターを使用してアクセスする実際の関数を呼び出す動的に構築されたコード片である、トランポリンへのポインターを取得します。外部関数の変数。

上向きの funargs 問題はより困難です。GCC は、外側の関数がアクティブでなくなった後 (コール スタックにレコードがなくなった後) にトランポリン ポインターを存在させることを妨げません。その後、静的リンク ポインターがガベージを指す可能性があります。アクティベーション レコードをスタックに割り当てることができなくなりました。通常の解決策は、それらをヒープに割り当て、ネストされた関数を表す関数オブジェクトが外側の関数のアクティベーション レコードを指すようにすることです。このようなオブジェクトはクロージャーと呼ばれます。次に、言語は通常、ガベージコレクションをサポートして、レコードを指すポインターがなくなったらレコードを解放できるようにする必要があります。

ラムダ (匿名関数) は実際には別の問題ですが、通常、その場で匿名関数を定義できる言語では、それらを関数値として返すこともできるため、最終的にはクロージャーになります。

于 2008-10-16T15:41:56.753 に答える
3

ラムダは、匿名の動的に定義される関数です。Cではそれを行うことはできません...クロージャー(または2つの組み合わせ)に関しては、典型的なLispの例は次のようになります。

(defun get-counter (n-start +-number)
     "Returns a function that returns a number incremented
      by +-number every time it is called"
    (lambda () (setf n-start (+ +-number n-start))))

C 用語では、次の例に示すように、のレキシカル環境 (スタック) がget-counter無名関数によってキャプチャされ、内部で変更されていると言えます。

[1]> (defun get-counter (n-start +-number)
         "Returns a function that returns a number incremented
          by +-number every time it is called"
        (lambda () (setf n-start (+ +-number n-start))))
GET-COUNTER
[2]> (defvar x (get-counter 2 3))
X
[3]> (funcall x)
5
[4]> (funcall x)
8
[5]> (funcall x)
11
[6]> (funcall x)
14
[7]> (funcall x)
17
[8]> (funcall x)
20
[9]> 
于 2008-10-16T15:01:37.130 に答える
2

クロージャーは、その場でミニオブジェクトを宣言できるように、関数定義の時点からいくつかの変数が関数ロジックと一緒にバインドされることを意味します。

C とクロージャーに関する重要な問題の 1 つは、スタックに割り当てられた変数が、クロージャーがそれらを指していたかどうかに関係なく、現在のスコープを離れると破棄されることです。これは、ローカル変数へのポインターを不用意に返すときに発生するようなバグにつながります。クロージャーは基本的に、関連するすべての変数がヒープ上の参照カウントまたはガベージコレクションされたアイテムであることを意味します。

すべての言語のラムダがクロージャであるかどうかわからないため、ラムダをクロージャと同一視することに不安があります.時々、ラムダは変数のバインディングなしでローカルに定義された無名関数であると思います(Python pre 2.1?)。

于 2009-01-13T12:40:12.157 に答える
2

クロージャは、環境内の自由変数をキャプチャします。周囲のコードがアクティブでなくなったとしても、環境は引き続き存在します。

MAKE-ADDER新しいクロージャを返すCommon Lisp の例。

CL-USER 53 > (defun make-adder (start delta) (lambda () (incf start delta)))
MAKE-ADDER

CL-USER 54 > (compile *)
MAKE-ADDER
NIL
NIL

上記の関数を使用して:

CL-USER 55 > (let ((adder1 (make-adder 0 10))
                   (adder2 (make-adder 17 20)))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder1))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder2))
               (print (funcall adder1))
               (print (funcall adder1))
               (describe adder1)
               (describe adder2)
               (values))

10 
20 
30 
40 
37 
57 
77 
50 
60 
#<Closure 1 subfunction of MAKE-ADDER 4060001ED4> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(60 10)
#<Closure 1 subfunction of MAKE-ADDER 4060001EFC> is a CLOSURE
Function         #<Function 1 subfunction of MAKE-ADDER 4060001CAC>
Environment      #(77 20)

この関数は、両方のクロージャの関数オブジェクトが同じであるDESCRIBEことを示していますが、環境が異なることに注意してください。

Common Lisp は、クロージャーと純粋な関数オブジェクト (環境を持たないもの) の両方を関数にし、同じ方法で両方を呼び出すことができます。ここでは を使用しFUNCALLます。

于 2015-11-26T19:08:03.663 に答える
1

主な違いは、Cでの字句スコープの欠如から生じます。

関数ポインタはまさにそれであり、コードのブロックへのポインタです。参照する非スタック変数は、グローバル、静的、または同様のものです。

クロージャOTOHには、「外部変数」または「アップバリュー」の形式で独自の状態があります。字句スコープを使用して、必要に応じてプライベートまたは共有することができます。同じ関数コードで多くのクロージャを作成できますが、変数インスタンスは異なります。

いくつかのクロージャーはいくつかの変数を共有できるため、オブジェクトのインターフェースになる可能性があります(OOPの意味で)。これをCで行うには、構造体を関数ポインターのテーブルに関連付ける必要があります(これは、C ++が行うことであり、クラスvtableを使用します)。

要するに、クロージャは関数ポインタといくつかの状態です。それはより高いレベルの構成です

于 2008-10-16T14:54:37.270 に答える
1

応答のほとんどは、クロージャがおそらく無名関数への関数ポインタを必要とすることを示していますが、マークが書いたように、クロージャは名前付き関数で存在できます。Perl での例を次に示します。

{
    my $count;
    sub increment { return $count++ }
}

クロージャーは、$count変数を定義する環境です。incrementサブルーチンでのみ使用でき、呼び出し間で持続します。

于 2008-10-17T14:29:56.697 に答える
0

C では、関数ポインターは逆参照時に関数を呼び出すポインターであり、クロージャーは関数のロジックと環境 (変数とそれらがバインドされている値) を含む値であり、ラムダは通常、次の値を参照します。実際には名前のない関数です。C では、関数はファースト クラスの値ではないため、渡すことができないため、代わりにポインターを渡す必要がありますが、関数型言語 (Scheme など) では、他の値を渡すのと同じ方法で関数を渡すことができます。

于 2008-10-16T15:07:26.247 に答える