変数

【C#】変数キャプチャから非同期までラムダ式の基本と応用テクニック

C#のラムダ式は、=>で引数と処理を結び付ける匿名関数で、FuncActionなどのデリゲート型に代入して扱います。

外側の変数をキャプチャして保持でき、式形式なら戻り値を簡潔に書けます。

型推論が働くため記述量が減り、LINQやイベント処理で特に活躍します。

目次から探す
  1. ラムダ式の基礎構文
  2. デリゲートとラムダ式の関係
  3. 変数キャプチャの仕組み
  4. キャプチャを利用した実用パターン
  5. ラムダ式と例外処理
  6. LINQ におけるラムダ式
  7. 型推論とラムダ式
  8. 式ツリーへの変換
  9. 非同期ラムダ式の活用
  10. パフォーマンスとメモリ管理
  11. デバッグと診断
  12. ローカル関数との比較
  13. 属性とラムダ式
  14. よくある落とし穴と対処法
  15. まとめ

ラムダ式の基礎構文

C#におけるラムダ式は、匿名関数を簡潔に表現するための構文です。

主にデリゲートやLINQクエリで使われ、コードの可読性や保守性を高める役割を果たします。

ここでは、ラムダ式の基本的な書き方や構成要素について詳しく解説いたします。

矢印演算子 => の役割

ラムダ式の中心的な構文要素は、矢印演算子 => です。

この演算子は、左側に引数リスト、右側に関数本体を記述するための区切りとして機能します。

例えば、以下のように書きます。

x => x * x

この例では、x が引数で、x * x が戻り値となる式です。

=> は「引数を受け取って、右側の処理を実行する」という意味合いを持ちます。

矢印演算子は、匿名関数の引数と処理を明確に分ける役割を果たし、コードの簡潔さを実現しています。

式形式ラムダ

式形式ラムダは、関数本体が単一の式で構成されている場合に使います。

式の評価結果がそのまま戻り値となるため、return キーワードや波括弧 {} を省略できます。

単一式での戻り値

式形式ラムダは、1つの式だけで処理を完結させる場合に便利です。

例えば、数値の二乗を計算するラムダ式は以下のように書けます。

Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 出力: 25

この例では、引数 x を受け取り、x * x の計算結果を返しています。

return を書かずに済むため、コードが非常にシンプルになります。

省略可能な型と括弧

引数の型は、コンパイラが文脈から推論できる場合、省略可能です。

また、引数が1つだけの場合は、括弧も省略できます。

例えば、以下の2つのラムダ式は同じ意味です。

Func<int, int> square1 = (int x) => x * x;
Func<int, int> square2 = x => x * x;

また、引数が複数ある場合は括弧が必要です。

Func<int, int, int> add = (x, y) => x + y;

このように、型や括弧の省略により、コードがより簡潔に書けるのが式形式ラムダの特徴です。

ステートメント形式ラムダ

ステートメント形式ラムダは、関数本体が複数の文(ステートメント)で構成される場合に使います。

この場合は、波括弧 {} で処理を囲み、必要に応じて return を明示的に書きます。

ループや条件分岐の記述

複雑な処理や複数のステートメントを含む場合は、ステートメント形式ラムダを使います。

例えば、引数の値に応じてメッセージを表示するラムダ式は以下のように書けます。

Action<int> checkNumber = x =>
{
    if (x > 0)
    {
        Console.WriteLine("正の数です。");
    }
    else if (x < 0)
    {
        Console.WriteLine("負の数です。");
    }
    else
    {
        Console.WriteLine("ゼロです。");
    }
};
checkNumber(10);  // 出力: 正の数です。
checkNumber(-5);  // 出力: 負の数です。
checkNumber(0);   // 出力: ゼロです。

この例では、if文を使った条件分岐を含む複数のステートメントを波括弧で囲み、処理をまとめています。

返り値を持たない場合

戻り値が不要な場合は、Action デリゲートを使い、ステートメント形式ラムダで処理を記述します。

例えば、文字列を受け取ってコンソールに表示するラムダ式は以下のように書けます。

Action<string> greet = name =>
{
    string greeting = $"こんにちは、{name}さん!";
    Console.WriteLine(greeting);
};
greet("太郎");  // 出力: こんにちは、太郎さん!

このように、複数の処理をまとめて書きたい場合や、戻り値が不要な場合にステートメント形式ラムダは役立ちます。

以上が、C#のラムダ式の基礎構文に関する解説です。

矢印演算子 => を使った簡潔な記述方法から、式形式とステートメント形式の使い分けまで理解いただけたかと思います。

デリゲートとラムダ式の関係

C#のラムダ式は、デリゲート型に変換されて利用されることが多いです。

デリゲートはメソッドの参照を保持する型で、ラムダ式はその匿名メソッドを簡潔に記述する手段として機能します。

ここでは、代表的なジェネリックデリゲートであるFuncActionの違い、ジェネリックデリゲートへの割り当て方法、そしてカスタムデリゲートへの適用方法について解説します。

Func と Action の違い

FuncActionは、C#でよく使われるジェネリックデリゲートの型です。

どちらもメソッドの参照を保持しますが、戻り値の有無で使い分けます。

  • Func

戻り値を持つメソッドを表します。

最後の型パラメーターが戻り値の型で、それ以前の型パラメーターが引数の型を表します。

例: Func<int, int, int> は、2つのint引数を受け取り、intを返すメソッドを表します。

  • Action

戻り値を持たないメソッドを表します。

型パラメーターは引数の型のみで、戻り値はvoidです。

例: Action<string> は、1つのstring引数を受け取り、戻り値がないメソッドを表します。

以下のサンプルコードで違いを確認しましょう。

using System;
class Program
{
    static void Main()
    {
        // Func: 2つのintを受け取り、intを返す
        Func<int, int, int> add = (x, y) => x + y;
        Console.WriteLine(add(3, 4)); // 出力: 7
        // Action: stringを受け取り、戻り値なし
        Action<string> greet = name => Console.WriteLine($"こんにちは、{name}さん!");
        greet("花子"); // 出力: こんにちは、花子さん!
    }
}
7
こんにちは、花子さん!

このように、戻り値が必要な場合はFunc、不要な場合はActionを使います。

ジェネリックデリゲートへの割り当て

ラムダ式は、対応するデリゲート型に割り当てることで利用できます。

ジェネリックデリゲートは型パラメーターを持つため、引数や戻り値の型に応じて適切な型を選択します。

例えば、引数が3つで戻り値がある場合はFunc<T1, T2, T3, TResult>を使います。

戻り値がない場合はAction<T1, T2, T3>となります。

