ラムダ式、デリゲート、およびイベント

Tip

ソフトウェアの開発は初めてですか? 最初に、 作業の開始 に関するチュートリアルから始めます。 ラムダ式を使用する前に、そこにコア型とメソッドのスキルを構築します。

小さな動作 (関数) を別のメソッドに直接渡したい場合があります。 たとえば、リストをフィルター処理しても、フィルター条件は状況に応じて変わります。 可能なすべての条件に対して個別の名前付きメソッドを記述するのではなく、条件自体を引数として渡します。

ラムダ式 は、これを可能にする C# 機能です。 ラムダ式は、名前を付けずに記述するコンパクトなインライン関数です。 パラメーター リストを本文から分離するには、矢印演算子 => を使用します。

x => x * 2

左から右への読み取り: x は入力パラメーターであり、 => は "移動" を意味し、 x * 2 は本文です。 返された値を計算します。 パラメーターまたは複数のパラメーターがない場合は、かっこで囲みます( () => 42 または (left, right) => left + right)。

デリゲートはラムダ式をサポートします

ラムダ式を使用するには、C# コンパイラは、パラメーターの型と戻り値の型という 2 つのことを認識している必要があります。 その説明 (パラメーター型と戻り値の型) は、 デリゲート型と呼ばれます。

デリゲート型は、メソッド シグネチャを表す型です。 デリゲート型の変数は、パラメーター型と戻り値の型が一致している限り、ラムダ式や名前付きメソッドなどの任意の一致するメソッドを保持できます。

delegate キーワードを使用してデリゲート型を宣言します。

delegate int Transform(int value);

この宣言には、「Transform は、1 つの int を受け入れ、 intを返すメソッドのデリゲート型です」と記述されています。その後、ラムダ式または名前付きメソッドをその型の変数に割り当てることができます。

Transform doubler = x => x * 2;    // assign a lambda expression
Transform squarer = Square;         // assign a named method

Console.WriteLine(doubler(5));      // 10
Console.WriteLine(squarer(5));      // 25

static int Square(int value) => value * value;

doublersquarerの両方がTransform型の値を保持します。 通常のメソッドと同じようにそのまま呼び出します。 コンパイラは、割り当てたものが宣言されたシグネチャと一致することを確認します。

組み込みのデリゲート型: FuncAction

すべての状況に対してカスタム デリゲート型を宣言すると、繰り返し作業となることがあります。 .NETでは、ほとんどのシナリオに対応する汎用デリゲート型 FuncAction の 2 つのファミリが用意されているため、delegate キーワードを自分で使用する必要はほとんどありません。

どちらのファミリも 0 ~ 16 個の入力型パラメーターを持つバージョンであるため、任意の数の入力にスケーリングされます。 2 つのファミリの主な違いは次のとおりです。

  • System.Func<T,TResult> ( Func<T1, T2, TResult>など) は、 値を返すメソッドを表します。 型パラメーターのうち、最後のものは常に戻り値の型であり、それ以前のものはすべて入力型です。
  • System.Action<T> (および Action<T1, T2>など) は、 何も返さなかった メソッド (void) を表します。 すべての型パラメーターは入力型です。 System.Action 型パラメーターを指定しない場合は、入力がなく、戻り値がないメソッドを表します。

たとえば、 Func<int, int, int> では、2 つの int 入力と 1 つの int 結果を持つメソッドについて説明します。 Action<string> は、1 つの string 入力を持ち、戻り値を持たないメソッドを記述します。

Func<int, int, int> add = (left, right) => left + right;
Action<string> report = message => Console.WriteLine($"Report: {message}");

int total = add(5, 9);
report($"5 + 9 = {total}");

ラムダで説明的なパラメーター名を使用して、メソッド本体全体をスキャンすることなく意図を理解できるようにします。

ラムダ式をメソッドに渡す

メソッドが Func または Action パラメーターを宣言すると、呼び出し元はそのデリゲート型に一致するラムダ式を渡します。 コンパイラは、ラムダのパラメーター型と戻り値の型が宣言されたデリゲート型と一致することを確認します。 一致しない場合、コードはコンパイルされません。

