前回は、匿名メソッドについて見ました。
今回は、C# でより頻繁に使われるラムダ式を整理します。
ラムダ式は、名前付きメソッドを作らずに、その場で処理を渡すための構文です。
匿名メソッドをより短く、読みやすく書ける構文と考えると理解しやすいです。
Action<string> printer = message => Console.WriteLine(message);
printer("Hello");
この message => Console.WriteLine(message) がラムダ式です。
ラムダ式とは
ラムダ式は、デリゲート型や式ツリーに変換できる匿名関数です。
まずは、名前付きメソッドを使う例を見てみます。
using System;
class Program
{
static void Main()
{
Action<string> printer = Print;
printer("Hello");
}
static void Print(string message)
{
Console.WriteLine(message);
}
}
この Print メソッドを、ラムダ式でその場に書けます。
using System;
class Program
{
static void Main()
{
Action<string> printer = message => Console.WriteLine(message);
printer("Hello");
}
}
実行結果です。
Hello
Action<string> は、string を 1 つ受け取り、戻り値がない処理を表すデリゲートです。
ラムダ式は、そのデリゲートに代入できる処理本体を書いています。
ラムダ式の構造を分解する
ラムダ式の基本形は、次のようになります。
引数 => 処理
たとえば、次のラムダ式を見てみます。
message => Console.WriteLine(message)
左側の message は引数です。
右側の Console.WriteLine(message) は実行する処理です。
中央の => は、左の引数を使って右の処理を実行することを表します。
=> はラムダ演算子と呼ばれます。
戻り値があるラムダ式も書けます。
Func<int, int> square = x => x * x;
Console.WriteLine(square(5));
実行結果です。
25
Func<int, int> は、int を受け取り、int を返すデリゲートです。
そのため、x => x * x は、引数 x を受け取って x * x を返す処理として扱われます。
複数ステートメントで引数を処理する
ラムダ式の右側には、1 つの式だけでなく、複数のステートメントを書けます。
複数行の処理を書く場合は、波かっこ { } を使います。
using System;
class Program
{
static void Main()
{
Action<string> logger = message =>
{
string timestamp = DateTime.Now.ToString("HH:mm:ss");
Console.WriteLine($"[{timestamp}] {message}");
};
logger("処理を開始しました");
}
}
実行結果の例です。
[09:30:00] 処理を開始しました
戻り値がある場合は、return を使います。
Func<int, int, int> calculateTotal = (price, quantity) =>
{
int subtotal = price * quantity;
int shippingFee = subtotal >= 3000 ? 0 : 500;
return subtotal + shippingFee;
};
Console.WriteLine(calculateTotal(1200, 2));
実行結果です。
2900
1 行で書ける単純な処理なら式形式、途中で変数を使ったり条件分岐したりするならブロック形式、と考えるとよいでしょう。
複数引数のラムダ式
引数が複数ある場合は、引数全体を丸かっこで囲みます。
Func<int, int, int> add = (x, y) => x + y;
Console.WriteLine(add(10, 20));
実行結果です。
30
型を明示して書くこともできます。
Func<int, int, int> add = (int x, int y) => x + y;
多くの場合、代入先のデリゲート型から引数の型を推論できるため、型は省略されます。
引数なしのラムダ式
引数がない場合は、空の丸かっこを書きます。
Action showTime = () =>
{
Console.WriteLine(DateTime.Now);
};
showTime();
戻り値がある場合も同じです。
Func<DateTime> getNow = () => DateTime.Now;
Console.WriteLine(getNow());
引数なしの場合、=> の左側を省略することはできません。
// これはできない
Func<DateTime> getNow = => DateTime.Now;
必ず () を書きます。
引数が 1 つの場合の省略形
引数が 1 つで、型を明示しない場合は、丸かっこを省略できます。
Func<int, bool> isEven = x => x % 2 == 0;
これは、次のように書いても同じです。
Func<int, bool> isEven = (x) => x % 2 == 0;
ただし、型を明示する場合は丸かっこが必要です。
Func<int, bool> isEven = (int x) => x % 2 == 0;
static ラムダ式を使う
C# 9.0 では、ラムダ式に static を付けられるようになりました。
Func<int, int> square = static x => x * x;
Console.WriteLine(square(5));
実行結果です。
25
static ラムダ式は、外側のローカル変数やインスタンスメンバーをキャプチャできません。
using System;
class Program
{
static void Main()
{
int factor = 2;
Func<int, int> multiply = static x => x * factor; // コンパイルエラー
}
}
factor は外側のローカル変数なので、static ラムダ式の中では使えません。
これは、意図しないキャプチャを防ぎたいときに役立ちます。
Func<int, bool> isPositive = static x => x > 0;
このように書くと、このラムダ式は外側の状態に依存しない処理であると明示できます。
ラムダ式で破棄パラメーターを使う
C# 9.0 では、ラムダ式の引数に破棄パラメーターを使いやすくなりました。
破棄パラメーターは、使わない引数を _ で受ける書き方です。
たとえば、イベントハンドラーでは sender やイベント引数を使わないことがあります。
button.Click += (_, _) =>
{
Console.WriteLine("クリックされました");
};
_ は「この引数は使わない」という意図を表します。
複数の引数をどちらも使わない場合でも、破棄としての _ は複数回使えます。
Func<int, int, int> constant = (_, _) => 42;
Console.WriteLine(constant(10, 20));
実行結果です。
42
引数の値は受け取っていますが、処理の中では使っていません。
使わない引数に x や unused のような名前を付けるより、_ を使った方が意図が明確になります。
匿名メソッドの例をラムダ式に置き換える
前回の匿名メソッドの例を、ラムダ式で書き直してみます。
匿名メソッドでは、次のように書いていました。
Action<string> printer = delegate (string message)
{
Console.WriteLine(message);
};
ラムダ式では、次のように書けます。
Action<string> printer = message =>
{
Console.WriteLine(message);
};
1 行で書けるなら、さらに短くできます。
Action<string> printer = message => Console.WriteLine(message);
イベント登録の例も見てみます。
using System;
class Worker
{
public event EventHandler? Completed;
public void Run()
{
Console.WriteLine("処理中...");
Completed?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
Worker worker = new Worker();
worker.Completed += (_, _) =>
{
Console.WriteLine("処理が完了しました");
};
worker.Run();
}
}
実行結果です。
処理中...
処理が完了しました
イベント引数を使わないため、(_, _) と書いています。
同じ処理は、static ラムダ式としても書けます。
worker.Completed += static (_, _) =>
{
Console.WriteLine("処理が完了しました");
};
外側のローカル変数を使わないイベントハンドラーでは、static を付けるとキャプチャしないことを明示できます。
実践例: 商品一覧をフィルターする
ラムダ式は、コレクション処理で特によく使われます。
次の例では、商品一覧から在庫があり、価格が 3000 円以下の商品だけを取り出します。
using System;
using System.Collections.Generic;
using System.Linq;
class Product
{
public string Name { get; }
public int Price { get; }
public bool InStock { get; }
public Product(string name, int price, bool inStock)
{
Name = name;
Price = price;
InStock = inStock;
}
}
class Program
{
static void Main()
{
List<Product> products = new List<Product>
{
new Product("ノート", 200, true),
new Product("キーボード", 4500, true),
new Product("マウス", 2500, true),
new Product("モニター", 24000, false)
};
List<Product> availableProducts = products
.Where(product => product.InStock && product.Price <= 3000)
.ToList();
foreach (Product product in availableProducts)
{
Console.WriteLine($"{product.Name}: {product.Price}円");
}
}
}
実行結果です。
ノート: 200円
マウス: 2500円
Where に渡している部分がラムダ式です。
product => product.InStock && product.Price <= 3000
これは、「商品を 1 つ受け取り、その商品を残すかどうかを bool で返す処理」です。
LINQ では、このように条件、変換、並べ替えなどの処理をラムダ式で渡す場面が多くあります。
実践例: 処理の一部を差し替える
ラムダ式は、処理の一部を外から差し替えたいときにも便利です。
次の例では、通知メッセージの作り方を Func<string, string> として渡しています。
using System;
class Notifier
{
public void Send(string userName, Func<string, string> createMessage)
{
string message = createMessage(userName);
Console.WriteLine($"送信: {message}");
}
}
class Program
{
static void Main()
{
Notifier notifier = new Notifier();
notifier.Send("Alice", name => $"{name} さん、注文が完了しました");
notifier.Send("Bob", name => $"{name} さん、発送が完了しました");
}
}
実行結果です。
送信: Alice さん、注文が完了しました
送信: Bob さん、発送が完了しました
Notifier は、メッセージをどのような文面にするかを知りません。
その部分をラムダ式として外から渡すことで、共通の処理と差し替え可能な処理を分離できます。
ラムダ式と式形式メンバー
ラムダ式の => と似た構文として、式形式メンバーがあります。
式形式メンバーは、メソッド、プロパティ、コンストラクター、デストラクターなどの本体を、短い式で書くための構文です。
たとえば、通常のメソッドは次のように書きます。
class Calculator
{
public int Add(int x, int y)
{
return x + y;
}
}
式形式メンバーを使うと、次のように書けます。
class Calculator
{
public int Add(int x, int y) => x + y;
}
プロパティでも使えます。
class User
{
public string FirstName { get; }
public string LastName { get; }
public User(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FullName => $"{FirstName} {LastName}";
}
C# 7.0 以降では、式形式メンバーとして書ける対象が広がりました。
たとえば、コンストラクターやファイナライザーにも使えます。
class Logger
{
private readonly string _category;
public Logger(string category) => _category = category;
~Logger() => Console.WriteLine($"{_category} を破棄します");
}
ただし、ラムダ式と式形式メンバーは同じものではありません。
ラムダ式は、デリゲートや式ツリーとして渡せる匿名関数です。
Func<int, int> square = x => x * x;
式形式メンバーは、クラスや構造体のメンバー本体を短く書くための構文です。
public int Square(int x) => x * x;
どちらも => を使いますが、使われる場所と意味が違います。
まとめ
ラムダ式は、処理をその場で書いてデリゲートや式ツリーに渡すための構文です。
- 基本形は
引数 => 処理 =>はラムダ演算子と呼ばれる- 1 行の式だけでなく、
{ }を使って複数ステートメントを書ける - 引数が複数ある場合は
(x, y) => ...と書く - 引数がない場合は
() => ...と書く - C# 9.0 では
staticラムダ式でキャプチャを禁止できる - C# 9.0 では、使わない引数を
_で表す破棄パラメーターを使いやすくなった - 匿名メソッドの多くはラムダ式でより簡潔に書ける
- 式形式メンバーも
=>を使うが、ラムダ式とは用途が異なる
ラムダ式を使えるようになると、イベント登録、LINQ、コールバック、条件式の差し替えなどを簡潔に書けます。
デリゲート、イベント、匿名メソッド、ラムダ式は、それぞれ別の構文に見えても、「処理を値として扱う」という同じ考え方でつながっています。