C# 初級講座
VB2019 Visual Studio 2022

第25回 ラムダ式

2022/12/25

この記事が対象とする製品・バージョン

Visual Studio 2022 Visual Studio 2022 対象です。
Visual Studio 2019 Visual Studio 2019 対象です。
Visual Studio 2017 Visual Studio 2017 対象外ですが、参考になります。
Visual Studio 2015 Visual Studio 2015 対象外ですが、参考になります。
Visual Studio 2013 Visual Studio 2013 対象外ですが、参考になります。
Visual Studio 2012 Visual Studio 2012 対象外ですが、参考になります。
Visual Studio 2010 Visual Studio 2010 対象外ですが、参考になります。
Visual Studio 2008 Visual Studio 2008 対象外ですが、参考になります。
Visual Studio 2005 Visual Studio 2005 対象外ですが、参考になります。
Visual Studio.NET 2003 Visual Studio.NET 2003 × 対象外です。
Visual Studio.NET 2002 Visual Studio.NET (2002) × 対象外です。
  Visual Studio Code 対象外ですが、参考になります。

 

目次

 

1.ラムダ式とは

ラムダ式とは、少しの不正確さに目をつむり、ざっくり言ってしまえば、名前のない関数(処理)です。

次のプログラムは 2 + 3 を実行するラムダ式を変数 Add に代入しています。


var add = () => 2 + 3;

 => は「ラムダ演算子」と呼びラムダ式の目印です。

 

これで変数 add は 2 + 3 を実行する関数(処理)になったので、この後、この変数を使って処理を呼び出せます。

var add = () => 2 + 3;

//result は 5 になります。
int result = add();

System.Diagnostics.Debug.WriteLine(result);

Debug.WriteLineで出力される場所

2 + 3 は 5 に決まっているので、このラムダ式は実用性は全然ありません。

処理を実行するには ( ) が必要なので add から処理を呼び出すときに空の ( ) がついている点に注意してください。

 

次のラムダ式は、翌日の日付を求めます。このラムダ式は変数 getTomorrow に代入しているので、この処理を実行して結果を受け取るにはgetTomorrowに ()を付けます。

var getTomorrow = () => DateTime.Now.AddDays(1);

DateTime tomorrow = getTomorrow();

System.Diagnostics.Debug.WriteLine($"明日は{tomorrow:M月d日}ですね。");

Debug.WriteLineで出力される場所

 

ラムダ演算子の前にある空の ( ) は、この処理の引数を表しています。空の場合は引数はないということです。

引数を使って、与えられた数値を +1 するラムダ式を書いてみましょう。

var plusOne = (int value) => value + 1;

int i1 = plusOne(100); //i1 は 101 になります。
int i2 = plusOne(i1); //i2 は 102 になります。

変数 plusOne はint型の引数を1つ必要とする関数(処理)になったので、この処理を呼び出すときは plusOne の後に ( ) を付けて、その中に数値型の引数を1つ渡すことになります。

 

() や => を使ったラムダ式の記述は一見奇妙に見えますが、ちょっと記述が変わっているだけでやっていることはメソッドと変わりないということがわかっていただけますでしょうか?

参考に単純な普通のメソッドの例との比較も紹介しておきましょう。MathクラスのPowメソッドは指定された数だけかけ算を実行します。たとえば、Math.Pow(3, 2)は3を2回かけ算するので9になります。Math.Pow(2, 4) は 2を4回かけ算するので16になるという具合です。

double result = Math.Pow(5,2);

System.Diagnostics.Debug.WriteLine($"5の2乗は{result}です。");

Debug.WriteLineで出力される場所

この例ではまだラムダ式は出てきていません。

第2引数を2に固定して、これと同じ機能のラムダ式を作ってみましょう。

var pow = (double x) => x * x;
double result = pow(5);

System.Diagnostics.Debug.WriteLine($"5の2乗は{result}です。");

Debug.WriteLineで出力される場所

2つを比べるとMath.Pow を ラムダ式で置き換えられることがわかります。シンプルにしたかったので第2引数は2に固定してしまいましたが、2つの引数をとって、完全にMath.Powを同じ機能にすることもできます。

ここで言いたかったのは、「ラムダ式は構文は奇妙ですが、メソッドと同様の機能を定義するものだ」ということです。

 

2.複数行のラムダ式

ラムダ式の処理は1行で書かなければいけないというわけではありません。 { } を使って複数行の処理を書くこともできます。{ } が付く以外にも値を返すときに return が必要になるという違いがあります。