以下は、3つの引数を受け取り計算を行うラムダ式の例です。

using System;
class Program
{
    static void Main()
    {
        // 3つのintを受け取り、合計を返すFunc
        Func<int, int, int, int> sum = (a, b, c) => a + b + c;
        Console.WriteLine(sum(1, 2, 3)); // 出力: 6
        // 3つのstringを受け取り、連結して表示するAction
        Action<string, string, string> printConcat = (s1, s2, s3) =>
        {
            string result = s1 + s2 + s3;
            Console.WriteLine(result);
        };
        printConcat("C#", "の", "世界"); // 出力: C#の世界
    }
}
6
C#の世界

このように、引数の数や戻り値の有無に応じて、FuncActionの型パラメーターを適切に指定して割り当てます。

カスタムデリゲート定義への適用

C#では、独自にデリゲート型を定義してラムダ式を割り当てることも可能です。

カスタムデリゲートは、特定のシグネチャを持つメソッド参照を表現したい場合に使います。

以下は、戻り値がboolで引数がstringのカスタムデリゲートを定義し、ラムダ式を割り当てる例です。

using System;
delegate bool StringPredicate(string input);
class Program
{
    static void Main()
    {
        // カスタムデリゲートにラムダ式を割り当て
        StringPredicate isLongerThan5 = s => s.Length > 5;
        Console.WriteLine(isLongerThan5("こんにちは")); // 出力: True
        Console.WriteLine(isLongerThan5("猫"));       // 出力: False
    }
}
True
False

この例では、StringPredicateというデリゲート型を定義し、stringを受け取りboolを返すラムダ式を割り当てています。

カスタムデリゲートは、FuncActionで表現しにくい特定のシグネチャを明示的に示したい場合に有効です。

また、イベントハンドラーなどで特定のデリゲート型が要求される場合も、ラムダ式をカスタムデリゲートに割り当てて利用します。

このように、ラムダ式はFuncActionなどのジェネリックデリゲート、さらにはカスタムデリゲートに柔軟に割り当てられます。

用途や戻り値の有無に応じて適切なデリゲート型を選択し、コードの簡潔化と可読性向上に役立ててください。

変数キャプチャの仕組み

ラムダ式は、定義されたスコープの外にある変数を参照できる特徴があります。

これを「変数キャプチャ」と呼び、内部的にはクロージャという仕組みで実現されています。

ここでは、クロージャの動作やキャプチャされた変数のライフタイム、foreachループでの注意点、そして静的ローカル変数との違いについて詳しく説明します。

スコープ外変数を保持するクロージャ

ラムダ式がスコープ外の変数を参照すると、その変数はクロージャとして保持されます。

クロージャとは、関数とその関数が参照する変数の組み合わせを指します。

これにより、ラムダ式が定義されたスコープが終了しても、キャプチャされた変数は生き続けます。

以下の例をご覧ください。

using System;
class Program
{
    static void Main()
    {
        int factor = 3;
        Func<int, int> multiply = x => x * factor;
        Console.WriteLine(multiply(5)); // 出力: 15
        factor = 5;
        Console.WriteLine(multiply(5)); // 出力: 25
    }
}
15
25

この例では、factorという変数をラムダ式がキャプチャしています。

multiplyを呼び出すたびに、factorの現在の値が使われるため、factorを変更すると結果も変わります。

これは、factorがクロージャとして保持されているためです。

キャプチャされた変数のライフタイム

通常、ローカル変数はそのスコープを抜けると破棄されますが、ラムダ式が変数をキャプチャすると、その変数のライフタイムはラムダ式のデリゲートが生きている間延長されます。

つまり、クロージャが変数の実体をヒープ上に保持し、ガベージコレクションの対象になるまで変数は存在し続けます。

この仕組みにより、非同期処理やイベントハンドラーなどでスコープ外の変数を安全に利用できます。

foreach ループでの注意点

ループ変数の再利用問題

foreachループ内でラムダ式がループ変数をキャプチャすると、意図しない動作になることがあります。

これは、ループ変数がループの各反復で再利用されるためです。

以下のコードを見てください。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var actions = new List<Action>();
        foreach (var i in new[] { 1, 2, 3 })
        {
            actions.Add(() => Console.WriteLine(i));
        }
        foreach (var action in actions)
        {
            action();
        }
    }
}
3
3
3

期待としては1, 2, 3が順に出力されるはずですが、実際はすべて3が出力されます。

これは、iがループ全体で1つの変数として使われており、最後の値3がキャプチャされているためです。

C# 5 以降の挙動の変更

C# 5以降では、この問題を回避するために、foreachループのループ変数が各反復ごとに新しい変数として扱われるようになりました。

つまり、上記のコードはC# 5以降のコンパイラでコンパイルすると、期待通りに1, 2, 3が出力されます。

ただし、forループのループ変数は依然として再利用されるため、同様の問題が発生することがあります。

forループでのキャプチャには注意が必要です。

以下はforループでの例です。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var actions = new List<Action>();
        for (int i = 0; i < 3; i++)
        {
            actions.Add(() => Console.WriteLine(i));
        }
        foreach (var action in actions)
        {
            action();
        }
    }
}
3
3
3

この場合は、iがループ終了後の値3でキャプチャされてしまいます。

回避策としては、ループ内で新しい変数に代入してからキャプチャする方法があります。

for (int i = 0; i < 3; i++)
{
    int temp = i;
    actions.Add(() => Console.WriteLine(temp));
}

このようにすることで、期待通り0, 1, 2が出力されます。

静的ローカル変数との違い

C# 8.0以降では、ローカル関数や変数にstatic修飾子を付けることができます。

staticローカル関数や変数は、外部の変数をキャプチャできません。

これにより、クロージャの生成を防ぎ、パフォーマンスの最適化に役立ちます。

例えば、以下のコードはコンパイルエラーになります。

static Func<int, int> CreateMultiplier()
{
    int factor = 2;
    // staticラムダは外部変数をキャプチャできないためエラー
    return static x => x * factor;
}

staticラムダ式は、外部変数に依存しない純粋な関数として扱われるため、キャプチャによるヒープ割り当てが発生しません。

キャプチャが不要な場合はstaticを使うことで効率的なコードが書けます。

このように、ラムダ式の変数キャプチャは便利な機能ですが、ループ変数の扱いやstatic修飾子の利用など、注意すべきポイントもあります。

適切に理解して使い分けることが重要です。

キャプチャを利用した実用パターン

ラムダ式の変数キャプチャ機能は、単に外部変数を参照するだけでなく、状態を保持しながら関数を生成したり、特定の設計パターンを実現したりするのに役立ちます。

