クロージャは主に関数型プログラミングであり、C#の主な特徴はオブジェクト指向であるが、デリゲートやラムダ式を使用して関数型プログラミングの風味を持つコードを書くこともできる。同様に、C#ではデリゲートやラムダ式を使用してクロージャを使用することもできる。
WIKIの定義によると、クロージャは構文クロージャや関数クロージャとも呼ばれ、関数型プログラミング言語で構文バインディングを実装するための技術です。クロージャは、関数(通常はエントリアドレス)と関連付けられた環境(シンボリックルックアップテーブルに相当)を格納する構造体として実装されている。クロージャは変数の生存期間を遅らせることもできる。
え~ん。定義が少し曖昧に見えるので、次の例を見てみましょう。
class Program
{
static Action CreateGreeting(string message)
{
return () => { Console.WriteLine("Hello " + message); };
}
static void Main()
{
Action action = CreateGreeting("DeathArthas");
action();
}
}
この例は非常に単純で、ラムダ式でActionオブジェクトを作成し、後でActionオブジェクトを呼び出します。
但是仔细观察会发现,当 Action 对象被调用的时候,CreateGreeting方法已经返回了,作为它的实参的 message 应该已经被销毁了,那么为什么我们在调用 Action 对象的时候,还是能够得到正确的结果呢?
謎は、ここでクロージャが形成されていることです。CreateGreetingはすでに返されていますが、ローカル変数は返されたラムダ式にキャッチされ、その寿命が遅れます。では、クロージャの定義を振り返ってみると、もう少し明確になりませんか?
クロージャはとてもシンプルで、実際には私たちが頻繁に使用していますが、時にはそれを知らないだけです。誰もが以下のようなコードを書いたことがある。
void AddControlClickLogger(Control control, string message)
{
control.Click += delegate
{
Console.WriteLine("Control clicked: {0}", message);
}
}
このコードは実際にクロージャを使用しています。コントロールがクリックされた時点で、メッセージが宣言サイクルを超えていることが確認できるからです。クロージャを適切に使用することで、空間と時間を分離したデリゲートを書くことができます。
しかし、クロージャを使用する場合は、落とし穴に注意してください。クロージャはローカル変数の寿命を遅らせるため、プログラムが予想と異なる結果を生成する場合があります。以下の例を見てみましょう。
class Program
{
static List<Action> CreateActions()
{
var result = new List<Action>();
for(int i = 0; i < 5; i++)
{
result.Add(() => Console.WriteLine(i));
}
return result;
}
static void Main()
{
var actions = CreateActions();
for(int i = 0;i<actions.Count;i++)
{
actions[i]();
}
}
}
この例も非常に簡単で、アクションのリストを作成し、それらを順番に実行します。結果を見る
5
5
5
5
5

多くの人がこの結果を見たと思います! 0、1、2、3、4ではありませんか?何が問題なの?
結局のところ、ここでの問題はクロージャの性質にあります。クロージャは変数の寿命を遅らせるというコインの反対側として、変数が誤って複数のクロージャによって参照される可能性があります。
この例では、ローカル変数iはiを共有する5つのクロージャによって同時に参照されているため、最後に表示される値はiがループを終了したときと同じ5になります。
この問題を解決するには、ローカル変数を宣言し、各クロージャが独自のローカル変数を参照できるようにするだけです。
//其他都保持与之前一致
static List<Action> CreateActions()
{
var result = new List<Action>();
for (int i = 0; i < 5; i++)
{
int temp = i; //添加局部变量
result.Add(() => Console.WriteLine(temp));
}
return result;
}
0
1
2
3
4
各クロージャは異なるローカル変数を参照するため、問題は解決されます。
除此之外,还有一个修复的方法,在创建闭包的时候,使用foreach而不是for。至少在 C# 7.0 的版本上面,这个问题已经被注意到了,使用 foreach 的时候编译器会自动生成代码绕过这个闭包陷阱。
//这样fix也是可以的
static List<Action> CreateActions()
{
var result = new List<Action>();
foreach (var i in Enumerable.Range(0,5))
{
result.Add(() => Console.WriteLine(i));
}
return result;
}
これはC#でのクロージャの使用とその使用における小さな落とし穴です。私は皆さんが古いHuの記事を通じてこの知識ポイントを学び、開発中の回り道を少なくすることを願っています!