はじめに

 第1回ではif文地獄をインターフェースで、第2回では画面凍結をasync/awaitで、それぞれ攻略してきました。連載最終回となる第3回のテーマは、Action/Funcの「デリゲート沼」です。
 Action、Func。C#の入門書を読み進めていると、ある日突然この並びに当たります。型パラメータが羅列されたこの型は何なのか。さらに「メソッドを変数に代入?」というモヤモヤ。
 今回は、新人プログラマーの相談から始まります。あなたが先輩役として、AIと一緒に、この沼から助け出してあげましょう。

「これって何のためにあるんですか?」と聞かれて

 ある日、新人さんが入門書を抱えて聞いてきました。「デリゲートのところで、ActionとかFuncってあるんですが、型パラメータが並んでいるだけで、何なのか見当がつきません。そもそも、メソッドを変数に代入するなんて」。
 そのAction、Funcについて、さっそくAIに聞いてみましょう。

C#のAction<T>とFunc<T, TResult>って何のためにあるんですか? 違いの前に、両方の根っこにあるデリゲートとは何かを、新人プログラマーにもわかるよう簡潔に教えてください。

 ChatGPTの回答を要約すると、次のとおりです。

まず大事なのは、「メソッド(処理)を、値みたいに扱える」という考え方です。
普通、変数には数字や文字列を入れますよね。
でもC#では、「あとで実行する処理そのもの」も変数に入れられます。
そのしくみが「デリゲート」です。
Action と Func は、「よく使うデリゲートを、簡単に書けるようにしたもの」です。
どちらも、「メソッドを、あとで呼び出せる値として扱う」ためのしくみです。

 モヤモヤの本丸はここですね。ActionとFuncの違いは、共通点をつかんだ後でゆっくり覚えればよさそうです。

「メソッドを、値みたいに扱う」をたとえる

 言葉では理解できても、新人さんはまだピンと来ない様子です。ここでもAIに、身近なたとえを引き出してもらいましょう。

C#の「メソッド(処理)を、値みたいに扱える」という感覚を、新人プログラマーにもピンとくる身近で日常的なたとえに置き換えてもらえますか? いくつか候補をあげて、その中からいちばんぴったりなものを選んでください。

 ChatGPTは、次のように整理してくれました(適宜要約しています)。

候補として「リモコンのボタン」「店員への依頼メモ」「スマホのホーム画面のショートカット」などがありますが、いちばんC#のデリゲートに近いのは「スマホのショートカットアイコン」だと思います。
このたとえの強いところは、
「処理を持ち運べる」「後で差し替えられる」「同じ呼び出し方で別動作になる」「実行はタップに近い」
という、デリゲートの核心がかなり自然に表現できる点です。

 ショートカットなら、新人さんも毎日触っていますね。「メソッドを変数に?」は、「ショートカットをホーム画面のアイコン枠に保存する」と捉えればすっきりです。

図解を依頼する

 このたとえで図解してもらいました。今回は、図中テキストの精度を比較して、ChatGPTを採用しています。

スマホのショートカットのたとえを使って、C#のデリゲート(Action/Func)の概念を、シンプルなイラストで図解してください。
  • スマホのショートカットで表現したデリゲートのイメージ(ChatGPTの生成)

    スマホのショートカットで表現したデリゲートのイメージ(ChatGPTの生成)

 アイコンの中身(やること)は外から見えませんが、タップすれば登録した処理が走る――これがAction/Funcの感覚ですね。

実コードで確かめてみる

 たとえのイメージを持ったまま、コードに移りましょう。まずは最小コードで確認し、商品リストの絞り込みへ広げていきます。

まず「変数に入れて、あとで呼ぶ」を体感する

 「メソッドを変数に?」のモヤモヤは、字面で見るのがいちばんです。AIに最小コードを出してもらいましょう。

「メソッドを変数に入れて、あとで呼び出す」感覚を体感できる最小のC#コードをお願いします。商品(Product:名前・価格・在庫の3プロパティ)の判定を題材に。適宜コメントをつけてください。

 ChatGPTが生成したコードがこちらです。Productクラス(Name/Price/Stockの3プロパティ)とusingディレクティブは省いた抜粋です。コード本体は、Mainメソッドの中身、または.NET 6以降のトップレベルステートメントとして読んでください。

// あとで judge に代入する候補のメソッド(Productを受け取ってboolを返す)
bool IsCheap(Product p)
{
    return p.Price < 1000;
}
bool IsOutOfStock(Product p)
{
    return p.Stock == 0;
}

var apple = new Product { Name = "りんご", Price = 200, Stock = 10 };