ここでは、キャプチャを活用した代表的な実用パターンを3つ紹介します。

インクリメント関数の生成

変数キャプチャを利用して、状態を持つインクリメント関数を簡単に作成できます。

以下の例では、カウンター変数をキャプチャしたラムダ式を返すメソッドを示します。

using System;
class Program
{
    static Func<int> CreateCounter()
    {
        int count = 0;
        return () =>
        {
            count++;
            return count;
        };
    }
    static void Main()
    {
        var counter = CreateCounter();
        Console.WriteLine(counter()); // 出力: 1
        Console.WriteLine(counter()); // 出力: 2
        Console.WriteLine(counter()); // 出力: 3
    }
}
1
2
3

この例では、CreateCounterメソッド内のローカル変数countをラムダ式がキャプチャしています。

counterを呼び出すたびにcountがインクリメントされ、状態が保持されていることがわかります。

これにより、状態を持つ関数を簡潔に生成できます。

シングルトンの疑似実装

ラムダ式のキャプチャを使って、シングルトンのように一度だけ生成されるインスタンスを擬似的に実装することも可能です。

以下は、遅延初期化を行うシンプルな例です。

using System;
class Program
{
    static Func<string> CreateSingleton()
    {
        string instance = null;
        return () =>
        {
            if (instance == null)
            {
                instance = "唯一のインスタンス";
                Console.WriteLine("インスタンスを生成しました。");
            }
            return instance;
        };
    }
    static void Main()
    {
        var getInstance = CreateSingleton();
        Console.WriteLine(getInstance()); // 出力: インスタンスを生成しました。唯一のインスタンス
        Console.WriteLine(getInstance()); // 出力: 唯一のインスタンス
    }
}
インスタンスを生成しました。
唯一のインスタンス
唯一のインスタンス

この例では、instance変数をキャプチャしたラムダ式が、初回呼び出し時にインスタンスを生成し、それ以降は同じインスタンスを返します。

シングルトンパターンの基本的な動作を簡潔に表現しています。

状態フルコールバックの構築

イベントや非同期処理で状態を持つコールバックを作成する際にも、変数キャプチャは便利です。

以下は、状態を持つコールバック関数を生成し、呼び出しごとに状態を更新する例です。

using System;
class Program
{
    static Action<string> CreateLogger()
    {
        int logCount = 0;
        return message =>
        {
            logCount++;
            Console.WriteLine($"ログ{logCount}: {message}");
        };
    }
    static void Main()
    {
        var logger = CreateLogger();
        logger("開始しました。");   // 出力: ログ1: 開始しました。
        logger("処理中です。");   // 出力: ログ2: 処理中です。
        logger("終了しました。"); // 出力: ログ3: 終了しました。
    }
}
ログ1: 開始しました。
ログ2: 処理中です。
ログ3: 終了しました。

この例では、logCountをキャプチャしたラムダ式が状態を保持し、呼び出しごとにログ番号を増やしています。

状態を持つコールバックを簡単に作成できるため、イベント処理や非同期処理での利用に適しています。

これらのパターンは、ラムダ式の変数キャプチャ機能を活用して状態を持つ関数やコールバックを簡潔に実装する方法です。

状態管理が必要な場面で、クラスを定義せずに手軽に実装できるため、コードのシンプル化に役立ちます。

ラムダ式と例外処理

ラムダ式は例外処理の文脈でも柔軟に利用できます。

trycatchブロック内での使用や、throw式との組み合わせ、さらには例外フィルターでの活用方法について具体的に見ていきます。

try -catch 内での利用

ラムダ式はtrycatchブロックの中で定義・実行できます。

例外が発生する可能性のある処理をラムダ式にまとめ、例外を捕捉して適切に処理するパターンがよく使われます。

以下の例では、ラムダ式内で例外が発生し、catchブロックで捕捉しています。

using System;
class Program
{
    static void Main()
    {
        Action riskyAction = () =>
        {
            Console.WriteLine("処理開始");
            int x = 0;
            int y = 10 / x; // ここで例外発生
            Console.WriteLine($"結果: {y}");
        };
        try
        {
            riskyAction();
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine($"例外をキャッチしました: {ex.Message}");
        }
    }
}
処理開始
例外をキャッチしました: ゼロによる除算です。

このように、ラムダ式内で例外が発生しても、外側のtrycatchで捕捉可能です。

ラムダ式自体は通常のメソッドと同様に例外をスローできるため、例外処理の流れに自然に組み込めます。

throw 式との組み合わせ

C# 7.0以降では、throwが式として使えるようになりました。

これにより、ラムダ式の中で条件演算子や式の中にthrowを組み込むことが可能です。

例えば、引数の検証を行い、不正な値の場合に例外をスローするラムダ式は以下のように書けます。

using System;
class Program
{
    static Func<int, int> safeDivide = x => x == 0 ? throw new ArgumentException("0は許容されません") : 100 / x;
    static void Main()
    {
        try
        {
            Console.WriteLine(safeDivide(10)); // 出力: 10
            Console.WriteLine(safeDivide(0));  // 例外発生
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine($"例外: {ex.Message}");
        }
    }
}
10
例外: 0は許容されません

この例では、x == 0の場合にthrow式で例外をスローし、それ以外は計算結果を返しています。

throw式を使うことで、ラムダ式内の条件分岐をより簡潔に記述できます。

例外フィルターでの活用

例外フィルターは、catch節に条件を付けて特定の条件下でのみ例外を捕捉する機能です。

ラムダ式と組み合わせることで、柔軟な例外処理が可能になります。

以下は、例外のメッセージに特定の文字列が含まれる場合のみ捕捉する例です。

using System;
class Program
{
    static void Main()
    {
        Action action = () => throw new InvalidOperationException("特定のエラーが発生しました");
        try
        {
            action();
        }
        catch (InvalidOperationException ex) when (ex.Message.Contains("特定のエラー"))
        {
            Console.WriteLine("特定のエラーを検出し、処理しました。");
        }
        catch (Exception)
        {
            Console.WriteLine("その他の例外を処理しました。");
        }
    }
}
特定のエラーを検出し、処理しました。

この例では、ラムダ式内で例外をスローし、catch節の例外フィルターでメッセージ内容を判定しています。

条件に合致した場合のみ例外を捕捉し、適切な処理を行っています。

ラムダ式は例外処理の中でも自然に使え、trycatch内での実行、throw式の活用、例外フィルターとの組み合わせにより、柔軟で簡潔なエラーハンドリングが可能です。

LINQ におけるラムダ式

