今回は「デリゲートとラムダ式」について解説します。
デリゲートはメソッド(関数)を部品化して再利用可能にするC#の機能です。ラムダ式は簡潔にメソッドを定義できる構文です。
以下がデリゲートとラムダ式の簡単な例です。
using System;
// Func<int, int>はデリゲート型。ラムダ式でメソッドを定義してデリゲートを作成
Func<int, int> multiply2 = x => x * 2;
// multiply2が参照するデリゲートのインスタンス(メソッド)を呼び出し、結果を保存
int result = multiply2(5);
// 結果をコンソールに出力
Console.WriteLine(result); //10と表示
新しく出てきた用語・概念を簡単に整理すると以下のような感じです。
うーん、この例だとデリゲートとラムダ式を使うと何が嬉しいのかわからないなぁ。
デリゲートとラムダ式を使うと、「大きなメソッド部品」の内部で使われる「小さなメソッド部品」を入れ替えることを簡単に実現できるのです。
以下がイメージです。
オブジェクト指向における「クラス」はプログラミングにおける部品化・再利用のための仕組みでしたね。
「デリゲートとラムダ式」も同様に部品化・再利用を効率よく行うための仕組みの1つです。
本記事ではこれまでのレストランメニュー表生成アプリを拡張し、メニューの並び替えとフィルタリングを行う例を通して、デリゲートとラムダ式の活用方法を学びます。
デリゲートとラムダ式は実践的なアプリ開発でも以下の点で非常に重要です。
- LINQ(統合クエリ言語)における活用
- Webアプリ等のGUIアプリにおけるイベント処理機構の実現
- 非同期処理における活用
デリゲートとラムダ式はあらゆるところで使われる重要な機能です。
これらによって、プログラムの部品化・再利用をどのように効率よく行うかを一緒に学んでいきましょう!
演習のコード一式はGitHubに置いてあります。
YouTubeの動画も作成しています。
講義1:デリゲートとラムダ式の基本
演習で実際に使う前に、デリゲートとラムダ式の基本について簡単に解説します。
以下のような、コンソールからの入力値を「2倍する」/「2乗する」の2パターンで処理を切り替え可能なコードを例として説明します。
using System;
// デリゲート: 整数を受け取り整数を返すメソッドの型を定義
Func<int, int> multiply2 = x => x * 2; // ラムダ式: 引数xを2倍にするメソッドを定義
Func<int, int> square = x => x * x; // ラムダ式: 引数xを2乗するメソッドを定義
Console.WriteLine("数値を入力してください:");
int input = int.Parse(Console.ReadLine());
Console.WriteLine("操作を選択してください (1: 2倍, 2: 2乗):");
int choice = int.Parse(Console.ReadLine());
Func<int, int> operation = choice == 1 ? multiply2 : square;
// 選択したラムダ式を使用
int result = operation(input);
Console.WriteLine($"結果: {result}");
「引数を2倍して返すメソッド」と「引数を2乗して返すメソッド」をそれぞれメソッド部品として定義し、それらを切り替えて使えるようにしているというイメージです。
デリゲート
デリゲート型はメソッドへの参照を保持する型で、デリゲートのインスタンスを代入することができます。
戻り値をもつ場合はFunc型、もたない場合はAction型を使い、以下のように定義・使用します。
Func<第1引数の型, 第2引数の型, … , 第n引数の型, 戻り値の型> func;
Action<第1引数の型, 第2引数の型, … , 第n引数の型> action;
上述した例のFunc<int,int>はint型引数を1つもち、int型の戻り値をもつメソッドを参照するためのデリゲート型です。
2倍にするメソッド(multiply2)、2乗するメソッド(square)のどちらもFunc<int,int>型へ代入できます。
FuncやActionはジェネリック型なので、あらゆる引数・戻り値の型をもつメソッドへ対応可能です。ジェネリック型については以下の記事も参考にしてください。
デリゲートについてはMicrosoftの「デリゲート (C# プログラミング ガイド)」も参考にしてください。
ラムダ式
ラムダ式を使うと、簡潔な構文でメソッドを定義し、デリゲートのインスタンスを作成できます。
引数1 => 式 //式で記述 (引数が1つ)
(引数1、引数2、…引数n) => 式 //式で記述 (引数が複数)
引数1 => {...} //ステートメントで記述 (引数が1つ)
(引数1、引数2、…引数n) => {...} //ステートメントで記述 (引数が複数)
上述した例では「x => x * 2」や「x => x * x」として、それぞれメソッドを定義しています。
ラムダ式について、詳しくはMicrosoftのラムダ式と匿名関数の記事も参考にしてください。
デリゲートやラムダ式関連は、C#言語進化の歴史的な経緯もあり、他にも様々な記法や関連する仕組みが存在します。
まずは今回紹介した基本を押さえておき、そこから必要に応じて知識を広げていくとよいでしょう。
演習:メニューのフィルタリングと並び替え ~デリゲートとラムダ式を使う~
作る機能
元となるコードは入門編(14)の演習コードです。これまで作ってきたレストランメニュー表生成プログラムです。
このコードでは、CSVファイルでレストランメニュー一覧を読み込み、HTML形式のメニュー表を出力します。
詳しくは、入門編(14)の記事を参考にしてください。
今回はこのプログラムへ以下の機能を追加してみましょう。
- メニューが価格の安い順番に並んだメニュー表を生成する。
- ベジタリアン向けのメニュー表も生成する。
元のコードは以下の構成になっています。
今回、デリゲートとラムダ式を活用し、Programクラスを修正して①、➁の機能を追加します。
演習コード
修正したProgram.csは以下になります。
using Restaurant.Generators;
using Restaurant.Menus;
using Restaurant.Readers;
namespace Restaurant
{
internal class Program
{
static void Main(string[] args)
{
try
{
// CSVからメニュー一覧を読み込む
string inputCsvFilePath = "menu.csv";
var csvReader = new CsvMenuReader(inputCsvFilePath);
List<Menu> menus = csvReader.ReadMenus();
// 0. 通常のメニュー表を出力
GenerateHtmlFile(menus, "menu.html");
// ★(a) 1. 価格の安い順にメニューを並べ替えて出力
var sortedMenus = menus.OrderBy(menu => menu.Price).ToList();
GenerateHtmlFile(sortedMenus, "sorted_menu.html");
// ★(b) 2. ベジタリアン向けの主菜メニューを抽出して出力
var vegetarianMenus = menus.Where(menu => menu is MainMenu mainMenu && mainMenu.IsVegetarian).ToList();
GenerateHtmlFile(vegetarianMenus, "vegetarian_menu.html");
}
//…例外対応部分は省略…
}
static void GenerateHtmlFile(List<Menu> menus, string fileName)
{
// メニュー一覧をHTMLコードとして生成する
MenuTableGenerator generator = new MenuTableGenerator(menus.ToArray());
string tableHtml = generator.GenerateTable();
// メニュー表をHTMLページとして生成する
MenuPageGenerator htmlGenerator = new MenuPageGenerator(tableHtml);
string html = htmlGenerator.GenerateHtml();
// 生成されたHTMLをファイルへ出力する
File.WriteAllText(fileName, html);
}
}
}
(a),(b)でそれぞれデリゲートとラムダ式を活用して、メニューの並べ替えやフィルタリングを行っています。
これらの部分にフォーカスして、詳しくコードをみていきましょう。
並べ替え
コードの(a)の部分をみてみましょう。
var sortedMenus = menus.OrderBy(menu => menu.Price).ToList();
OrderByメソッドはコレクションの要素を何かしらの値に基づき昇順に並べ替えるメソッドです。(降順に並べ替えるOrderByDescendingメソッドもあります)
OrderByは以下のようなイメージで処理を行っています。
ここではメニューを価格が安い順(int型であるPriceフィールドの値が小さい順)に並び替えています。
ここで、OrderByは「Func<Menu,int> keySelector」という「何を基準として並べ替えるか?」を指定するための引数を持ちます。
「menu => menu.Price」はメニュー(コレクションの各要素)を引数としてメニューの価格を戻り値とするラムダ式ですね。
正確には、OrderByはジェネリック型のメソッドなので、「Func <TSource,TKey> keySelector」を引数にとります。
TSourceはコレクションの要素の型で、TKeyは並び替えに使う値の型です。
これによって、様々な型の要素について、様々な値を用いて並び替えを行うことができるのですね。
ジェネリック型のデリゲート型…なんか頭が混乱する。
ここは慣れるまで難しいところですね。
ジェネリック型、デリゲート型のおかげでOrderByは「あらゆる型をどのような基準でも並べ替えられる」とても汎用的な部品になっているのです。
以下のように、OrderByという1つのメソッドを「様々な型」x「様々な比較方法」でカスタマイズ可能なのです。
デリゲートとラムダ式(そして、ジェネリック型)を使うと、基本的な要素部品を用意しておけば、それらを組み合わせることで柔軟にやりたいことを実現できるのですね。
フィルタリング
次に、ベジタリアン向けの主菜メニューを抽出処理(コードの(b)部分)をみてみましょう。
var vegetarianMenus = menus.Where(menu => menu is MainMenu mainMenu && mainMenu.IsVegetarian).ToList()
ここでは「メニューが主菜(MainMenu型)かつベジタリアン向け(IsVegetarianがtrue)」であるメニューを抽出して新しいリストを作っています。
Whereは「Func<Menu, bool> predicate」という「何を満たす要素を抽出するか?」を指定するための「条件」となるデリゲート型の引数を持ちます。
「menu => menu is MainMenu mainMenu && mainMenu.IsVegetarian」というラムダ式で「条件」を定義して与えています。
WhereメソッドもOrderByメソッドと同様、ジェネリック型であり様々な型と抽出条件でカスタマイズできるようになっています!
プログラムを実行
以下のCSVファイルを用意を入力として、プログラムを実行してみましょう。
Type,Name,Description,Price,IsVegetarian,IsCold
Main,黒毛和牛ステーキ,ジューシーで柔らかなステーキです。,3000,false,
Main,ベジタブルカレー,野菜をたっぷりと使った、スパイシーなカレーです。,2400,true,
Main,グリルチキン,香ばしく焼き上げたジューシーなチキンです。,1800,false,
Main,ベジタリアンパスタ,新鮮な野菜とハーブを使用したパスタです。,1500,true,
Drink,ホットコーヒー,丁寧に焙煎されたコーヒー豆を使用しています。,500,,false
Drink,メロンソーダ,爽やかな甘さが楽しめるメロンソーダです。,400,,true
以下のようなHTMLが出力されます。
並べかえ、フィルタリング機能を実装できたよ!
あらかじめ用意されているOderByやWhereというメソッドを、デリゲートとラムダ式という仕組みでカスタマイズし、簡単に実装できました!
講義2:デリゲートとラムダ式について補足
デリゲートとラムダ式の記法や、代表的なユースケースについて補足をします。
進化の歴史と記法
C#でデリゲートを使ってメソッドを部品として扱う方法は、以下のようにC#のバージョンアップに伴って進化してきました。
square1、square2、square3は全て同じ処理を行うメソッドを定義しています。
using System;
class Program
{
// 1. C# 1.0 (2002): 従来のデリゲート
delegate int SquareDelegate(int x);
static int Square(int x) { return x * x; }
static void Main(string[] args)
{
// C# 1.0: 従来のデリゲート
SquareDelegate square1 = new SquareDelegate(Square);
Console.WriteLine($"C# 1.0: {square1(5)}");
// 2. C# 2.0 (2005年): 匿名メソッド
SquareDelegate square2 = delegate(int x) { return x * x; };
Console.WriteLine($"C# 2.0: {square2(5)}");
// 3. C# 3.0 (2007年): ラムダ式とFunc
Func<int, int> square3 = x => x * x;
Console.WriteLine($"C# 3.0: {square3(5)}");
}
}
ものすごくざっくり説明すると、以下のように進化しました。
- C#1.0:クラスで定義されたメソッドを、部品として扱えるようにした
- C#2.0:メソッド部品を使う場所で定義できるようにした(匿名メソッド)
- C#3.0:匿名メソッドを簡潔に書けるようにした(ラムダ式)
(このとき、汎用的なデリゲート型であるFunc、Actionも導入されました)
「より簡潔に書ける」よう進化しているのがわかりますね。
講義1、演習で使っていたのはC#3.0相当で、モダンなC#開発で最も使われるものです。
基本的に最新の記法を使えばよいと思いますが、過去の記法で書かれた既存コードもあるので、知識としては知っておいた方がよいでしょう。
代表的なユースケース(イベント処理)
デリゲートとラムダ式の代表的なユースケースは、冒頭でも説明した通り以下があります。
(再掲します)
- LINQ(統合クエリ言語)における活用
- Webアプリ等のGUIアプリにおけるイベント処理機構の実現
- 非同期処理における活用
①は演習の題材としても使いました。
②イベント処理の例として、C# BlazorによるWebアプリコードの一部分を示します。
(応用的な内容になるので理解できなくて大丈夫です。雰囲気だけ感じてください)
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<!--ラムダ式を使用してボタンクリック時のイベントハンドラを直接定義-->
<button class="btn btn-primary" @onclick="@(() => currentCount++)">Click me</button>
@code {
private int currentCount = 0;
}
ちなみに実行した画面は以下のようになります。
Blazor独自の記法で少しわかりにくいですが、「() => currentCount++」というラムダ式を、ボタンクリック時に実行されるメソッドとして設定しています。
大きな部品(イベント処理の機構)に、小さな部品(ボタンクリック時の動作)を入れて動作をカスタマイズしてるってことだね!
その通りです!
実践的なアプリ開発においてデリゲートやラムダ式はあたりまえのように使われるため、その基本を押さえておくのは重要ということですね。
C# BlazorによるWebアプリ開発について興味がある方はWebアプリ開発編もぜひご覧ください。
まとめ
今回は、メソッドの部品化・再利用を行う仕組みであるデリゲートとラムダ式について学びました。
デリゲートを使うとメソッドを変数に代入したり、引数として渡したりとできます。
ラムダ式を使うと、部品となるメソッドを簡潔に定義できます。
これらを活用することで、レストランメニューの並び替えやフィルタリングといった処理を簡単に作ることができました。
デリゲートとラムダ式は、LINQ・イベント処理・非同期処理など、C#プログラミングの様々な場面で活用される重要な概念です。ぜひ使いこなせるようになりましょう!
引き続き、C#やプログラミングの考え方を一緒に学んでいきましょう!