// 「Productを受け取って、boolを返すメソッド」を入れられる変数
Func<Product, bool> judge;

// メソッドを変数に入れる
judge = IsCheap;

// 変数に入れたメソッドを、あとで呼び出す
Console.WriteLine(judge(apple));   // True

// 別のメソッドに差し替える
judge = IsOutOfStock;

// 同じ judge(...) でも、実行される処理が変わる
Console.WriteLine(judge(apple));   // False

 実コードではFunc judge = IsCheap;と1行にまとめてもかまいません。AIの回答ではあえて、「枠を用意する」と「ショートカットを置く」の2ステップにしています。
 judge = IsCheap;――これが、まさにメソッドを変数にした瞬間ですね。judge(apple) と書くと、変数に入っている処理が呼び出されます。ショートカットのたとえと並べると、こうなります。

  • Func judge = ホーム画面のアイコン枠
  • judge = IsCheap; = 枠に「IsCheap」というショートカットを置く
  • judge(apple) = アイコンをタップして実行する
  • judge = IsOutOfStock; = 同じ枠に別のショートカットを入れ替える

 AIのコードの、judge = IsCheap;のところ、メソッドを丸ごと代入しているようにも見えますが、厳密にはメソッド本体そのものではなく、IsCheapを呼び出すための参照です。ショートカットのアイコンも、処理本体ではなく本体への呼び出し口でしたね。Action/Funcの変数も、メソッド本体ではなくその参照を持つ箱、と押さえておけば十分です。
 「メソッドを変数に?」の正体は、このたった数行に詰まっています。コードとたとえを橋渡しする対応図を描いてもらいましょう。

次のC#のコードとスマホのホーム画面の「アイコン枠」のたとえを、左右に並べた1枚の対応図として描いてください。 
(さきほどの4行の箇条書きそのまま入力)
  • C#のコード4行と、スマホ操作の対応図(ChatGPTの生成)

    C#のコード4行と、スマホ操作の対応図(ChatGPTの生成)

 コードの字面だけ見ると硬く感じるFuncも、右側のスマホ操作と並ぶと、ぐっと身近な手触りになりませんか?

新人さん「p => ... って何ですか?」

 ここまで見ていた新人さんが、こんどはこう聞いてきました。「入門書のこのあとに、p => p.Price < 1000 みたいな書き方が出てきます。これは何ですか?」
 これをそのままAIにぶつけてみましょう。

C#で、p => p.Price < 1000といったコードの、=>の記法を新人プログラマー向けに解説してください。
・=>はそもそも何を表しているのか
・左側、右側はそれぞれ何か
・先ほど見た IsCheap のような名前付きメソッドの代わりに、この記法を使うと、何がうれしいのか
新人プログラマーにもわかる言葉で、簡潔にお願いします。

 ChatGPTからは、こんな回答が返ってきました(適宜要約しています)。

・=>はC#では ラムダ式 と呼ばれる記法で、ざっくり言うと、「左のものを受け取って、右の処理・結果を返す」という意味です。
・左側は引数(入力)。例:pはProductを1つ受け取る、という意味です(型は文脈から推論されます)。
・右側は処理(本体)。例:p => p.Price < 1000は「pを受け取ったらp.Price < 1000を評価して、true/falseを返す」という意味になります。複数行の処理も書けます:p => { Console.WriteLine(p.Name); return p.Stock == 0; }。

うれしい点:その場で短い処理を書けるので、bool IsCheap(Product p) { ... }のように別メソッドとして名前を付けて定義する手間が減ります。Func/Actionと相性がよく、Where(p => ...)のように呼び出し側で条件や処理を差し替えやすいです。

 AI回答をよく見ると、{}でくくった中にreturnが書かれていますね。値を返すラムダ式は、本体が式1つだけならreturnは書かない、{ }でくくったときだけ必要、と覚えておきましょう。
 ChatGPTの言う「別メソッドを定義する手間が減る」――ここがラムダ式のうれしさですね。ショートカットを毎回フォルダから選ばず、その場で「やること」を書いて貼り付ける感覚です。さきほどの、judge = IsOutOfStock;も、ラムダ式で、judge = p => p.Stock == 0;と書き直せます。

Funcを使った絞り込み

 変数に処理を入れて呼び出す感覚をつかんだら、こんどはメソッドの引数として渡してみましょう。商品リストの絞り込みを題材にします。
 AIには、クラス構造や絞り込みの3パターンまで具体的に指定します。数値の比較・等価判定・文字列のContainsを並べて、同じ型のjudgeで差し替えられるところを確かめるためです。