LINQ(Language Integrated Query)は、C#でデータ操作を簡潔に記述できる強力な機能です。

LINQのメソッドチェーンでは、ラムダ式が条件指定や変換処理の中心として使われます。

ここでは、WhereSelectなどの代表的なメソッドでのラムダ式の使い方、集計メソッドでの複雑な式、そしてクエリ構文との相互変換について詳しく説明します。

Where での条件抽出

Whereメソッドは、シーケンスから条件に合致する要素だけを抽出するために使います。

ラムダ式で条件を指定し、真となる要素だけが結果に含まれます。

以下の例では、整数のリストから偶数だけを抽出しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
        var evenNumbers = numbers.Where(n => n % 2 == 0);
        foreach (var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
2
4
6

ラムダ式n => n % 2 == 0は、引数nが偶数かどうかを判定しています。

Whereはこの条件を満たす要素だけを返します。

Select でのプロジェクション

Selectメソッドは、シーケンスの各要素を別の形に変換(プロジェクション)するために使います。

ラムダ式で変換処理を指定し、新しいシーケンスを生成します。

以下の例では、文字列のリストから各文字列の長さを取得しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var words = new List<string> { "apple", "banana", "cherry" };
        var lengths = words.Select(word => word.Length);
        foreach (var length in lengths)
        {
            Console.WriteLine(length);
        }
    }
}
5
6
6

ラムダ式word => word.Lengthは、各文字列の長さを返しています。

Selectはこの結果を新しいシーケンスとして返します。

集計メソッドでの複雑な式

LINQの集計メソッド(SumAverageCountなど)でもラムダ式を使って複雑な条件や計算を行えます。

例えば、特定の条件を満たす要素の合計を計算する場合などです。

以下の例では、商品のリストから価格が100以上の商品の合計価格を計算しています。

using System;
using System.Collections.Generic;
using System.Linq;
class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}
class Program
{
    static void Main()
    {
        var products = new List<Product>
        {
            new Product { Name = "ペン", Price = 50 },
            new Product { Name = "ノート", Price = 120 },
            new Product { Name = "消しゴム", Price = 80 },
            new Product { Name = "定規", Price = 150 }
        };
        int total = products.Where(p => p.Price >= 100)
                            .Sum(p => p.Price);
        Console.WriteLine($"100円以上の商品の合計価格: {total}");
    }
}
100円以上の商品の合計価格: 270

ここでは、Whereで価格が100以上の商品のみ抽出し、Sumでその価格を合計しています。

Sumのラムダ式p => p.Priceは、合計対象の値を指定しています。

クエリ構文との相互変換

LINQにはメソッドチェーン形式の構文と、SQLに似たクエリ構文があります。

どちらも同じ処理を表現できますが、ラムダ式はメソッドチェーン形式で使われることが多いです。

以下は、先ほどの偶数抽出をクエリ構文で書いた例です。

using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
    static void Main()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
        var evenNumbers = from n in numbers
                          where n % 2 == 0
                          select n;
        foreach (var num in evenNumbers)
        {
            Console.WriteLine(num);
        }
    }
}
2
4
6

このクエリ構文は、メソッドチェーンのWhereSelectを組み合わせたものと等価です。

クエリ構文は読みやすい場合もありますが、複雑な処理や動的な条件指定にはメソッドチェーンとラムダ式が柔軟です。

LINQにおけるラムダ式は、条件抽出や変換、集計など多彩な操作を簡潔に記述できる重要な要素です。

クエリ構文との使い分けも理解し、状況に応じて最適なスタイルを選んでください。

型推論とラムダ式

C#のラムダ式は、型推論と密接に関係しており、引数の型や戻り値の型を省略してもコンパイラが適切に推定してくれます。

これにより、コードがより簡潔で読みやすくなります。

ここでは、引数型の省略ルール、戻り値型の推定、そしてタプルやディスカード _ を活用した書き方について詳しく説明します。

引数型の省略ルール

ラムダ式の引数型は、通常、コンパイラがデリゲートや式ツリーの型情報から推論します。

そのため、引数の型を明示的に書かなくても問題ありません。

例えば、以下の2つのラムダ式は同じ意味を持ちます。

using System;
class Program
{
    static void Main()
    {
        Func<int, int> square1 = (int x) => x * x;
        Func<int, int> square2 = x => x * x;
        Console.WriteLine(square1(4)); // 出力: 16
        Console.WriteLine(square2(5)); // 出力: 25
    }
}
16
25

引数が1つの場合は、括弧も省略可能です。

ただし、引数が複数ある場合は括弧が必須です。

Func<int, int, int> add = (x, y) => x + y;

引数の型を省略できるのは、ラムダ式が割り当てられるデリゲートの型が明確な場合に限られます。

型が不明な場合は、明示的に型を指定する必要があります。

戻り値型の推定

ラムダ式の戻り値の型は、式の内容からコンパイラが自動的に推定します。

式形式のラムダ式では、式の評価結果の型が戻り値の型となります。

例えば、以下の例では戻り値の型を明示していませんが、Func<int, int>の戻り値型がintであるため、x * xの結果もintと推定されます。

Func<int, int> square = x => x * x;

ステートメント形式のラムダ式では、return文で返す値の型が戻り値の型となります。

Func<int, int> square = x =>
{
    return x * x;
};

戻り値がない場合はActionデリゲートを使い、戻り値型の推定は不要です。

タプルとディスカード _ の活用

C# 7.0以降、タプル型を使って複数の値をまとめて扱うことができます。

ラムダ式の引数や戻り値にタプルを使うことで、複数の値を簡潔に処理できます。

以下は、タプルを引数に取り、その要素を加算して返すラムダ式の例です。

using System;
class Program
{
    static void Main()
    {
        Func<(int, int), int> addTuple = tuple => tuple.Item1 + tuple.Item2;
        int result = addTuple((3, 4));
        Console.WriteLine(result); // 出力: 7
    }
}
7

タプルの要素にはItem1Item2などでアクセスできますが、名前付きタプルを使うとより分かりやすくなります。

Func<(int x, int y), int> addNamedTuple = point => point.x + point.y;

また、ディスカード_は、不要な値を無視するために使います。

ラムダ式の引数で使うと、特定の引数を使わないことを明示できます。

例えば、2つの引数のうち1つだけ使う場合は以下のように書けます。

Action<int, int> printFirst = (x, _) => Console.WriteLine(x);
printFirst(10, 20); // 出力: 10

このように、ディスカードを使うことで、使わない引数を明示的に無視し、コードの意図をわかりやすくできます。

型推論により、ラムダ式は引数型や戻り値型を省略してもコンパイラが適切に判断します。

