1. はじめに
学校で「関数の関数だよ」と言われたことがあるかもしれません。高階関数(Higher-Order Functions, HOF)とは、関数を引数に取る、または関数を結果として返す、あるいはその両方を行う関数のことです。
簡単に言うと:あなたのメソッドが別の関数(例えばデリゲートやラムダ)をパラメータとして受け取れる、またはそれを結果として返せるなら — おめでとう、あなたのメソッドは高階関数です!
高階関数は、単にドアを開けるだけでなく、あとで別のドアを開けるための別の鍵を渡してくれるマスターキーのようなものです。
実務での利用例
これだけ聞くと面白いけど実際何の役に立つの?という疑問が出ますね。短く言うとこうです:
高階関数はコードを柔軟に、再利用可能に、簡潔にします。
これはLINQのような仕組み、コレクションのフィルタリングやソート、データ処理パイプラインの構築、コールバックやイベントの設定、さらには依存性注入のような場面の基盤です。
いくつかの現実的なシナリオ:
- メソッドの振る舞いを制限するためにロジックを渡す(例:フィルタリング、ソート、変換)。
- 汎用のデータハンドラを作り、必要な操作をデリゲートで「ねじ込む」。
- 各関数が自分のレシピでデータを変換するチェーン(パイプライン)を構築する。
- クロスプラットフォーム抽象化を作る:Windowsで何をするか、Linuxで何をするかを単に関数として渡すだけ。
2. シンプルな高階関数の例
関数を受け取る関数
最もクラシックな例は、デリゲートやラムダを受け取るメソッドです。
// 高階関数: processという関数をパラメータとして受け取る
void ForEach<T>(IEnumerable<T> collection, Action<T> process)
{
foreach (var item in collection)
{
process(item); // 引数として渡された関数を呼び出す
}
}
// 使用例:
var numbers = new List<int> { 1, 2, 3 };
ForEach(numbers, n => Console.WriteLine($"要素: {n}"));
ここで何が起きているか?メソッドForEachは各要素に対して何をするか知らない。やっているのは渡されたプロセッサ(process)を呼ぶことだけです。プロセッサは画面に出力するかもしれないし、DBに保存するかもしれないし、UIに描画するかもしれません。
そう、これはC#のコレクションにあるForEachの動きで、ほとんどのLINQメソッドも高階関数です!
関数を返す関数
次は少し重めの例 — 関数を返すメソッドです。
// 高階関数: 別の関数を返す
Func<int, int> CreateMultiplier(int factor)
{
// factor変数を使うラムダを返す
return x => x * factor;
}
// 使用例:
var multiplyBy10 = CreateMultiplier(10);
Console.WriteLine(multiplyBy10(7)); // 70
ここでCreateMultiplierは、引数をあらかじめ決められたfactorで掛ける関数を返します。いわゆる「関数のファクトリ」です。
関数を受け取りかつ返す関数
Func<int, int> Compose(Func<int, int> f, Func<int, int> g)
{
// gを適用してからfを適用する関数を返す: f(g(x))
return x => f(g(x));
}
// 使用例:
Func<int, int> increment = x => x + 1;
Func<int, int> doubleIt = x => x * 2;
var incrementThenDouble = Compose(doubleIt, increment);
Console.WriteLine(incrementThenDouble(5)); // (5 + 1) * 2 = 12
こうしたコンポジションは、Select, Where, OrderByなどのメソッドでデータストリームを処理する際の基礎になっています。
3. C#が高階関数をどうサポートしているか
関数型言語(Haskell, F#)では関数はデフォルトで高階です。でもC#も(2.0以降)デリゲートとラムダのおかげでこのアプローチをサポートしています。
- デリゲート(Func, Action, Predicate) — 関数の型。
- ラムダ式 — その場で関数を作る構文。
- メソッドはデリゲートを受け取り、返すことができる — つまり高階関数は「箱から出して」使える。
視覚的なスキーマ
flowchart LR
A[データ] --> B[関数1]
B --> C[関数2]
C --> D[結果]
subgraph "処理パイプライン(高階関数)"
B
C
end
4. アプリケーションを発展させる
ステップバイステップのデモアプリを発展させます — 「ミニ文字列ハンドラ」にしましょう。
シンプルな高階メソッドを追加
ユーザー名のリストがあって、関数で任意に変換したいとします。
// 文字列のリストと変換用の関数を受け取るメソッド
List<string> TransformNames(List<string> names, Func<string, string> transformer)
{
var result = new List<string>();
foreach (var name in names)
{
result.Add(transformer(name));
}
return result;
}
このメソッドの使い方は?
var names = new List<string> { "アンナ", "ボリス", "セルゲイ" };
// 大文字に変換
var upperNames = TransformNames(names, n => n.ToUpper());
// 各名前に「様/さん」を付ける
var politeNames = TransformNames(names, n => "拝啓 " + n);
foreach (var n in upperNames)
Console.WriteLine(n); // АННА, БОРИС, СЕРГЕЙ(大文字のロシア語表記は例。ここは実際の出力に依存します)
foreach (var n in politeNames)
Console.WriteLine(n); // 拝啓 アンナ, ...
メソッドTransformNamesは汎用的です:変換ロジックを引数(transformer)に委譲します。例えばToUpperを呼ぶことも、別の任意のレシピを使うこともできます。
型への適応
この例は任意の型に簡単に適用できます。
// 任意のTに対して動く汎用メソッド - 高階関数
List<TResult> Map<T, TResult>(List<T> items, Func<T, TResult> transformer)
{
var result = new List<TResult>();
foreach (var item in items)
{
result.Add(transformer(item));
}
return result;
}
利用例:
var numbers = new List<int> { 1, 2, 3 };
var doubled = Map(numbers, x => x * 2); // [2, 4, 6]
var strings = Map(numbers, x => $"数値: {x}"); // ["数値: 1", ...]
5. 高階関数によるフィルタリングと集約
フィルタや検索ロジックはずっと前から高階関数で実現されています。
// フィルタ: 高階関数
List<T> Filter<T>(List<T> items, Predicate<T> criteria)
{
var result = new List<T>();
foreach (var item in items)
{
if (criteria(item)) // 条件関数を呼び出す
{
result.Add(item);
}
}
return result;
}
使い方:
var names = new List<string> { "アンナ", "ボリス", "アンドレイ" };
var aNames = Filter(names, n => n.StartsWith("ア"));
// 結果: "アンナ", "アンドレイ"
6. 関数合成(function composition)の概念
高階関数は個別の関数を使うだけでなく、それらをチェーンして合成することも可能にします。C#では、2つの関数を受け取り、それらを組み合わせた新しい関数を返すような関数で実装できます。
// 関数コンポーザ: まずgを適用し、その後fを適用する関数を返す
Func<T, TResult> Compose<T, TIntermediate, TResult>(
Func<TIntermediate, TResult> f,
Func<T, TIntermediate> g)
{
return x => f(g(x));
}
// 例:
Func<int, int> plusOne = n => n + 1;
Func<int, int> timesTwo = n => n * 2;
var plusOneThenDouble = Compose(timesTwo, plusOne);
Console.WriteLine(plusOneThenDouble(3)); // (3 + 1) * 2 = 8
7. 便利な注意点
事情の説明: なぜ昔はもっと面倒だったのか?
デリゲートやラムダが出る前は、開発者はたくさんの似たようなループを書き、コードの断片をコピーして「フィルタ」「変換」「グルーピング」をしていました。高階関数が登場すると、振る舞いの変わる部分を関数パラメータに切り出せるようになり、重複を劇的に減らし、コードの表現力が上がりました。
ちょっとした構文シュガー: 式体メソッドとしての関数
高階関数はしばしば expression-bodied メソッドで実装され、1行で書けます:
List<string> FilterNames(Predicate<string> pred) =>
Names.Where(name => pred(name)).ToList();
List<TResult> MapNames<TResult>(Func<string, TResult> transformer) =>
Names.Select(transformer).ToList();
LINQの仕組みと比較
LINQが高階関数をどう使うか見てみましょう:
| LINQメソッド | 受け取るデリゲート | 目的 |
|---|---|---|
|
|
要素をフィルタする |
|
|
要素を変換する |
|
|
キーでソートする |
|
|
コレクションを集約する(畳み込み) |
|
|
条件を満たす要素があるかチェックする |
これらのメソッドはすべて高階関数のアイデアを中心に構築されています:あなたが動作のルールを書き、標準ライブラリが「インフラ」を提供します。
8. 起こりうるミスと落とし穴
デリゲートの型を混乱すること。
最初はどこでActionが必要で、どこでFuncやPredicateが必要か分かりにくいです。
ヒント: 関数がboolを返すなら多分Predicate。値を返すならFunc、何も返さないならActionを使う。
変数のキャプチャ(クロージャ)。
返された関数が外側のスコープの変数を使う場合、呼び出す時点でその変数の値が期待通りかを注意してください。変数はコピーされず「キャプチャ」されます。
複雑なチェーンのデバッグ。
関数を長いパイプラインで組むと、どの層がデータを間違って処理したか分かりにくくなります。途中でログやコメントを入れましょう:
n => {
Console.WriteLine("操作前:" + n);
var res = n * 2;
Console.WriteLine("操作後:" + res);
return res;
}
GO TO FULL VERSION