はじめに
第1回ではif文地獄をインターフェースで、第2回では画面凍結をasync/awaitで、それぞれ攻略してきました。連載最終回となる第3回のテーマは、Action/Funcの「デリゲート沼」です。
Action
今回は、新人プログラマーの相談から始まります。あなたが先輩役として、AIと一緒に、この沼から助け出してあげましょう。
「これって何のためにあるんですか?」と聞かれて
ある日、新人さんが入門書を抱えて聞いてきました。「デリゲートのところで、Action
そのAction、Funcについて、さっそくAIに聞いてみましょう。
C#のAction<T>とFunc<T, TResult>って何のためにあるんですか? 違いの前に、両方の根っこにあるデリゲートとは何かを、新人プログラマーにもわかるよう簡潔に教えてください。
ChatGPTの回答を要約すると、次のとおりです。
まず大事なのは、「メソッド(処理)を、値みたいに扱える」という考え方です。
普通、変数には数字や文字列を入れますよね。
でもC#では、「あとで実行する処理そのもの」も変数に入れられます。
そのしくみが「デリゲート」です。
Action と Func は、「よく使うデリゲートを、簡単に書けるようにしたもの」です。
どちらも、「メソッドを、あとで呼び出せる値として扱う」ためのしくみです。
モヤモヤの本丸はここですね。ActionとFuncの違いは、共通点をつかんだ後でゆっくり覚えればよさそうです。
「メソッドを、値みたいに扱う」をたとえる
言葉では理解できても、新人さんはまだピンと来ない様子です。ここでもAIに、身近なたとえを引き出してもらいましょう。
C#の「メソッド(処理)を、値みたいに扱える」という感覚を、新人プログラマーにもピンとくる身近で日常的なたとえに置き換えてもらえますか? いくつか候補をあげて、その中からいちばんぴったりなものを選んでください。
ChatGPTは、次のように整理してくれました(適宜要約しています)。
候補として「リモコンのボタン」「店員への依頼メモ」「スマホのホーム画面のショートカット」などがありますが、いちばんC#のデリゲートに近いのは「スマホのショートカットアイコン」だと思います。
このたとえの強いところは、
「処理を持ち運べる」「後で差し替えられる」「同じ呼び出し方で別動作になる」「実行はタップに近い」
という、デリゲートの核心がかなり自然に表現できる点です。
ショートカットなら、新人さんも毎日触っていますね。「メソッドを変数に?」は、「ショートカットをホーム画面のアイコン枠に保存する」と捉えればすっきりです。
図解を依頼する
このたとえで図解してもらいました。今回は、図中テキストの精度を比較して、ChatGPTを採用しています。
スマホのショートカットのたとえを使って、C#のデリゲート(Action/Func)の概念を、シンプルなイラストで図解してください。
アイコンの中身(やること)は外から見えませんが、タップすれば登録した処理が走る――これが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;――これが、まさにメソッドを変数にした瞬間ですね。judge(apple) と書くと、変数に入っている処理が呼び出されます。ショートカットのたとえと並べると、こうなります。
- Func
judge = ホーム画面のアイコン枠 - judge = IsCheap; = 枠に「IsCheap」というショートカットを置く
- judge(apple) = アイコンをタップして実行する
- judge = IsOutOfStock; = 同じ枠に別のショートカットを入れ替える
AIのコードの、judge = IsCheap;のところ、メソッドを丸ごと代入しているようにも見えますが、厳密にはメソッド本体そのものではなく、IsCheapを呼び出すための参照です。ショートカットのアイコンも、処理本体ではなく本体への呼び出し口でしたね。Action/Funcの変数も、メソッド本体ではなくその参照を持つ箱、と押さえておけば十分です。
「メソッドを変数に?」の正体は、このたった数行に詰まっています。コードとたとえを橋渡しする対応図を描いてもらいましょう。
次のC#のコードとスマホのホーム画面の「アイコン枠」のたとえを、左右に並べた1枚の対応図として描いてください。
(さきほどの4行の箇条書きそのまま入力)
コードの字面だけ見ると硬く感じる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
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
ただし、==のつもりで=を書くミスは要注意です。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(メンバーリスト)<著者について>
パソコン黎明期からプログラミングの進化を追い、Web・モバイル・IoT・AIまで多様な開発現場を駆け抜ける。
現在、株式会社気象工学研究所で気象×ITの最前線に立ちつつ、執筆コミュニティ『WINGSプロジェクト』のメンバーとして活動中。