タプルやディスカードを活用することで、複雑なデータ構造や不要な引数を扱う際も簡潔で明快なコードが書けます。

式ツリーへの変換

C#のラムダ式は、単なる匿名関数としてだけでなく、式ツリー(Expression Tree)として表現することも可能です。

式ツリーは、コードの構造をデータとして表現し、動的な解析や変換を行う際に非常に有用です。

ここでは、Expression<TDelegate>型の生成方法、式ツリーの解析と編集、そしてORマッパー(Object-Relational Mapper)への応用について詳しく説明します。

Expression<TDelegate> 型の生成

通常のラムダ式はデリゲート型に変換され、実行可能なコードとして扱われます。

一方、式ツリーとして扱いたい場合は、System.Linq.Expressions名前空間のExpression<TDelegate>型を使います。

これにより、ラムダ式の構造が式ツリーとしてコンパイル時に生成され、実行されるのではなく、式の構造情報として保持されます。

以下は、式ツリーを生成する例です。

using System;
using System.Linq.Expressions;
class Program
{
    static void Main()
    {
        // 通常のラムダ式(デリゲート)
        Func<int, int, int> addDelegate = (x, y) => x + y;
        // 式ツリーとしてのラムダ式
        Expression<Func<int, int, int>> addExpression = (x, y) => x + y;
        Console.WriteLine(addDelegate(3, 4)); // 出力: 7
        // 式ツリーの内容を表示
        Console.WriteLine(addExpression);
    }
}
7
(x, y) => (x + y)

Expression<Func<int, int, int>>は、Func<int, int, int>の式ツリー版です。

addExpressionは実行可能なコードではなく、式の構造を表すオブジェクトとして扱われます。

式ツリーの解析と編集

式ツリーは、ノードの集合で構成されており、各ノードは演算子や変数、定数などを表します。

これを解析・編集することで、動的にクエリを生成したり、式を変換したりできます。

以下は、式ツリーのパラメーター名を取得し、式の内容を解析する例です。

using System;
using System.Linq.Expressions;
class Program
{
    static void Main()
    {
        Expression<Func<int, int, int>> expr = (a, b) => a * b + 10;
        // パラメーター名の取得
        foreach (var param in expr.Parameters)
        {
            Console.WriteLine($"パラメーター名: {param.Name}");
        }
        // 式の本体を表示
        Console.WriteLine($"式の本体: {expr.Body}");
    }
}
パラメーター名: a
パラメーター名: b
式の本体: ((a * b) + 10)

さらに、ExpressionVisitorクラスを継承して式ツリーを編集することも可能です。

例えば、特定の演算子を別の演算子に置き換えるなどの処理が行えます。

ORマッパーへの応用

式ツリーは、LINQ to SQLやEntity FrameworkなどのORマッパーで重要な役割を果たします。

これらのフレームワークは、式ツリーを解析してSQLクエリなどのデータベースクエリに変換します。

例えば、以下のようなLINQクエリは、内部的に式ツリーとして表現され、SQLに変換されます。

using System;
using System.Linq;
using System.Collections.Generic;
class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}
class Program
{
    static void Main()
    {
        var products = new List<Product>
        {
            new Product { Name = "ペン", Price = 50 },
            new Product { Name = "ノート", Price = 120 },
            new Product { Name = "消しゴム", Price = 80 }
        };
        var query = products.AsQueryable()
                            .Where(p => p.Price > 60)
                            .Select(p => p.Name);
        foreach (var name in query)
        {
            Console.WriteLine(name);
        }
    }
}
ノート
消しゴム

このWhereSelectのラムダ式は式ツリーとして渡され、ORマッパーが解析してSQLのWHERE句やSELECT句に変換します。

これにより、プログラマはC#のコードでデータベースクエリを直感的に記述できます。

式ツリーは、ラムダ式の構造をデータとして扱う強力な仕組みであり、動的なクエリ生成やコード解析、ORマッパーの基盤として不可欠です。

Expression<TDelegate>型を使って式ツリーを生成し、解析・編集することで、柔軟なプログラム設計が可能になります。

非同期ラムダ式の活用

C#では、async/awaitキーワードを使って非同期処理を簡潔に記述できます。

ラムダ式にもasync修飾子を付けることで、非同期の匿名関数を作成可能です。

ここでは、async修飾子の使い方、awaitの配置と戻り値の扱い、Taskのキャンセル対応、そしてコンテキスト同期のコントロールについて詳しく説明します。

async 修飾子の使い方

非同期ラムダ式は、asyncキーワードをラムダ式の前に付けることで定義します。

これにより、ラムダ式内でawaitを使った非同期処理が可能になります。

以下は、非同期ラムダ式をFunc<Task>型の変数に代入し、非同期処理を実行する例です。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        Func<Task> asyncLambda = async () =>
        {
            await Task.Delay(1000);
            Console.WriteLine("非同期処理が完了しました。");
        };
        await asyncLambda();
    }
}
非同期処理が完了しました。

async修飾子を付けることで、ラムダ式内でawaitが使え、非同期処理の完了を待機できます。

await の配置と戻り値

非同期ラムダ式の戻り値は、TaskまたはTask<TResult>型になります。

戻り値がない場合はTask、値を返す場合はTask<TResult>を使います。

例えば、整数を返す非同期ラムダ式は以下のように書けます。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        Func<Task<int>> asyncFunc = async () =>
        {
            await Task.Delay(500);
            return 42;
        };
        int result = await asyncFunc();
        Console.WriteLine($"結果: {result}");
    }
}
結果: 42

awaitは非同期処理の完了を待ち、戻り値を取得します。

ラムダ式の戻り値はreturn文で指定し、Task<TResult>として返されます。

Task のキャンセル対応

非同期処理では、キャンセルが重要な要素です。

CancellationTokenを使ってキャンセルをサポートする非同期ラムダ式を作成できます。

以下は、キャンセルトークンを受け取り、キャンセルが要求された場合に例外をスローする例です。

using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        var cts = new CancellationTokenSource();
        Func<CancellationToken, Task> cancellableAsync = async token =>
        {
            for (int i = 0; i < 5; i++)
            {
                token.ThrowIfCancellationRequested();
                Console.WriteLine($"処理中... {i + 1}");
                await Task.Delay(500, token);
            }
            Console.WriteLine("処理が完了しました。");
        };
        var task = cancellableAsync(cts.Token);
        // 1秒後にキャンセルを要求
        cts.CancelAfter(1000);
        try
        {
            await task;
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("処理がキャンセルされました。");
        }
    }
}
処理中... 1
処理中... 2
処理がキャンセルされました。