int[] numbers = [1, 2, 3, 4, 5, 6];
int[] evenNumbers = Filter(numbers, value => value % 2 == 0).ToArray();

Console.WriteLine(string.Join(", ", evenNumbers));

Filter メソッドは、Func<int, bool>という名前のpredicate パラメーターを宣言します。 Func<int, bool>型は、呼び出し元に予想される図形 (1 つのint入力、1 つのbool結果) を通知します。 呼び出し元は、その引数として value => value % 2 == 0 を渡します。 このパターンは、LINQ と多くの.NET API 全体に表示されます。

ラムダ式を自己完結型にする

ラムダ式は、周囲のコードから変数を参照できます。 キャプチャ とは、ラムダが自身の本体の外部で宣言された変数への参照を保持することを意味します。 ラムダとそれがキャプチャする変数の組み合わせは 、クロージャと呼ばれます。

何もキャプチャする必要がない場合は、 static 修飾子をラムダに追加します。 静的ラムダは、本体内で宣言された独自のパラメーターと値のみを使用できます。 外側のスコープからローカル変数またはインスタンスの状態をキャプチャすることはできません。

Func<int, bool> isEven = static value => value % 2 == 0;

Console.WriteLine(isEven(14));
Console.WriteLine(isEven(15));

静的ラムダは意図を明確にし、偶発的なキャプチャを防ぎます。

入力が無関係な場合は破棄パラメーターを使用する

デリゲートシグネチャに、必要のないパラメーターが含まれている場合があります。 破棄 _ を使用して、その選択を明示的に通知します。

一般的な例としては、 senderEventArgsを使用しないイベント ハンドラー、いくつかの入力のみが必要なコールバック、使用しないインデックスを提供する LINQ オーバーロードなどがあります。

Action<int, int, string> statusUpdate = (_, _, message) => Console.WriteLine(message);
statusUpdate(200, 42, "Operation completed");

破棄すると、重要なパラメーターが表示されるため、読みやすさが向上します。

イベントは省略可能な通知を提供します

イベントは、何かが発生したときに 1 つのオブジェクト (パブリッシャー) が他のオブジェクト (サブスクライバー) に通知するために使用するメカニズムです。 パブリッシャーは、リッスンしているユーザーやサブスクライバーの数を知る必要はありません。 サブスクライバーはオプトインを選択します。

イベントはデリゲートに基づいて構築されます。 イベントは、 event キーワードによって追加の制限が適用されるデリゲート フィールドです。外部コードは、イベントのサブスクライブ (+=) またはサブスクライブ解除 (-=) のみを行うことができます。イベントを宣言するクラスのみが呼び出し (発生) できます。

イベント デリゲート型の.NET規則は System.EventHandler<TEventArgs> です。ここで、T は通知に含まれるデータの種類です。 そのシグネチャには常に、 sender (イベントを発生させたオブジェクト) と T型のイベント データの 2 つのパラメーターがあります。

MessagePublisher publisher = new();
publisher.MessagePublished += (_, message) => Console.WriteLine($"Received: {message}");

publisher.Publish("Records updated");

コードをレビューする:

  • MessagePublisherevent EventHandler<string>? MessagePublishedを宣言します。 event キーワードは、呼び出し元がサブスクライブまたはサブスクライブ解除のみを実行できることを意味します。直接呼び出すことはできません。
  • publisher.MessagePublished += (_, message) => ... はラムダ式を使用してサブスクライブします。 _は、sender パラメーターを破棄します。このハンドラーはパラメーターを必要としないためです。
  • publisher.Publish("Records updated") はイベントを発生させ、サブスクライブされたすべてのハンドラーを実行します。

サブスクリプションは任意です。 ?.Invoke(...) メソッドのPublishは、少なくとも 1 つのサブスクライバーがアタッチされている場合にのみイベントが発生します。 パブリッシャーは、誰かがそれをリッスンしているかどうか、あるいは受信側が存在するかかどうかを気にすることなく、イベントを発生させます。

こちらも参照ください