前回は、C# のイベントについて見ました。
今回は、イベント登録やコールバックで使える匿名メソッドを整理します。
匿名メソッドは、名前付きメソッドを別に用意せず、その場で処理を書くための構文です。
現在の C# ではラムダ式を使う場面が多いですが、匿名メソッドを理解しておくと、デリゲートとラムダ式の関係も見えやすくなります。
匿名メソッドとは
匿名メソッドは、delegate キーワードを使ってその場でメソッド本体を書く構文です。
たとえば、次のような名前付きメソッドがあるとします。
using System;
class Program
{
static void Main()
{
Action<string> printer = Print;
printer("Hello");
}
static void Print(string message)
{
Console.WriteLine(message);
}
}
この Print メソッドを別に定義せず、代入する場所で直接書けます。
using System;
class Program
{
static void Main()
{
Action<string> printer = delegate (string message)
{
Console.WriteLine(message);
};
printer("Hello");
}
}
実行結果です。
Hello
この部分が匿名メソッドです。
delegate (string message)
{
Console.WriteLine(message);
}
名前はありませんが、Action<string> に代入できるメソッドとして扱われます。
イベント登録で使う
匿名メソッドは、イベントハンドラーをその場で書きたいときにも使えます。
using System;
class Worker
{
public event EventHandler? Completed;
public void Run()
{
Console.WriteLine("処理中...");
Completed?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
Worker worker = new Worker();
worker.Completed += delegate (object? sender, EventArgs e)
{
Console.WriteLine("処理が完了しました");
};
worker.Run();
}
}
実行結果です。
処理中...
処理が完了しました
一度しか使わない短いイベントハンドラーであれば、名前付きメソッドを作らずに済みます。
ただし、あとで -= によって購読解除したい場合は注意が必要です。
worker.Completed += delegate (object? sender, EventArgs e)
{
Console.WriteLine("完了");
};
worker.Completed -= delegate (object? sender, EventArgs e)
{
Console.WriteLine("完了");
};
この 2 つは、同じ内容に見えても別の匿名メソッドです。
そのため、購読解除したい場合は、変数に入れてから登録します。
EventHandler handler = delegate (object? sender, EventArgs e)
{
Console.WriteLine("完了");
};
worker.Completed += handler;
worker.Completed -= handler;
ラムダ式との関係
匿名メソッドは、ラムダ式に近い仕組みです。
次の匿名メソッドは、
Action<string> printer = delegate (string message)
{
Console.WriteLine(message);
};
ラムダ式では次のように書けます。
Action<string> printer = message =>
{
Console.WriteLine(message);
};
さらに 1 行で書くこともできます。
Action<string> printer = message => Console.WriteLine(message);
現在はラムダ式の方がよく使われます。
ただし、匿名メソッドは「デリゲートにその場でメソッド本体を渡している」という構造が見えやすい構文です。
ローカル変数にアクセスする
匿名メソッドは、外側のローカル変数にアクセスできます。
using System;
class Program
{
static void Main()
{
int count = 0;
Action increment = delegate
{
count++;
Console.WriteLine($"count = {count}");
};
increment();
increment();
}
}
実行結果です。
count = 1
count = 2
匿名メソッドの外側で宣言した count に、匿名メソッドの内側からアクセスしています。
このように、匿名メソッドやラムダ式が外側の変数を使うことを、キャプチャと呼びます。
キャプチャされた変数は共有される
キャプチャされた変数は、値がコピーされるだけではありません。
匿名メソッドと外側のコードが、同じ変数を共有します。
using System;
class Program
{
static void Main()
{
int value = 10;
Action show = delegate
{
Console.WriteLine(value);
};
value = 20;
show();
}
}
実行結果です。
20
匿名メソッドを作った時点では value は 10 でした。
しかし、その後に value を 20 に変更してから show() を呼ぶと、20 が表示されます。
つまり、匿名メソッドは作成時点の値ではなく、変数そのものを参照しています。
ループ変数を使うときの注意
キャプチャでよく注意が必要なのは、ループ変数です。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(delegate
{
Console.WriteLine(i);
});
}
foreach (Action action in actions)
{
action();
}
}
}
実行結果です。
3
3
3
0, 1, 2 ではなく、3 が 3 回表示されます。
これは、各匿名メソッドが同じ i を参照しているためです。
意図した値を保持したい場合は、ループ内で別のローカル変数に入れます。
for (int i = 0; i < 3; i++)
{
int current = i;
actions.Add(delegate
{
Console.WriteLine(current);
});
}
この場合は、次のように表示されます。
0
1
2
static 匿名メソッドを使う
C# 9.0 では、匿名関数に static を付けられるようになりました。
匿名メソッドでも、次のように書けます。
using System;
class Program
{
static void Main()
{
Action<string> printer = static delegate (string message)
{
Console.WriteLine(message);
};
printer("Hello");
}
}
static を付けた匿名メソッドは、外側のローカル変数やインスタンスメンバーをキャプチャできません。
using System;
class Program
{
static void Main()
{
string prefix = "LOG:";
Action<string> logger = static delegate (string message)
{
Console.WriteLine($"{prefix} {message}"); // コンパイルエラー
};
}
}
prefix は外側のローカル変数なので、static 匿名メソッドの中では使えません。
これは不便に見えるかもしれませんが、意図しないキャプチャを防げるという利点があります。
ラムダ式でも同じように static を付けられます。
Func<int, int> square = static x => x * x;
キャプチャが不要な処理では、static を付けることで「外側の状態に依存しない処理である」と明示できます。
匿名メソッドで破棄パラメーターを使う
C# 9.0 では、匿名関数のパラメーターに破棄を使いやすくなりました。
破棄とは、使わない値を _ で受ける書き方です。
たとえば、イベントハンドラーでは sender や EventArgs を使わないことがあります。
button.Click += delegate (object? _, EventArgs _)
{
Console.WriteLine("クリックされました");
};
このように _ を使うと、「この引数は使わない」という意図を表せます。
複数の引数をどちらも使わない場合でも、破棄としての _ は同じ名前を複数回使えます。
Func<int, int, int> constant = delegate (int _, int _)
{
return 42;
};
ラムダ式でも同じ考え方で書けます。
Func<int, int, int> constant = (_, _) => 42;
破棄パラメーターを使うと、使っていない引数に名前を付けなくて済むため、コードの意図がすっきりします。
実践例: イベント登録を匿名メソッドで書く
最後に、イベントと匿名メソッドを組み合わせた例を見ます。
using System;
class ProgressChangedEventArgs : EventArgs
{
public int Percent { get; }
public ProgressChangedEventArgs(int percent)
{
Percent = percent;
}
}
class Downloader
{
public event EventHandler<ProgressChangedEventArgs>? ProgressChanged;
public event EventHandler? Completed;
public void Download()
{
for (int percent = 0; percent <= 100; percent += 50)
{
ProgressChanged?.Invoke(this, new ProgressChangedEventArgs(percent));
}
Completed?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
Downloader downloader = new Downloader();
string fileName = "report.pdf";
downloader.ProgressChanged += delegate (object? sender, ProgressChangedEventArgs e)
{
Console.WriteLine($"{fileName}: {e.Percent}%");
};
downloader.Completed += static delegate (object? _, EventArgs _)
{
Console.WriteLine("ダウンロードが完了しました");
};
downloader.Download();
}
}
実行結果です。
report.pdf: 0%
report.pdf: 50%
report.pdf: 100%
ダウンロードが完了しました
ProgressChanged のハンドラーは、外側の fileName を使っているためキャプチャしています。
一方、Completed のハンドラーは外側の変数を使わないため、static delegate として書いています。
まとめ
匿名メソッドは、名前付きメソッドを作らずに、デリゲートへその場で処理を渡すための構文です。
delegateキーワードで匿名メソッドを書ける- イベント登録やコールバックで使える
- 外側のローカル変数にアクセスできる
- 外側の変数を使うことをキャプチャと呼ぶ
- キャプチャされた変数は、値のコピーではなく変数そのものとして共有される
- C# 9.0 では
static匿名メソッドでキャプチャを禁止できる - C# 9.0 では、使わない引数を破棄パラメーター
_として表しやすくなった
現在はラムダ式を使うことが多いですが、匿名メソッドはデリゲートの仕組みを理解するうえで重要です。
イベント、デリゲート、匿名メソッド、ラムダ式はそれぞれ別の機能に見えますが、どれも「処理を値として扱う」という考え方でつながっています。