キャンセルトークンをラムダ式の引数として受け取り、ThrowIfCancellationRequestedでキャンセルを検知します。

Task.Delayにもトークンを渡すことで、待機中のキャンセルも可能です。

コンテキスト同期のコントロール

非同期ラムダ式は、デフォルトで呼び出し元の同期コンテキスト(例:UIスレッド)に戻って処理を継続します。

これを制御するには、ConfigureAwait(false)を使います。

以下は、ConfigureAwait(false)を使ってコンテキスト同期を抑制する例です。

using System;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        Func<Task> asyncLambda = async () =>
        {
            await Task.Delay(500).ConfigureAwait(false);
            Console.WriteLine($"スレッドID: {Environment.CurrentManagedThreadId}");
        };
        Console.WriteLine($"開始スレッドID: {Environment.CurrentManagedThreadId}");
        await asyncLambda();
    }
}
開始スレッドID: 1
スレッドID: 4

ConfigureAwait(false)を使うと、非同期処理完了後の継続が元の同期コンテキストに戻らず、スレッドプールのスレッドで実行されます。

UIスレッドに戻る必要がないバックグラウンド処理では、これを使うことでデッドロック回避やパフォーマンス向上が期待できます。

非同期ラムダ式は、async修飾子とawaitを組み合わせて非同期処理を簡潔に記述でき、キャンセル対応やコンテキスト同期の制御も柔軟に行えます。

これらの機能を活用して、効率的で安全な非同期コードを実装してください。

パフォーマンスとメモリ管理

ラムダ式は便利な機能ですが、パフォーマンスやメモリ管理の観点から注意すべきポイントがあります。

特に変数キャプチャによるヒープ割り当てや、アロケーションを抑える書き方、インライン展開の可能性、そしてref structとの関係について理解しておくことが重要です。

キャプチャによるヒープ割り当て

ラムダ式が外部の変数をキャプチャすると、その変数はクロージャとしてヒープ上に割り当てられます。

これは、キャプチャされた変数のライフタイムがラムダ式のデリゲートのライフタイムに合わせて延長されるためです。

以下の例を見てみましょう。

using System;
class Program
{
    static void Main()
    {
        int counter = 0;
        Func<int> increment = () =>
        {
            counter++;
            return counter;
        };
        Console.WriteLine(increment()); // 出力: 1
        Console.WriteLine(increment()); // 出力: 2
    }
}

この場合、counterはラムダ式にキャプチャされているため、incrementデリゲートが生きている間、counterはヒープ上に保持されます。

これにより、スタック上のローカル変数よりもメモリ割り当てが大きくなり、ガベージコレクションの負荷が増す可能性があります。

アロケーションを減らす書き方

ヒープ割り当てを減らすためには、キャプチャを避けるか、staticラムダ式を使う方法があります。

C# 9.0以降では、static修飾子をラムダ式に付けることで、外部変数のキャプチャを禁止し、ヒープ割り当てを防げます。

using System;
class Program
{
    static void Main()
    {
        Func<int, int> square = static x => x * x;
        Console.WriteLine(square(5)); // 出力: 25
    }
}

このラムダ式は外部変数をキャプチャしないため、ヒープ割り当てが発生しません。

また、ループ内でのキャプチャを避けるために、ループ変数をローカル変数に代入してからラムダ式に渡す方法も有効です。

for (int i = 0; i < 3; i++)
{
    int temp = i;
    Action action = () => Console.WriteLine(temp);
    action();
}

このようにすることで、不要なクロージャの生成を防げます。

ラムダ式のインライン展開

コンパイラやJITは、ラムダ式をインライン展開することがあります。

インライン展開とは、関数呼び出しのオーバーヘッドを減らすために、関数の本体を呼び出し元に直接展開する最適化です。

ただし、ラムダ式がキャプチャを含む場合や複雑な場合はインライン展開されにくいです。

staticラムダ式や単純な式形式ラムダ式はインライン展開されやすく、パフォーマンス向上に寄与します。

インライン展開の有無はJITコンパイラの判断に依存するため、明示的に制御することはできませんが、コードをシンプルに保つことが促進要因となります。

ref struct 制約との関係

ref structは、スタック上にのみ割り当てられる特殊な構造体で、ヒープ割り当てを防ぐために使われます。

Span<T>などが代表例です。

ラムダ式はref structをキャプチャできません。

これは、ref structのライフタイムがスタックに限定されているため、ヒープに割り当てられるクロージャに含めることができないからです。

以下のコードはコンパイルエラーになります。

using System;
class Program
{
    static void Main()
    {
        Span<int> span = stackalloc int[3] { 1, 2, 3 };
        Func<int> func = () => span[0]; // エラー: ref structをキャプチャできない
    }
}

この制約により、ref structを使う場合はラムダ式でのキャプチャを避ける設計が必要です。

代わりにローカル関数や明示的な引数渡しを検討してください。

ラムダ式のパフォーマンスとメモリ管理では、キャプチャによるヒープ割り当てが最も注意すべきポイントです。

staticラムダ式の活用やキャプチャ回避の工夫、インライン展開の可能性を意識しつつ、ref structとの制約も踏まえて効率的なコードを書くことが重要です。

デバッグと診断

ラムダ式は便利な機能ですが、匿名関数であるためデバッグ時に挙動を把握しづらいことがあります。

ここでは、Visual Studioでのステップ実行方法、dbgviewを使ったクロージャの確認、そしてコンパイル後のIL(中間言語)コードの確認方法について詳しく解説します。

Visual Studio でのステップ実行

Visual Studioでは、ラムダ式の中身も通常のメソッドと同様にステップ実行が可能です。

ただし、匿名関数のため、デバッガのコールスタックや変数ウォッチで名前がわかりにくい場合があります。

以下のポイントを押さえるとスムーズにデバッグできます。

  • ブレークポイントの設定

ラムダ式の中の任意の行にブレークポイントを設定できます。

Visual Studioは匿名メソッドのソースコードを認識しているため、通常のメソッドと同様に停止します。

  • ローカル変数の監視

キャプチャされた変数はクロージャクラスのフィールドとして保持されるため、ローカル変数ウィンドウで確認できます。

変数名は元の名前のまま表示されることが多いです。

  • コールスタックの確認

ラムダ式は内部的にメソッドとして生成されるため、コールスタックに<Main>b__0のような名前で表示されます。

これがラムダ式の実体です。

以下は簡単な例です。

using System;
class Program
{
    static void Main()
    {
        int factor = 2;
        Func<int, int> multiply = x => x * factor;
        int result = multiply(5); // ここにブレークポイントを設定
        Console.WriteLine(result);
    }
}

