C#におけるプログラミングにおいて、デリゲートは非常に強力な機能の一つです。

その中でも「Actionデリゲート」は、戻り値を持たないメソッドを変数のように扱えるため、コールバック処理やイベント駆動型のプログラミング、非同期処理など、現代的なC#開発には欠かせない存在となっています。

本記事では、C#のActionデリゲートの基礎から応用までを網羅的に解説します。

特に、開発現場で混同されやすいFuncデリゲートとの違いや、ラムダ式を用いた効率的な書き方、実務で役立つ具体的な活用シーンについて詳しく掘り下げていきます。

これからC#を習得しようとしている方はもちろん、より洗練されたコードを書きたい中級者の方も、ぜひ参考にしてください。

Actionデリゲートとは何か

Actionデリゲートは、.NET Framework 3.5で導入された「戻り値を持たない(void)メソッドをカプセル化するための汎用デリゲート」です。

従来、C#でメソッドを引数として渡すためには、その都度独自のデリゲート型をdelegateキーワードで定義する必要がありました。

しかし、多くの場面で必要とされるのは「特定の引数を受け取り、何も返さない」という単純な構造です。

これを標準化したものがActionデリゲートです。

Actionの基本的な定義

Actionデリゲートは、引数の数に応じて複数のオーバーロードが用意されています。

引数がないものから、最大16個の引数を受け取るものまで定義されており、System名前空間に含まれています。

  • Action : 引数なし、戻り値なし
  • Action<T> : 引数1つ、戻り値なし
  • Action<T1, T2> : 引数2つ、戻り値なし
  • (以下、最大16個まで継続)

このように、ジェネリクスを活用することで、どんな型の引数であっても柔軟に対応できるよう設計されています。

Actionデリゲートの基本的な使い方

まずは、Actionデリゲートの最もシンプルな使い方を見ていきましょう。

既存のメソッドをActionに代入し、それを呼び出すまでの流れを確認します。

1. 引数なしのAction

引数を持たず、標準出力にメッセージを表示するだけのシンプルな例です。

C#
using System;

class Program
{
    static void Main()
    {
        // 1. メソッドを変数(Action)に格納
        Action greetAction = SayHello;

        // 2. デリゲート経由でメソッドを実行
        greetAction();
    }

    static void SayHello()
    {
        Console.WriteLine("こんにちは、Actionの世界へ!");
    }
}
実行結果
こんにちは、Actionの世界へ!

2. 引数ありのAction

次に、引数を受け取るActionの使い方です。

ジェネリクス型引数に、受け取りたい型を指定します。

C#
using System;

class Program
{
    static void Main()
    {
        // string型の引数を1つ取るAction
        Action<string> printMessage = DisplayMessage;

        // 引数を渡して実行
        printMessage("C#プログラミング");
    }

    static void DisplayMessage(string message)
    {
        Console.WriteLine($"メッセージ: {message}");
    }
}
実行結果
メッセージ: C#プログラミング

このように、Actionを使用することでメソッドを変数として保持できるため、プログラムの実行時に動的に処理を切り替えることが可能になります。

ラムダ式を用いた簡潔な記述方法

C#では、Actionデリゲートを扱う際に「名前付きメソッド」を定義するのではなく、ラムダ式(Lambda Expression)を用いてその場で処理を記述するのが一般的です。

これにより、コードの記述量を大幅に削減し、可読性を高めることができます。

ラムダ式によるActionの初期化

ラムダ式を使用すると、別途メソッドを定義する手間が省けます。

C#
using System;

class Program
{
    static void Main()
    {
        // ラムダ式を用いた記述
        Action<string, int> showInfo = (name, age) => 
        {
            Console.WriteLine($"{name}さんは{age}歳です。");
        };

        // 実行
        showInfo("田中", 25);
    }
}
実行結果
田中さんは25歳です。

ラムダ式の基本構文は (引数) => { 処理内容 } です。

処理が1行だけの場合は、波括弧 {} を省略することも可能です。

ActionとFuncの違い

Actionデリゲートと非常によく似たものに、Funcデリゲートがあります。

これら2つの最大の違いは、戻り値があるかどうかです。

違いの比較表

特徴ActionFunc
戻り値なし(void)あり(必須)
ジェネリクス指定引数の型のみ指定引数の型 + 最後の1つが戻り値の型
主な用途画面表示、ログ出力、プロパティ設定計算処理、データのフィルタリング、状態取得

Funcデリゲートのコード例

比較のために、Funcデリゲートの使い方も見てみましょう。

C#
using System;

class Program
{
    static void Main()
    {
        // 2つのintを受け取り、intを返すFunc
        Func<int, int, int> add = (a, b) => a + b;

        int result = add(10, 20);
        Console.WriteLine($"合計: {result}");
    }
}

Actionは「実行して終わり」の処理に適しており、Funcは「何かを計算して結果を受け取りたい」場合に適しています。

設計の段階で、戻り値が必要かどうかを基準に使い分けるようにしましょう。

実践的な活用シーン

Actionデリゲートは、単なる変数の代用以上の価値を発揮します。

ここでは、実際の開発現場でよく見られる「コールバック」や「共通処理の分離」での活用例を紹介します。

1. メソッドの引数としてActionを渡す(コールバック)

ある処理が終わった後に、任意の処理を実行させたい場合にActionを引数として渡します。

これは「デザインパターンのTemplate Methodパターン」の簡易版としても機能します。

