前回は、デリゲートを使った状態通知とマルチキャストを見ました。
今回は、C# で標準的に使われるジェネリックデリゲートである Action<> と Func<> を整理します。
デリゲートは自分で定義できます。
delegate void MessageHandler(string message);
delegate int Calculator(int x, int y);
しかし、よく使う形のデリゲートは .NET にあらかじめ用意されています。
代表的なものが、Action<> と Func<> です。
ジェネリックデリゲートとは
ジェネリックデリゲートは、型引数によって引数や戻り値の型を指定できるデリゲートです。
たとえば、次の独自デリゲートを考えます。
delegate void LogHandler(string message);
これは、string を 1 つ受け取り、戻り値がないメソッドを表します。
同じ形は、Action<string> で表せます。
Action<string> logger;
また、次の独自デリゲートを考えます。
delegate int Calculator(int x, int y);
これは、int を 2 つ受け取り、int を返すメソッドを表します。
同じ形は、Func<int, int, int> で表せます。
Func<int, int, int> calculator;
Action は戻り値がない処理を表す
Action は、戻り値がない処理を表すデリゲートです。
引数がない場合は、Action を使います。
using System;
class Program
{
static void Main()
{
Action action = SayHello;
action();
}
static void SayHello()
{
Console.WriteLine("Hello");
}
}
実行結果です。
Hello
Action は、次の独自デリゲートに近い意味を持ちます。
delegate void SomeAction();
Action
引数を 1 つ持つ場合は、Action<T> を使います。
using System;
class Program
{
static void Main()
{
Action<string> logger = WriteLog;
logger("処理を開始しました");
}
static void WriteLog(string message)
{
Console.WriteLine($"LOG: {message}");
}
}
実行結果です。
LOG: 処理を開始しました
Action<string> は、次の形のメソッドを表します。
stringを 1 つ受け取る- 戻り値は
void
独自デリゲートで書くなら、次と同じ形です。
delegate void LogHandler(string message);
Action<T1, T2>
引数が複数ある場合も Action を使えます。
using System;
class Program
{
static void Main()
{
Action<string, int> printUser = PrintUser;
printUser("Alice", 25);
}
static void PrintUser(string name, int age)
{
Console.WriteLine($"{name} は {age} 歳です");
}
}
実行結果です。
Alice は 25 歳です
Action<string, int> は、string と int を受け取り、戻り値がない処理を表します。
Func は戻り値がある処理を表す
Func は、戻り値がある処理を表すデリゲートです。
Func では、最後の型引数が戻り値の型になります。
Func<int, int, int>
これは、次の形のメソッドを表します。
- 1 つ目の引数が
int - 2 つ目の引数が
int - 戻り値が
int
例を見てみます。
using System;
class Program
{
static void Main()
{
Func<int, int, int> calculator = Add;
int result = calculator(10, 20);
Console.WriteLine(result);
}
static int Add(int x, int y)
{
return x + y;
}
}
実行結果です。
30
独自デリゲートで書くなら、次と同じ形です。
delegate int Calculator(int x, int y);
Func
引数がなく、戻り値だけがある場合は Func<TResult> を使います。
using System;
class Program
{
static void Main()
{
Func<DateTime> getNow = GetCurrentTime;
Console.WriteLine(getNow());
}
static DateTime GetCurrentTime()
{
return DateTime.Now;
}
}
Func<DateTime> は、引数を取らず、DateTime を返す処理を表します。
ラムダ式と組み合わせる
Action や Func は、ラムダ式とよく組み合わせて使います。
Action<string> logger = message =>
{
Console.WriteLine($"LOG: {message}");
};
logger("ラムダ式から出力");
Func の例です。
Func<int, int, int> multiply = (x, y) =>
{
return x * y;
};
Console.WriteLine(multiply(3, 4));
実行結果です。
12
戻り値をそのまま返すだけなら、さらに短く書けます。
Func<int, int, int> multiply = (x, y) => x * y;
ラムダ式を使うと、別メソッドを定義しなくても、その場で処理を渡せます。
実践例: フィルター処理を差し替える
Func<T, bool> は、条件判定を表すときによく使われます。
次の例では、リストから条件に合う値だけを取り出します。
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = Filter(numbers, IsEven);
List<int> largeNumbers = Filter(numbers, n => n >= 4);
Console.WriteLine(string.Join(", ", evenNumbers));
Console.WriteLine(string.Join(", ", largeNumbers));
}
static List<int> Filter(List<int> source, Func<int, bool> predicate)
{
List<int> result = new List<int>();
foreach (int item in source)
{
if (predicate(item))
{
result.Add(item);
}
}
return result;
}
static bool IsEven(int number)
{
return number % 2 == 0;
}
}
実行結果です。
2, 4, 6
4, 5, 6
Filter メソッドは、条件そのものを知りません。
条件は、引数として渡された Func<int, bool> に任せています。
static List<int> Filter(List<int> source, Func<int, bool> predicate)
このようにすると、処理の流れは共通化しつつ、判定ロジックだけを差し替えられます。
実践例: 処理完了時のコールバック
Action は、処理完了時のコールバックにも使えます。
using System;
class Program
{
static void Main()
{
DownloadFile(
"report.pdf",
fileName => Console.WriteLine($"{fileName} のダウンロードが完了しました")
);
}
static void DownloadFile(string fileName, Action<string> completed)
{
Console.WriteLine($"{fileName} をダウンロード中...");
// 実際にはここでダウンロード処理を行う
completed(fileName);
}
}
実行結果です。
report.pdf をダウンロード中...
report.pdf のダウンロードが完了しました
DownloadFile は、完了後に何をするかを知りません。
完了後の処理は、Action<string> として外部から受け取っています。
Action と Func の使い分け
基本的な使い分けはシンプルです。
| デリゲート | 使う場面 |
|---|---|
Action / Action<> |
戻り値がない処理 |
Func<> |
戻り値がある処理 |
例です。
Action<string> print = message => Console.WriteLine(message);
Func<int, int, int> add = (x, y) => x + y;
戻り値がないなら Action、戻り値があるなら Func と考えるとよいでしょう。
独自 delegate と Action / Func の使い分け
Action や Func があれば、独自のデリゲート型は不要に見えるかもしれません。
しかし、独自デリゲートにも意味があります。
たとえば、次のデリゲートを見てみます。
delegate void OrderStatusChangedHandler(Order order);
これは、技術的には次のように書けます。
Action<Order>
ただし、OrderStatusChangedHandler という名前があることで、「注文状態が変わったときの通知である」という意味がコード上に残ります。
一方、単純なコールバックや小さな処理の受け渡しであれば、Action / Func の方が簡潔です。
目安は次の通りです。
| 場面 | 選択肢 |
|---|---|
| 汎用的な処理を渡したい | Action / Func |
| ドメイン上の意味を名前で表したい | 独自 delegate |
| イベントとして公開したい | 独自 delegate または EventHandler 系 |
まとめ
Action<> と Func<> は、.NET が標準で用意しているジェネリックデリゲートです。
Actionは戻り値がない処理を表すFuncは戻り値がある処理を表すFuncでは最後の型引数が戻り値の型になる- ラムダ式と組み合わせると、その場で処理を渡せる
- 条件判定には
Func<T, bool>がよく使われる - 完了通知などのコールバックには
Action<T>が使いやすい - 意味のある名前を残したい場合は、独自
delegateも有効
デリゲートを理解すると、C# のイベント、ラムダ式、LINQ、非同期処理のコールバックなどが読みやすくなります。
まずは、戻り値がなければ Action、戻り値があれば Func と考えるところから始めるとよいでしょう。