このコードでブレークポイントを設定し、ステップ実行すると、ラムダ式内の計算も追跡できます。

dbgview でのクロージャ確認

dbgview(DebugView)は、Windows向けのデバッグツールで、アプリケーションのデバッグ出力をリアルタイムで監視できます。

ラムダ式のクロージャの状態をログ出力して確認する際に役立ちます。

例えば、ラムダ式内でキャプチャした変数の値をDebug.WriteLineで出力し、dbgviewで監視する方法です。

using System;
using System.Diagnostics;
class Program
{
    static void Main()
    {
        int counter = 0;
        Action increment = () =>
        {
            counter++;
            Debug.WriteLine($"counterの値: {counter}");
        };
        increment();
        increment();
    }
}

dbgviewを起動してこのプログラムを実行すると、counterの値: 1counterの値: 2といったログがリアルタイムで表示されます。

これにより、クロージャの状態変化を外部から監視可能です。

コンパイル後 IL の確認

ラムダ式はコンパイル時に匿名メソッドやクロージャクラスとして変換されます。

IL(Intermediate Language)コードを確認することで、ラムダ式の内部構造やキャプチャの仕組みを理解できます。

ILコードの確認には、以下のツールが使えます。

  • ILSpy

オープンソースの.NETアセンブリブラウザで、ILコードやC#コードに逆コンパイル可能です。

  • dotPeek

JetBrains製の無料リバースエンジニアリングツール。

  • Visual StudioのIL Disassembler

Visual Studioの一部機能として利用可能です。

以下は、簡単なラムダ式のILコード例(ILSpyでの表示イメージ)です。

Func<int, int> square = x => x * x;

このコードは、内部的に以下のような匿名メソッドとクロージャクラスに変換されます。

  • クロージャクラス(キャプチャがある場合)
  • 匿名メソッド(<Main>b__0のような名前)

ILコードでは、メソッド呼び出しやローカル変数の扱いが確認でき、ラムダ式がどのように実装されているかを詳細に把握できます。

Visual Studioのステップ実行でラムダ式の動作を追い、dbgviewでクロージャの状態をログ監視し、ILコードで内部構造を解析することで、ラムダ式の挙動を深く理解できます。

これらのツールを活用して効率的なデバッグと診断を行いましょう。

ローカル関数との比較

C#では、ラムダ式とローカル関数の両方を使ってメソッド内に小さな関数を定義できます。

どちらも匿名的にコードをまとめる手段ですが、書式やパフォーマンス、変数キャプチャの挙動に違いがあります。

ここでは、書式と可読性、パフォーマンス差、キャプチャとの相性について詳しく比較します。

書式と可読性

ラムダ式は、匿名関数を簡潔に表現するための式で、主にデリゲートやLINQの引数として使われます。

式形式とステートメント形式があり、短い処理を一行で書けるのが特徴です。

Func<int, int> square = x => x * x;

一方、ローカル関数はメソッド内に名前付きの関数を定義する構文で、通常のメソッドと同様に複数のステートメントを含められます。

int Square(int x)
{
    return x * x;
}

ローカル関数は名前があるため、再帰呼び出しや複雑な処理の分割に向いています。

また、コードの構造が明確になるため、可読性が高い場合があります。

ラムダ式は短く簡潔に書ける反面、複雑な処理や複数のステートメントを含む場合は、ローカル関数のほうが読みやすくなることがあります。

パフォーマンス差

パフォーマンス面では、ローカル関数のほうが有利な場合があります。

特に、変数をキャプチャしない場合は、ローカル関数は通常のメソッド呼び出しと同様に扱われ、デリゲートの生成やクロージャの割り当てが発生しません。

一方、ラムダ式はデリゲートとして扱われるため、キャプチャがなくてもデリゲートインスタンスの生成が必要になることがあります。

以下の例で比較します。

using System;
class Program
{
    static void Main()
    {
        // ローカル関数(キャプチャなし)
        int LocalSquare(int x) => x * x;
        // ラムダ式(キャプチャなし)
        Func<int, int> lambdaSquare = x => x * x;
        Console.WriteLine(LocalSquare(5));    // 出力: 25
        Console.WriteLine(lambdaSquare(5));   // 出力: 25
    }
}

ローカル関数はデリゲートを生成しないため、呼び出しコストが低くなります。

ラムダ式はデリゲート生成が必要なので、頻繁に呼び出す場合はパフォーマンスに影響することがあります。

ただし、ラムダ式もJITコンパイラの最適化でインライン展開されることがあり、実際の差はケースバイケースです。

キャプチャとの相性

変数キャプチャに関しては、ローカル関数とラムダ式で挙動が似ていますが、微妙な違いがあります。

  • ラムダ式はキャプチャした変数をクロージャクラスに保持し、ヒープ割り当てが発生します
  • ローカル関数もキャプチャがある場合はクロージャを生成しますが、キャプチャがない場合はクロージャを生成しません

また、ローカル関数はrefoutパラメーターを使えるのに対し、ラムダ式はこれらのパラメーターをサポートしていません。

これにより、状態を直接変更したい場合はローカル関数のほうが柔軟です。

以下の例を見てください。

using System;
class Program
{
    static void Main()
    {
        int count = 0;
        // ローカル関数はrefパラメーターを使える
        void Increment(ref int x) => x++;
        Increment(ref count);
        Console.WriteLine(count); // 出力: 1
        // ラムダ式はrefパラメーターを使えないため、キャプチャで代用
        Action increment = () => count++;
        increment();
        Console.WriteLine(count); // 出力: 2
    }
}

このように、ローカル関数はより柔軟に変数を操作できるため、キャプチャが絡む複雑な処理では有利です。

まとめると、ラムダ式は簡潔で式的な記述に適し、ローカル関数は名前付きで複雑な処理や再帰、refパラメーターを使う場合に適しています。

パフォーマンス面ではキャプチャの有無が大きく影響し、キャプチャがない場合はローカル関数のほうが効率的です。

用途や可読性、パフォーマンス要件に応じて使い分けることが重要です。

属性とラムダ式

C#の属性は、コードにメタデータを付加してコンパイラやツールに情報を伝えるために使われます。

ラムダ式と組み合わせる際には、nullable状態の推論や属性付きデリゲートの宣言に関する注意点があります。

ここでは、それぞれについて詳しく解説します。

nullable 状態の推論

C# 8.0以降で導入されたNullable Reference Types(NRT)機能により、参照型の変数がnullを許容するかどうかを明示的に区別できます。

ラムダ式の引数や戻り値のnullable状態は、コンパイラがコンテキストから推論します。