C#
using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine("データ処理を開始します...");

        // 処理が完了した後のアクションをラムダ式で渡す
        ProcessData("顧客リスト.csv", (fileName) => 
        {
            Console.WriteLine($"{fileName} の処理が正常に完了しました。通知を送ります。");
        });
    }

    static void ProcessData(string fileName, Action<string> onCompleted)
    {
        // 擬似的な重い処理
        Thread.Sleep(2000); 
        Console.WriteLine($"{fileName} を読み込み中...");

        // 完了後にコールバックを実行
        onCompleted?.Invoke(fileName);
    }
}

この構成のメリットは、ProcessData メソッド自体は「完了後に何をするか」を知らなくて済むという点です。

呼び出し側が自由に行動を指定できるため、コンポーネントの再利用性が向上します。

2. List<T>.ForEach での利用

C#のリスト操作において、各要素に対して一括で処理を行いたい場合にActionが活用されます。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        var names = new List<string> { "Alice", "Bob", "Charlie" };

        // ForEachメソッドは Action<T> を引数に取る
        names.ForEach(name => Console.WriteLine($"Hello, {name}!"));
    }
}

従来の foreach 文よりも宣言的に記述できるため、リストに対する単純な反復処理によく使われます。

Actionデリゲートと例外処理

Actionデリゲートを扱う際には、例外(エラー)のハンドリングについても注意が必要です。

デリゲート内部で発生した例外は、そのデリゲートを呼び出した場所に伝播します。

C#
using System;

class Program
{
    static void Main()
    {
        Action riskyAction = () => 
        {
            Console.WriteLine("危険な処理を実行します...");
            throw new InvalidOperationException("エラーが発生しました!");
        };

        try
        {
            riskyAction();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"例外をキャッチしました: {ex.Message}");
        }
    }
}

コールバックとしてActionを渡す設計にする場合、どこで例外をキャッチすべきか(Action内部か、呼び出し元か)を明確にしておくことが、堅牢なアプリケーション開発の鍵となります。

パフォーマンス上の考慮事項

Actionデリゲートは非常に便利ですが、パフォーマンスに極端に厳しい環境(例えば、1秒間に数百万回呼ばれるループ内など)では、わずかなオーバーヘッドが発生することを意識しておく必要があります。

  • デリゲートの生成コスト: ラムダ式や匿名メソッドを頻繁に生成すると、ガベージコレクション(GC)の負荷になる場合があります。
  • クロージャの影響: ラムダ式の外側の変数(ローカル変数など)を参照する場合、コンパイラは隠しクラスを生成します。これを「クロージャ」と呼び、追加のメモリ割り当てが発生します。

とはいえ、一般的な業務アプリケーションやWeb開発においては、Action を使うことによる生産性の向上のほうが、パフォーマンス上の微細なデメリットよりも遥かに大きいため、過度に心配する必要はありません。

マルチキャストデリゲートとしてのAction

Actionデリゲートは「マルチキャストデリゲート」としての性質を持っています。

これは、1つのAction変数に複数のメソッドを登録し、一斉に実行できることを意味します。

C#
using System;

class Program
{
    static void Main()
    {
        Action notificationSystem = () => Console.WriteLine("ログを保存しました。");
        notificationSystem += () => Console.WriteLine("メールを送信しました。");
        notificationSystem += () => Console.WriteLine("ダッシュボードを更新しました。");

        Console.WriteLine("--- 通知処理開始 ---");
        notificationSystem();
        Console.WriteLine("--- 通知処理終了 ---");
    }
}
実行結果
--- 通知処理開始 ---
ログを保存しました。
メールを送信しました。
ダッシュボードを更新しました。
--- 通知処理終了 ---

+= 演算子を使うことで、複数の処理を数珠つなぎに実行できます。

ただし、途中で例外が発生すると後続の処理が実行されないため、その点には注意が必要です。

Actionをより安全に使うためのTips

nullチェックの徹底

Action型の変数が null の状態で呼び出すと、NullReferenceException が発生します。

これを防ぐために、C# 6.0以降で導入された「null条件演算子」を利用するのがベストプラクティスです。

C#
Action myAction = null;

// 安全な呼び出し方法
myAction?.Invoke();

Invoke() メソッドを明示的に呼び出し、その前に ? を付けることで、変数がnullの場合は何もせず、nullでない場合のみ実行されるようになります。

カスタムデリゲートとの使い分け

Actionは汎用的で便利ですが、引数の意味が分かりにくい場合があります。

例えば、Action<string, string, int> という型を見ただけでは、それぞれの引数が何を意味するのか(名前なのか、住所なのか、IDなのか)が判別できません。

複雑なロジックや、公開APIのインターフェースとして定義する場合は、あえて独自のデリゲート名を定義することを検討してください。

C#
// Actionではなく独自のデリゲートを定義して意味を明確にする
delegate void UserRegistrationHandler(string userName, string email, int retryCount);

まとめ

C#のActionデリゲートは、戻り値のないメソッドを柔軟に扱うための標準的な仕組みです。

  • Action は戻り値が void のメソッドに使用する。
  • Func は戻り値が必要な場合に使用する。
  • ラムダ式 と組み合わせることで、簡潔で読みやすいコードが書ける。
  • コールバック として活用することで、処理の関心の分離が可能になる。
  • null条件演算子 (?.) を使って、安全に呼び出すのが基本。

これらの知識をマスターすることで、C#の強みである「柔軟なデリゲート機構」を最大限に引き出し、保守性の高いプログラムを構築できるようになります。

日々の開発の中で、foreach文を List.ForEach に書き換えてみたり、共通処理をActionで切り出してみたりすることから始めてみてください。

デリゲートを使いこなすことが、脱初心者への大きな一歩となるはずです。