次のラムダ式は1行だけです。よくサンプルで使う System.Diagnostics.Debug.WriteLineメソッドの処理を実行します。

var print = (object? value) => System.Diagnostics.Debug.WriteLine(value);

print("Hello!");
print("このメッセージは既定では出力ウィンドウのデバッグに表示されます。");

Debug.WriteLineで出力される場所

Debug.WriteLineメソッドの処理を 変数 print に代入したので、これ以降、変数printを使ってDebug.WriteLineを呼び出すことができます。すごい!

ラムダ式の引数が object? という型になっているのは、Debug.WriteLineメソッドの型に合わせたからです。

object 型なので、数値でも文字でもどんな型でも引数に指定することができるということです。 ? がついているので null の可能性がある値でも大丈夫ということです。つまり、object? で定義されているものにはどんなものでも渡せます。

 

これで、System.Diagnostics.Debug.WriteLine と長く書く代わりに print と短く書けるようになりました。短く書きたい場合には、一般的にラムダ式を使うよりもusingを使うのですが、今回はラムダ式を説明したいのでちょっと一般的ではない書き方をしています。

 

さて、このラムダ式に機能を追加しましょう。Debug.WriteLineだけではなく、Console.WriteLineも行うようにします。

次のように { } を使って2行の処理を1つのラムダ式にできます。

var print = (object? value) => {
    System.Diagnostics.Debug.WriteLine(value);
    Console.WriteLine(value);
};

print("Hello!");
print("このメッセージは既定では出力ウィンドウのデバッグに表示されます。");

Debug.WriteLineで出力される場所

すばらしい。これで print の処理を呼び出すだけで出力ウィンドウのデバッグとコンソールウィンドウの両方にメッセージを表示できるようになりました。{ } を使えば3行でも、100行でも処理を記述することができます。その中で if や while を使ってもよいし、他のメソッドや機能を呼び出すこともできます。C#でできることは何でもできます。

 

では、指定した数値を指定した回数だけかけ算する Math.Pow のラムダ式バージョンを作ってみましょう。

var pow = (double value, double count) =>
{
    double result = 1;
    for (int i = 0; i < count; i++)
    {
        result *= value;
    }
    return result;
};

double result = pow(5, 2);

System.Diagnostics.Debug.WriteLine($"5の2乗は{result}です。");

Debug.WriteLineで出力される場所

ラムダ式の中で for ループを使って指定した回数かけ算を繰り返すようにしました。これで Math.Pow と同じような機能です。

ただ、Math.Powの方は、回数の部分に小数を指定したり、マイナスを指定したりできるので、このラムダ式とはちょっと違うのですが…。

 

ここで注目してほしいのは値をラムダ式から値を返すときに return を使っていることです。

1行だけのラムダ式の時は値を返すときに return を記述する必要がありませんでした。

シンプルな例で比べてみましょう。

下記は2 + 3 を実行する1行形式のラムダ式の例です。

var add = () => 2 + 3;

int result = add();

System.Diagnostics.Debug.WriteLine(result);

Debug.WriteLineで出力される場所

これをあえて複数行形式で書くと { }; と return が増えます。

var add = () => {
    return 2 + 3;
};

int result = add();

System.Diagnostics.Debug.WriteLine(result);

Debug.WriteLineで出力される場所

 

3.Action

3-1.型がわかると使う場所がわかります

 C#はラムダ式の型を推論できるので、明示的に型を指定する必要はありません。これまでの例では var add = ... のように var を使ってラムダ式の型を推論させていました。

このようにしてラムダ式の型を推論させると、値を返さないラムダ式の型はAction...(読み方:Action=アクション) に、値を返すラムダ式の 型は Func... (読み方:Func=ファンク)になります。 ... で省略した部分にはラムダ式の定義のよって複数の型パラメーターが付きます。

 

ラムダ式の型がわかると、Visual Studioに表示されるヒントを見ただけで、ラムダ式が使える場所がわかり、どんなラムダ式が必要とされているかもわかります。

たとえば、何かのメソッドを呼び出そうとしたときにインテリセンスで引数の型が Func<bool, string> と表示されていれば、それはstring型の引数を1つとり、bool型の値を返すラムダ式が必要とされているという意味です。

この章では、ラムダ式の型の理解を深めてこういったヒントの意味がわかるようになることを目指します。

 

3-2.戻り値のないラムダ式の型