新人プログラマーにも理解できる最小限のコードで、Func<Product, bool>を実際に触れる例を見せてください。商品リストの絞り込みを題材にしてください。

・Productクラス(プロパティはName(string)、Price(int)、Stock(int)の3つだけ)
・ProductFilterクラスにPick(List<Product> products, Func<Product, bool> judge)を実装
・Mainメソッドから、ラムダ式を直接渡して3パターンの絞り込みを呼び出す(数値しきい値・等価判定・文字列Containsの3種類)

 ChatGPTは、次のようなコードを返してくれました(同じく要点を抜粋)。

public class ProductFilter
{
    // Pickは条件の中身を知らない――judgeを“呼び出す”だけ
    public List<Product> Pick(List<Product> products, Func<Product, bool> judge)
    {
        var result = new List<Product>();
        foreach (var p in products)
        {
            if (judge(p)) result.Add(p);  //「変数に入った代理の処理」を実行
        }
        return result;
    }
}

// Main内(Productクラスは前節と同じ、抜粋)
var products = new List<Product>
{
    new Product { Name = "セール イヤホン",   Price = 980,   Stock = 12 },
    new Product { Name = "ノートPC",          Price = 89800, Stock = 5  },
    new Product { Name = "スマホケース",      Price = 1280,  Stock = 0  },
    new Product { Name = "セール マグカップ", Price = 600,   Stock = 3  },
    new Product { Name = "USBハブ",           Price = 480,   Stock = 6  },
};
var filter = new ProductFilter();

// 渡すラムダ式を変えるだけで、Pickの動きが変わる
var cheap   = filter.Pick(products, p => p.Price < 1000);            // → 3件
var soldOut = filter.Pick(products, p => p.Stock == 0);              // → 1件
var sale    = filter.Pick(products, p => p.Name.Contains("セール")); // → 2件

 コードに続いて次のような回答がありました。

この例のポイントは、Pick 自体は「どう絞り込むか」を知らないことです。
p => p.Price < 1000のような「判定ルール」を、あとから外側から渡しています。
つまり Func<Product, bool> は、
Productを受け取って、true / false を返す、“判定処理そのもの”
を変数のように扱うための型です。

 ここでも、Pickの中身は変えず、渡すラムダ式だけを差し替えています。判定を外から差し替えられる――これがFuncの便利さです。
 ただし、p => ...と書いたラムダ式も、この例では、判定の中身がPickのコードに直接埋め込まれるわけではありません。Pickは、あとで呼び出せる「判定処理」をFuncとして受け取っています。Pickに渡しているのは、やはりその処理を呼び出すための入り口です。

Actionで「結果が不要な処理」を渡す

 FuncとActionの違いは、戻り値のあり/なしです。Funcの感覚がつかめれば、Actionも同じ要領です。Pickの相方として、絞り込み結果に処理を1つずつ適用するApplyメソッドを、ProductFilterに足してもらいましょう。

ProductFilterに、Pickの相方になるApply(List<Product> products, Action<Product> action)を追加してください。Pickと同じ手触りで、中ではforeachでactionを呼び出すだけにしてください。絞り込み結果のリストに、別のActionを差し替えて適用する例も付けてもらえますか?

 ChatGPTで生成されたコードの要点は、次のとおりです。

public class ProductFilter
{
    // Pickは前節のとおり(省略)

    // Applyは処理の中身を知らない――actionを"呼び出す"だけ
    public void Apply(List<Product> products, Action<Product> action)
    {
        foreach (var p in products)
        {
            action(p);  //「変数に入った代理の処理」を実行
        }
    }
}

// Main内(productsとfilterは前節と同じ)
var sale = filter.Pick(products, p => p.Name.Contains("セール"));

// Action<Product>:表示するだけ
Action<Product> print = p => Console.WriteLine(p.Name);
filter.Apply(sale, print);
// → セール イヤホン
// → セール マグカップ

// 別のActionに差し替え、直接渡すこともできます
filter.Apply(sale, p => Console.WriteLine($"{p.Name}:{p.Price}円"));
// → セール イヤホン:980円
// → セール マグカップ:600円

 PickもApplyも、渡された代理を呼び出すだけ。戻り値で集めるか、ただ呼ぶか――その違いだけです。渡すActionを差し替えれば出力もそのまま入れ替わり、Funcのときとまったく同じ手触りですね。

よくある疑問・ミス

 締めくくりに、慣れないうちはやりがちなミスをAIに挙げてもらいましょう。

Action/Func/ラムダ式を使うときに、新人プログラマーがやりがちなミスを3つ挙げてください。問題は何か、正しくはどう書くかも簡潔にお願いします。

 ChatGPTの回答では、次の3点が挙げられました(適宜要約しています)。