例えば、以下のコードでは、ラムダ式の引数namestring?(null許容)かどうかが推論されます。

using System;
class Program
{
    static void Main()
    {
        Func<string?, int> getLength = name => name?.Length ?? 0;
        Console.WriteLine(getLength(null));    // 出力: 0
        Console.WriteLine(getLength("太郎")); // 出力: 2
    }
}

この例では、namenullの場合に備えて?.演算子を使っています。

Func<string?, int>と明示的に型を指定することで、引数がnullを許容することを示しています。

一方、型を省略して以下のように書くと、

Func<string, int> getLength = name => name.Length;

namenullを許容しないstringとして推論されます。

nullable状態の推論は、デリゲート型の定義に依存するため、適切に型を指定することが重要です。

属性付きデリゲートの宣言

ラムダ式自体に直接属性を付けることはできませんが、ラムダ式を割り当てるデリゲート型に属性を付けることは可能です。

特に、メソッドのパラメーターや戻り値に対して属性を付けたい場合は、カスタムデリゲートを定義して属性を付与します。

以下は、パラメーターに[NotNull]属性を付けたカスタムデリゲートの例です。

using System;
using System.Diagnostics.CodeAnalysis;
delegate void NotNullAction([NotNull] string input);
class Program
{
    static void Main()
    {
        NotNullAction action = input => Console.WriteLine(input.ToUpper());
        action("hello"); // 出力: HELLO
        // action(null); // 実行時にNullReferenceExceptionになる可能性あり
    }
}

この例では、NotNullActionデリゲートのinputパラメーターに[NotNull]属性を付けています。

これにより、静的解析ツールやIDEがnullを渡さないよう警告を出すことができます。

また、FuncActionなどの組み込みデリゲートには属性を付けられないため、属性を使いたい場合はカスタムデリゲートを定義する必要があります。

ラムダ式のnullable状態は、割り当て先のデリゲート型から推論されるため、適切な型指定が重要です。

属性を付けたい場合は、カスタムデリゲートを使ってパラメーターや戻り値に属性を付与し、静的解析やコード品質向上に役立ててください。

よくある落とし穴と対処法

ラムダ式は便利な機能ですが、使い方を誤ると意図しない動作やパフォーマンス問題を引き起こすことがあります。

ここでは、特に注意が必要な「キャプチャ変数の変更忘れ」「長寿命のクロージャによるメモリリーク」「非同期ラムダの例外消失」について具体例と対処法を解説します。

キャプチャ変数の変更忘れ

ラムダ式が外部変数をキャプチャすると、その変数の最新の値を参照します。

しかし、変数の値を更新し忘れると、期待した動作にならないことがあります。

例えば、以下のコードを見てください。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        var actions = new List<Action>();
        int counter = 0;
        for (int i = 0; i < 3; i++)
        {
            actions.Add(() => Console.WriteLine(counter));
            counter++; // ここでcounterを更新
        }
        foreach (var action in actions)
        {
            action();
        }
    }
}
3
3
3

期待としては0, 1, 2が出力されるはずですが、すべて3が出力されます。

これは、ラムダ式がcounter変数をキャプチャしており、実行時点での最新値3を参照しているためです。

対処法

変数の値をキャプチャ時点で固定したい場合は、ループ内で新しいローカル変数に代入してからキャプチャします。

for (int i = 0; i < 3; i++)
{
    int temp = i;
    actions.Add(() => Console.WriteLine(temp));
}

これにより、tempは各ループ反復ごとに新しい変数となり、期待通り0, 1, 2が出力されます。

長寿命のクロージャによるメモリリーク

ラムダ式がキャプチャした変数はクロージャとしてヒープに割り当てられ、ラムダ式のデリゲートが生きている限り解放されません。

これが意図せず長期間残ると、メモリリークの原因になります。

例えば、イベントハンドラーにラムダ式を登録し、解除し忘れるケースです。

using System;
class Publisher
{
    public event Action? OnEvent;
    public void Raise() => OnEvent?.Invoke();
}
class Program
{
    static void Main()
    {
        var publisher = new Publisher();
        int counter = 0;
        // ラムダ式がcounterをキャプチャ
        publisher.OnEvent += () =>
        {
            counter++;
            Console.WriteLine($"イベント発生: {counter}");
        };
        publisher.Raise(); // 出力: イベント発生: 1
        // イベント解除を忘れると、publisherが解放されない可能性あり
    }
}

この場合、publisherOnEventに登録したラムダ式がcounterをキャプチャし、publisherが解放されないことがあります。

対処法

  • イベント登録後は必ず解除します
  • クロージャのキャプチャを最小限にします
  • WeakReferenceWeakEventパターンを検討します

非同期ラムダの例外消失

非同期ラムダ式内で例外が発生しても、適切にawaitされなかったり、例外処理がされなかったりすると例外が消失し、問題の原因がわかりにくくなります。

以下の例では、非同期ラムダの例外がキャッチされません。

using System;
using System.Threading.Tasks;
class Program
{
    static void Main()
    {
        Func<Task> asyncLambda = async () =>
        {
            await Task.Delay(100);
            throw new InvalidOperationException("エラー発生");
        };
        // awaitしないため例外が捕捉されない
        asyncLambda();
        Console.WriteLine("処理続行");
        Task.Delay(500).Wait(); // 非同期処理の完了を待つだけ
    }
}
処理続行

例外は発生していますが、awaitしていないためMainメソッド内で捕捉されず、プログラムは例外を無視して続行します。

対処法

  • 非同期ラムダは必ずawaitします
  • Taskの例外をtry-catchで捕捉します
  • 非同期イベントハンドラーの場合は、例外をログに記録するなどの対策を行います
try
{
    await asyncLambda();
}
catch (Exception ex)
{
    Console.WriteLine($"例外捕捉: {ex.Message}");
}

これらの落とし穴は、ラムダ式の特性を理解し、適切に変数のスコープや非同期処理の扱いを管理することで回避できます。

コードの意図を明確にし、メモリ管理や例外処理を怠らないことが重要です。

まとめ

この記事では、C#のラムダ式の基礎から応用まで幅広く解説しました。

ラムダ式の構文やデリゲートとの関係、変数キャプチャの仕組み、非同期処理での活用方法、LINQでの使い方、パフォーマンスやデバッグのポイント、ローカル関数との違い、属性の扱い、そしてよくある落とし穴と対処法まで網羅しています。

これらを理解することで、ラムダ式を効果的かつ安全に活用し、可読性とパフォーマンスに優れたC#コードを書く力が身につきます。

関連記事

Back to top button
目次へ