変数名の上にマウスをホバーさせると型名が表示されるので、確認してみましょう。

次の例は、コンソールにOKと出力するラムダ式を変数OKに代入しています。このラムダ式は値を返しません。

(下の画像ではマウスは映っていませんが)変数OKの上にマウスをホバーさせるとこの変数が Action? 型であることが確認できます。

? はこの変数には null も代入できるということを示しており、この場合は、型は Action と考えてよいです。

このラムダ式は型を明示すると次のように書き換えられるということです。私たちはOKにnullを代入するつもりがないので ? はとり除いてしまいます。

Action OK = () => Console.WriteLine("OK");

OK();

ちなみにこの例では Console.WriteLine を使っているので、実際にどうなるか試してみるにはコンソールアプリが楽です。

なぜ、私がいつものSystem.Diagnostics.Debug.WriteLineを採用しなかったかというと、記述が長くて説明用の画像やプログラムがコンパクトにならないからです。

ラムダ式に、引数を付けると型が変化します。stringの引数を1つとるように改造してみます。変数名もPrintに変更しました。

こうすると、型は Action<string> になります。

引数を2つにすると、それに応じて型パラメーターも増えます。次の例は引数を2つとるラムダ式です。引数を使っていないので意味はないのですが型が変わることは確認できます。

 

3-3.Actionの実践

これで、値を返さないラムダ式の型が表しているものが何かわかりましたね。

では、少し実践してみましょう。4つの要素を持つ List<string> を作ってみます。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

この要素のそれぞれに何かの処理を行いたいと思います。ファイルに出力するのでもよいし、メールで送信するのでも良いのですが、ここでは単純に それぞれの要素を System.Diagnostics.Debug.WriteLine で出力する処理をすることにしましょう。

Listには、それぞれの要素に対して処理を実行する ForEachメソッドがあります。