1. Funcなのに戻り値がない/Actionで値を返す
Func<T, TResult>は値を返す処理、Action<T>は返さない処理です。判定や計算はFunc、表示など実行だけならActionにします。

2. ==のつもりで=を書く
p => p.Stock = 0は代入です。判定したいなら、p => p.Stock == 0と書きます。

3. 渡す前に呼び出してしまう
Pick(products, IsCheap(p))では、実行結果を渡してしまいます。メソッドならPick(products, IsCheap)、ラムダならp => p.Price < 1000です。

 この3つは、Funcのような「boolを返すデリゲート」に渡す場面なら、型が合わずビルド段階で止まります。
 ただし、==のつもりで=を書くミスは要注意です。Actionに渡す場面では、p => p.Stock = 0がコンパイルを通過してしまい、判定のつもりが値を書き換える副作用に化けます。型は強い味方ですが、「値を返すのか、ただ実行するのか」を意識してFuncとActionを選ぶことも大切です。

さらに学びたい方へ

 今回はデリゲートの中でも、よく使うAction/Funcに絞りました。ここでつかんだ感覚は、C#のあちこちに通じています。なかでもいちばんよく出会うのが、LINQです。products.Where(p => p.Price < 1000)のような書き方、入門書でも出てきますよね。じつはこのWhere、さきほど自作したPickと深いつながりがあります。AIに種明かししてもらいましょう。

さきほど自作したPick(List<Product>, Func<Product, bool>)と、LINQのproducts.Where(p => p.Price < 1000)は、何が同じで何が違いますか? 新人プログラマーにもわかるよう、簡潔にお願いします。

 ChatGPTの回答を短くまとめると、こうです。

本質は同じです。どちらも商品を1つずつ見て、条件がtrueのものだけ残します。その条件を、Func<Product, bool>として外から渡しています。
違いは、Pickが商品リスト専用の自作版なのに対し、WhereはC#標準の汎用版であることです。さらにWhereはすぐにループせず、必要になるまで処理を遅らせます。
新人向けには、Pickは「自作の簡易Where」、Whereは「C#標準の高機能版」くらいで十分です。

 つまり、さきほど自作したPickの正体は、いわば、簡易版のWhere。LINQにはWhereのほかにも、Select(変換)やOrderBy(並び替え)など、処理を渡して使うメソッドが豊富にそろっています。PickでつかんだFuncを渡す感覚は、そのままLINQへの入り口になりますね。
 ちなみにAIが触れた「必要になるまで処理を遅らせる」、これはLINQの遅延実行と呼ばれる性質です。気になったら「LINQの遅延実行って何?」とAIに聞いてみましょう。
 デリゲートについて、もう一歩踏み込みたいなら、こんどは自分で定義する側にも目を向けてみてください。

C#で、Func<Product, bool>のような既製の型ではなく、自分でdelegateキーワードでデリゲート型を定義する場面はありますか? 簡単な例で教えてください。

 ほかにも「デリゲートとイベント(button.Click += OnClick;)はどう違うか」など、デリゲートを起点にした問いはいくつも広げられます。気になったところから、手元のChatGPTやGeminiにぶつけてみてください。

おわりに

 今回は、入門書で挫折しがちなActionとFuncのデリゲート沼を、スマホのショートカットのたとえで攻略しました。「処理へのショートカットを持つ」と考えると、ぐっと見通しがよくなりますね。
 この連載では、違和感をぶつけ、身近なたとえに置き換え、シンプルな図解にし、最小限のコードで理解し、やりがちなミスで検証する――この流れは、どんなC#の概念にも、そして他の言語や、まったく未知の技術にも応用できます。
 AIは答えを教えてくれる先生ではなく、横に座って一緒にホワイトボードを覗き込んでくれる学習パートナー。問いを設計するのはあくまで自分、というスタンスを忘れずに。
 連載におつき合いいただき、ありがとうございました。AI図解で、これからもどんどんC#のモヤモヤを攻略していきましょう。

WINGSプロジェクト 髙江 賢(著)山田祥寛(監修)
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
RSS
X:@WingsPro_info(公式)@WingsPro_info/wings(メンバーリスト)
Facebook

<著者について>
パソコン黎明期からプログラミングの進化を追い、Web・モバイル・IoT・AIまで多様な開発現場を駆け抜ける。
現在、株式会社気象工学研究所で気象×ITの最前線に立ちつつ、執筆コミュニティ『WINGSプロジェクト』のメンバーとして活動中。