上記のプログラムに続けて、names.ForEach( と入力すると、次のようなインテリセンスが表示されます。

ForEachメソッドの引数の型が Action<string> になっていることが確認できます。

だから、要素に対して実行する処理は string を引数にとるラムダ式で指定するということが読み取れます。

つまり、次のような1行形式のラムダ式か、


(string xxxx) => ....

次のような複数行形式のラムダ式が期待されています。


(string xxxx) => {
    ....
    ....
};

ところで、このラムダ式のstring型の引数は一体なんでしょうか?これはインテリセンスのヒントから読み取れないかもしれませんが、ちょっと考えればわかります。Listの各要素の対して処理を実行するので、その処理は、対象の要素を取得する必要がありますよね。この引数はその対象の要素なのです。ForEachメソッドの中で、ここで指定したラムダ式が要素の数だけ呼び出され、引数にその要素を渡してくれるということです。

この例ではListをList<string>で定義しているので、要素の型が string になるため、このラムダ式も Action<string> となっています。ListをList<int>で定義する場合、ここで必要とされるラムダ式もAction<int>に変化します。

ということなので、次のように記述すると、各要素を出力することができます。1行形式のラムダ式でも書ける内容ですが、いろいろな書き方に慣れておいてほしいので複数行にしてみました。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

names.ForEach((string item) => {
    System.Diagnostics.Debug.WriteLine(item);
});

Debug.WriteLineで出力される場所

 

3-4.型推論

この場面ではラムダ式の引数の型推論が可能です。Visual Studio で ForEach( と 入力した時にインテリセンスで、Action<string> と表示されますね。プログラマーがstringと明示しなくてもVisual Studioはstringだとわかっているので、プログラマーはstringの指定を省略してVisual Studioに推論させることができます。

また、この例だとstring を取り除くと (item) となり、( ) の意味がないので ( ) も取り除けます。

次のようになります。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

names.ForEach(item => {
    System.Diagnostics.Debug.WriteLine(item);
});

 

なお、ラムダ式はあらかじめ変数に代入しておいてもOKなんですが、Visual Studioが型推論できるのはあくまで ForEach の引数なので、それとは別の場所で、ラムダ式をあらかじめ定義しておこうとするとVisual Studioは型推論ができないので自分で string と型指定する必要があります。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

var operation = (string item) => {
    System.Diagnostics.Debug.WriteLine(item);
};

names.ForEach(operation);

Debug.WriteLineで出力される場所

 

4.Func

4-1.戻り値のあるラムダ式の型

次は値を返すラムダ式の型を見てみましょう。

変数名の上でマウスをホバーさせる Func<T>? という型であると確認できます。T の部分を戻り値の型によって変わります。

下の例では、 戻り値が int なのでラムダ式の型は Func<int>? です。

Actionと同様に ? はこの変数に null を代入できることを示しているので、ラムダ式の型は Func<int> と考えることができます。

型を明示して書き換えると次のようになります。

Func<int> Add = () => 2 + 3;

int result = Add();

System.Diagnostics.Debug.WriteLine(result);

Debug.WriteLineで出力される場所

 

引数がある場合を見てみましょう。

次の例は文字列を引数にとって、その文字数を返すラムダ式です。

Actionと同様に ? はこの変数に null を代入できることを示しているので、ラムダ式の型は Func<int> と考えることができます。

型を明示して書き換えると次のようになります。

var getLength = (string value) => value.EnumerateRunes().Count();

int mojiCount = getLength("🗿です。");

System.Diagnostics.Debug.WriteLine(mojiCount + " 文字です。");

Debug.WriteLineで出力される場所

変数名 getLengthの上でマウスをホバーさせると型は Func<string, int>? と表示されます。

 

最初の型パラメーターが引数の型、最後の型パラメーターが戻り値の型というわけです。

 

引数が2つの例も見ておきましょう。次のラムダ式は意味はないのですが、int と long 引数をとり、string を返します。

var lambda = (int x, long y) => "これが戻り値";

string result = lambda(1, 2);

System.Diagnostics.Debug.WriteLine(result);

Debug.WriteLineで出力される場所

 

変数 lambda の上にマウスをホバーされると型が Func<int, long, string> であることがわかります。

型パラメーターはラムダ式の引数を順番に表し、最後の型パラメーターはラムダ式の戻り値の型を言うわけです。

 

4-2.Funcの実践

Funcの型がわかったところで、実例を体験しておきましょう。

さきほどと同じ題材です。4つの要素を持つ List<string> を作ってみます。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

この中で文字に a が含まれるものだけを抜粋したいと思います。Whereメソッド(読み方:Whereメソッド)を使うと、条件を指定して要素を抜粋できるので利用してみます。

names.Where( と入力するとインテリセンスには次のように表示されます。

このインテリセンスの内容ちょっと複雑なんですが、Whereメソッドの引数は1つで Func<string, bool>型であるということは読み取れると思います。

stringを引数にとり、boolを返すラムダ式をここで指定できるというわけです。

ここで指定するラムダ式は、実行時にListの要素を引数として受け取ります。そして、その要素を採用するか不採用にするか自分で判定処理を書いて、採用するなら true を返し、不採用なら false を返せということなんです。

 

だから、 a が含まれるものだけ抜粋するには次のように記述します。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

var results = names.Where(item =>
{
    if (item.Contains("a"))
    {
        return true;
    }
    else
    {
        return false;
    }
});

//results の内容を出力します。Banana と Cat が出力されます。
results.ToList().ForEach(item => System.Diagnostics.Debug.WriteLine(item));

Debug.WriteLineで出力される場所

Whereメソッドの引数の型はVisual Studioが推論できるので、ラムダ式のパラメーターは単に item と記述できます。あえて、型を明示する場合は(string item) => ... という記述にします。

 

4-3.いろいろな書き方

これでラムダ式の説明としては良いのですが、このプログラムは少し冗長です。プロはこういう書き方はしません。

item.Contains("a") 自身が true または false を表しているので、これが true なら true を返し、そうでなければ false を返すというこのプログラムは無駄があるのです。items.Contains("a") の戻り値をそのまま返せばよいので、次のように書けます。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

var results = names.Where(item =>
{
    return (item.Contains("a"));
});

//results の内容を出力します。Banana と Cat が出力されます。
results.ToList().ForEach(item => System.Diagnostics.Debug.WriteLine(item));

Debug.WriteLineで出力される場所

これだとラムダ式の内容が1行だけなので{ } を省いて1行形式のラムダ式にしてしまいましょう。1行形式にすると値を返すときの return は書かない点に注意してください。次のように書き換えられます。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

var results = names.Where(item => item.Contains("a"));

//results の内容を出力します。Banana と Cat が出力されます。
results.ToList().ForEach(item => System.Diagnostics.Debug.WriteLine(item));

Debug.WriteLineで出力される場所

私はこのくらいが好みですが、少数派かもしれません。

ここまで改造すると結果を1回変数 results に代入するのは無駄が多いと感じる人がたくさんいます。その場合、変数 results を経由せず次のようにメソッドを続けて呼び出します。

List<string> names = new List<string>();

names.Add("Apple");
names.Add("Banana");
names.Add("Cat");
names.Add("Dog");

names.Where(item => item.Contains("a"))
     .ToList()
     .ForEach(item => System.Diagnostics.Debug.WriteLine(item));

Debug.WriteLineで出力される場所

この書き方の場合、1行が長くなってしまうので、メソッド呼び出しのところで適宜改行するのが主流です。

変数を経由せずメソッドの戻り値に対して直接メソッドを呼び出して処理をつなげていく手法をメソッドチェーンと呼びます。

ラムダ式はメソッドチェーンと相性がよく、このようなプログラムもしばしば見かけます。

 

4-4.ラムダ式の型

以上で、ラムダ式の型としてAction と Func を説明しました。

私は「ラムダ式の型はActionとFuncです。」とは言っていない点はお気づきでしょうか?

私は var x = ... のようにラムダ式の型を推論させると Action か Func になると言っているだけです。ラムダ式の型としてはこれ以外のものもあり得ます。ただ、特に重要ではないのであまり知っておく必要はありません。

たとえば、Predicate<T>という型を使うと、引数が T で、戻り値が bool のラムダ式を扱えます。


Predicate<string> lambda = (string item) => true;

このプログラムを次のように型推論させると、Func<string, bool>になります。


var lambda = (string item) => true;

PredicateはActionやFuncが登場する前の一時期に使われていた型で、今後この型を使うものが増えることはないでしょう。

他にもdelegate(読み方:delegate=デリゲート)を使うと、自分でラムダ式を代入できる型を定義できます。だから、世の中にはラムダ式を代入できる型は無数にあります。delegateはあまり出番がないので初級講座では扱わない予定です。

 

5.ラムダ式内の変数の注意

今回は、ラムダ式の手始めに簡単な使い方を説明しています。ラムダ式を実現するために背後にはいろいろな技術が使われています。これらの説明は今回は省略しています。

ちゃんと理解しないでラムダ式を使うと思わぬ怪我をすることがありますので、1つだけ簡単に注意しておきます。

ラムダ式の外で定義されている変数をラムダ式の中で使うときは注意が必要です。

その変数はラムダ式に捕まえられる形になり(この状態を「キャプチャー」と呼びます)、その変数のスコープ終わっても、ラムダ式が生きている限り変数も本来のスコープを超えて生き続けます。

また、変数の値は、ラムダ式が実行されるときに評価されます。最初変数の値が 5 でもラムダ式を実行するときに変数が 10 になっていれば、ラムダ式はその変数を 10 と評価します。

int outerNumber = 5;

var printNumber = () => System.Diagnostics.Debug.WriteLine(outerNumber);

outerNumber = 10;

// 10 と出力されます。
printNumber();

Debug.WriteLineで出力される場所

 

6.練習問題

問1.Windows フォームアプリで メッセージボックスを表示する処理をラムダ式で定義して呼び出すことにしました。□ に当てはまるのはどれでしょうか?
var msgBox = (string message) □ MessageBox.Show(message, "情報", MessageBoxButtons.OK, MessageBoxIcon.Information);

msgBox("メッセージのテスト"); 
=
=>
=> {

 

問2.次のプログラムは5秒間の感覚を測定するプログラムですが、エラーになります。理由はなんでしょうか?
var printNow = () => Console.WriteLine(DateTime.Now.ToString("H:mm:ss.fff"));

printNow; 
Console.WriteLine("5秒経ったと思ったらEnterを押してください。");
Console.ReadLine(); //Enterが押されるまで何もしないで待機します。
printNow;
ラムダ式の後ろの ; が不要
ラムダ式に { } がない
printNowの呼び出しに () がない

問3.値を返す複数行形式のラムダ式について正しいものはどれでしょうか?すべて選択してください。

{ } が必要
=>が必要
returnが必要
型推論するとAction型になる(※1)

※1:引数と戻り値によって型パラメーターが複数付く場合があります。

 

問4.あるクラスからCountメソッドを呼び出そうとしたら、インテリセンスに次のように表示されました。このCountメソッドの引数に指定するラムダ式はどのようなものでしょうか?
引数がstringとboolの2つ
戻り値がstringで引数がbool
戻り値がboolで引数がstring

 

 

 

次の回は、タプルなどのデータ構造を扱います。

C#初級講座講座 次の回へ

 

改訂履歴

2022/12/25

  • 初版