C# delegatesとは?初心者がつまずく使い方・Action/Funcとの違い・実践例をわかりやすく解説

はじめに

C#を学び始めると、ある程度クラスやメソッドに慣れたころに「delegate」という概念が出てきます。

delegateは、C#の中でも初心者がつまずきやすい機能のひとつです。理由は、変数・メソッド・型・イベント・ラムダ式・Action・Funcなど、複数の概念と関係しているためです。

しかし、C# delegatesの考え方そのものはそれほど難しくありません。

一言でいうと、delegateはメソッドを変数のように扱うための仕組みです。

この記事では、C# delegatesとは何か、基本構文、実践的な使い方、Action・Funcとの違い、eventとの関係、初心者がつまずきやすいポイントまで、順番にわかりやすく解説します。

1. C# delegatesとは?まず初心者向けに結論から解説

1-1. delegateは「メソッドを変数のように扱う仕組み」

通常、C#ではメソッドを次のように直接呼び出します。

C#
void SayHello()
{
Console.WriteLine("Hello");
}

SayHello();

一方、delegateを使うと、メソッドを変数に代入してから呼び出せます。

C#
delegate void MessageDelegate();

void SayHello()
{
Console.WriteLine("Hello");
}

MessageDelegate message = SayHello;
message();

この例では、SayHelloというメソッドをmessageというdelegate型の変数に代入しています。

つまりdelegateは、次のようなイメージです。

C#
変数 = メソッド;
変数();

普通の変数には数値や文字列を入れますが、delegate型の変数には処理そのものを入れられます。

1-2. C# delegatesが必要になる場面

C# delegatesは、主に次のような場面で使われます。

処理を外から差し替えたいとき、ある処理が終わったあとに別の処理を呼び出したいとき、条件判定や計算ロジックを柔軟に変更したいとき、イベント処理を実装したいときなどです。

たとえば、次のようなケースを考えてみましょう。

C#
int Add(int a, int b)
{
return a + b;
}

int Subtract(int a, int b)
{
return a - b;
}

足し算をするか、引き算をするかを状況によって切り替えたい場合、delegateを使うと「どの処理を使うか」を外から渡せます。

C#
delegate int CalculateDelegate(int a, int b);

int Execute(int x, int y, CalculateDelegate calculate)
{
return calculate(x, y);
}

このように、delegateを使うとメソッドの中に固定の処理を書き込まず、必要に応じて処理を差し替えられます。

1-3. delegateを使うと何が便利になるのか

delegateを使うメリットは、コードの柔軟性が上がることです。

たとえば、処理A、処理B、処理Cの大部分は同じで、一部の計算や判定だけが違う場合があります。その違う部分だけをdelegateで外から渡せば、共通処理を何度も書かずに済みます。

C#
void Process(int value, Action<int> action)
{
Console.WriteLine("処理開始");
action(value);
Console.WriteLine("処理終了");
}

呼び出し側では、実行したい処理を自由に渡せます。

C#
Process(10, x => Console.WriteLine(x * 2));
Process(10, x => Console.WriteLine(x + 100));

このように、delegateは「共通処理はそのままにして、変えたい処理だけを外から渡す」ために便利です。

1-4. 初心者が混乱しやすいポイント

初心者がC# delegatesで混乱しやすいポイントは、次の4つです。

まず、delegateはメソッドそのものではなく、メソッドを参照するための型です。

次に、delegateに代入できるメソッドは、引数と戻り値の形が一致している必要があります。この形を「シグネチャ」と呼びます。

また、C#では自作delegateだけでなく、ActionFuncというあらかじめ用意されたdelegate型もよく使われます。

さらに、delegateはラムダ式やeventとも深く関係しています。そのため、delegateを理解すると、C#のイベント処理、LINQ、非同期処理、コールバック処理なども読みやすくなります。

2. delegateの基本構文と使い方

2-1. delegate型を宣言する方法

delegateを使うには、まずdelegate型を宣言します。

基本構文は次のとおりです。

C#
delegate 戻り値の型 delegate名(引数);

たとえば、戻り値がなく、引数もないメソッドを扱うdelegateは次のように書きます。

C#
delegate void MyDelegate();

これは、「戻り値がvoidで、引数がないメソッドを代入できるdelegate型」という意味です。

次のようなメソッドを代入できます。

C#
void Hello()
{
Console.WriteLine("こんにちは");
}

一方、次のように引数があるdelegateも宣言できます。

C#
delegate void PrintDelegate(string message);

このdelegateには、string型の引数を1つ受け取り、戻り値がないメソッドを代入できます。

C#
void Print(string message)
{
Console.WriteLine(message);
}

2-2. delegateにメソッドを代入する方法

delegate型を宣言したら、その型の変数にメソッドを代入できます。

C#
delegate void MessageDelegate();

class Program
{
static void Main()
{
MessageDelegate message = SayHello;
message();
}

static void SayHello()
{
Console.WriteLine("Hello");
}
}

ポイントは、代入するときにメソッド名だけを書くことです。

C#
MessageDelegate message = SayHello;

このとき、次のように書く必要はありません。

C#
MessageDelegate message = SayHello();

SayHello()と書くと、メソッドを代入するのではなく、その場でメソッドを実行する意味になってしまいます。

delegateに代入したい場合は、メソッド名だけを書きます。

2-3. delegate経由でメソッドを呼び出す方法

delegateにメソッドを代入したら、通常のメソッドのように呼び出せます。

C#
message();

引数があるdelegateの場合は、呼び出し時に引数を渡します。

C#
delegate void PrintDelegate(string message);

class Program
{
static void Main()
{
PrintDelegate print = PrintMessage;
print("delegate経由で呼び出しました");
}

static void PrintMessage(string message)
{
Console.WriteLine(message);
}
}

実行すると、次のように表示されます。

C#
delegate経由で呼び出しました

delegate変数を通して呼び出しているだけで、実際に実行されるのは代入されたメソッドです。

2-4. 戻り値あり・引数ありのdelegate例

戻り値があるdelegateも作れます。

C#
delegate int CalculateDelegate(int a, int b);

このdelegateには、int型の引数を2つ受け取り、int型を返すメソッドを代入できます。

C#
class Program
{
delegate int CalculateDelegate(int a, int b);

static void Main()
{
CalculateDelegate calc = Add;

int result = calc(10, 5);
Console.WriteLine(result);
}

static int Add(int a, int b)
{
return a + b;
}
}

実行結果は次のとおりです。

C#
15

このように、delegateのシグネチャとメソッドのシグネチャが一致していれば代入できます。

2-5. nullチェックと安全な呼び出し方

delegate変数には、何も代入されていない場合があります。

C#
MessageDelegate message = null;
message();

このようにnullのdelegateを呼び出すと、NullReferenceExceptionが発生します。

そのため、呼び出す前にnullチェックを行います。

C#
if (message != null)
{
message();
}

C#では、null条件演算子を使って次のように書くこともできます。

C#
message?.Invoke();

引数がある場合も同じです。

C#
print?.Invoke("こんにちは");

delegateを安全に呼び出す場合は、?.Invoke()を使う書き方がよく使われます。

3. delegateの実践的なサンプルコード

3-1. 計算処理をdelegateで切り替える例

delegateの代表的な使い方は、処理を切り替えることです。

C#
class Program
{
delegate int CalculateDelegate(int a, int b);

static void Main()
{
Console.WriteLine(Execute(10, 5, Add));
Console.WriteLine(Execute(10, 5, Subtract));
Console.WriteLine(Execute(10, 5, Multiply));
}

static int Execute(int a, int b, CalculateDelegate calculate)
{
return calculate(a, b);
}

static int Add(int a, int b)
{
return a + b;
}

static int Subtract(int a, int b)
{
return a - b;
}

static int Multiply(int a, int b)
{
return a * b;
}
}

実行結果は次のようになります。

C#
15
5
50

Executeメソッドの中では、具体的に足し算なのか引き算なのか掛け算なのかを知りません。

C#
return calculate(a, b);

渡されたdelegateを実行しているだけです。

このように、呼び出し側が処理内容を決められるのがdelegateの大きな特徴です。

3-2. 条件判定をdelegateで外から渡す例

次に、条件判定をdelegateで渡す例を見てみましょう。

C#
class Program
{
delegate bool CheckDelegate(int value);

static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6 };

PrintNumbers(numbers, IsEven);
PrintNumbers(numbers, IsGreaterThanThree);
}

static void PrintNumbers(int[] numbers, CheckDelegate check)
{
foreach (int number in numbers)
{
if (check(number))
{
Console.WriteLine(number);
}
}
}

static bool IsEven(int value)
{
return value % 2 == 0;
}

static bool IsGreaterThanThree(int value)
{
return value > 3;
}
}

PrintNumbersは、どの条件で表示するかを知りません。

C#
if (check(number))

条件の中身は外から渡されます。

IsEvenを渡せば偶数だけ表示し、IsGreaterThanThreeを渡せば3より大きい数だけ表示します。

このように、delegateを使うと「何をするか」だけでなく「どの条件で処理するか」も外から渡せます。

3-3. コールバック処理として使う例

delegateはコールバック処理にもよく使われます。

コールバックとは、ある処理が終わったあとに呼び出される処理のことです。

C#
class Program
{
delegate void CompleteDelegate(string message);

static void Main()
{
Download(OnDownloadComplete);
}

static void Download(CompleteDelegate onComplete)
{
Console.WriteLine("ダウンロード中...");

// 何らかの処理が完了したと仮定
onComplete("ダウンロードが完了しました");
}

static void OnDownloadComplete(string message)
{
Console.WriteLine(message);
}
}

この例では、Downloadメソッドの処理が終わったあとに、onCompleteを呼び出しています。

C#
onComplete("ダウンロードが完了しました");

呼び出し側は、完了時に実行したい処理を自由に指定できます。

3-4. UI・非同期処理・イベント処理で使われる例

delegateは、UIアプリケーションや非同期処理、イベント処理でもよく使われます。

たとえば、ボタンがクリックされたときに特定の処理を実行する場合、「クリックされたらこのメソッドを呼び出す」という形で処理を登録します。

イメージとしては次のような形です。

C#
button.Click += Button_Click;

void Button_Click(object sender, EventArgs e)
{
Console.WriteLine("ボタンがクリックされました");
}

このButton_Clickのようなメソッドは、イベントに登録され、特定のタイミングで呼び出されます。

内部的には、イベント処理とdelegateは深く関係しています。

また、非同期処理でも「処理が終わったあとに何をするか」を渡したい場面があります。このようなときにもdelegateやAction、Funcの考え方が使われます。

3-5. delegateを使わない場合との比較

delegateを使わない場合、処理を切り替えるためにif文やswitch文が増えがちです。

C#
int Execute(int a, int b, string operation)
{
if (operation == "add")
{
return a + b;
}
else if (operation == "subtract")
{
return a - b;
}
else
{
return 0;
}
}

この書き方でも動きますが、処理の種類が増えるたびにExecuteメソッドを修正する必要があります。

delegateを使うと、Executeメソッド自体は変更せずに、外から処理を渡せます。

C#
int Execute(int a, int b, Func<int, int, int> operation)
{
return operation(a, b);
}

呼び出し側は次のように書けます。

C#
int result = Execute(10, 5, (x, y) => x + y);

delegateを使うことで、処理の追加や変更に強いコードになります。

4. ActionとFuncとは?delegateとの違い

4-1. Actionは戻り値なしのdelegate

Actionは、C#にあらかじめ用意されているdelegate型です。

戻り値がないメソッドを扱うときに使います。

C#
Action action = SayHello;
action();

void SayHello()
{
Console.WriteLine("Hello");
}

引数がある場合は、型引数を指定します。

C#
Action<string> print = PrintMessage;
print("こんにちは");

void PrintMessage(string message)
{
Console.WriteLine(message);
}

Action<string>は、「string型の引数を1つ受け取り、戻り値がない処理」を表します。

複数の引数も指定できます。

C#
Action<string, int> show = (name, age) =>
{
Console.WriteLine($"{name}さんは{age}歳です");
};

show("田中", 25);

4-2. Funcは戻り値ありのdelegate

FuncもC#にあらかじめ用意されているdelegate型です。

戻り値があるメソッドを扱うときに使います。

C#
Func<int, int, int> add = Add;

int result = add(10, 5);
Console.WriteLine(result);

int Add(int a, int b)
{
return a + b;
}

Func<int, int, int>の意味は、次のとおりです。

C#
Func<第1引数の型, 第2引数の型, 戻り値の型>

最後の型が戻り値の型です。

つまり、Func<int, int, int>は「intを2つ受け取り、intを返す処理」を表します。

引数が1つの場合は次のように書きます。

C#
Func<int, bool> isEven = x => x % 2 == 0;

これは「intを1つ受け取り、boolを返す処理」です。

4-3. 自作delegate・Action・Funcの使い分け

C#では、自作delegate、Action、Funcを使い分ける必要があります。

簡単な処理であれば、基本的にはActionFuncを使うことが多いです。

C#
Action<string> log = message => Console.WriteLine(message);
Func<int, int, int> add = (a, b) => a + b;

一方で、自作delegateを使うと、処理に意味のある名前をつけられます。

C#
delegate int PriceCalculator(int price, int taxRate);

このように書くと、単なるFunc<int, int, int>よりも「価格計算をする処理」だとわかりやすくなります。

C#
Func<int, int, int> calculator;
PriceCalculator calculator;

どちらも似たような処理を表せますが、後者の方が業務上の意味を伝えやすい場合があります。

4-4. Predicateとの違い

Predicate<T>もC#に用意されているdelegate型です。

Predicate<T>は、T型の値を1つ受け取り、boolを返す処理を表します。

C#
Predicate<int> isEven = x => x % 2 == 0;

Console.WriteLine(isEven(4));

これは次のFuncとほぼ同じ意味です。

C#
Func<int, bool> isEven = x => x % 2 == 0;

どちらも条件判定に使えます。

ただし、Predicate<T>は「条件を満たすかどうかを判定する」という意味が明確です。

C#
Predicate<string> isLongText = text => text.Length >= 10;

条件判定であることを強調したい場合はPredicate<T>、一般的な戻り値ありの処理として扱いたい場合はFunc<T, bool>を使うとよいでしょう。

4-5. 初心者はどれを使えばよいか

初心者は、まず次の基準で考えるとわかりやすいです。

戻り値がない処理ならActionを使います。

C#
Action<string> print = message => Console.WriteLine(message);

戻り値がある処理ならFuncを使います。

C#
Func<int, int, int> add = (a, b) => a + b;

条件判定ならPredicateまたはFunc<T, bool>を使います。

C#
Predicate<int> isEven = x => x % 2 == 0;

処理に業務的な意味のある名前をつけたい場合は、自作delegateを使います。

C#
delegate bool UserValidator(User user);

最初からすべてを使い分けようとせず、まずはActionFuncを理解すると、C# delegates全体のイメージがつかみやすくなります。

5. delegate・匿名メソッド・ラムダ式の関係

5-1. 匿名メソッドとは

匿名メソッドとは、名前のないメソッドのことです。

通常のメソッドには名前があります。

C#
void SayHello()
{
Console.WriteLine("Hello");
}

匿名メソッドでは、メソッド名を書かずに、その場で処理を定義します。

C#
Action action = delegate()
{
Console.WriteLine("Hello");
};

action();

このdelegate() { ... }の部分が匿名メソッドです。

わざわざ別のメソッドを定義しなくても、その場でdelegateに処理を代入できます。

5-2. ラムダ式とは

ラムダ式は、匿名メソッドをさらに簡潔に書くための構文です。

先ほどの匿名メソッドは、ラムダ式で次のように書けます。

C#
Action action = () =>
{
Console.WriteLine("Hello");
};

さらに1行で書くこともできます。

C#
Action action = () => Console.WriteLine("Hello");

引数がある場合は次のように書きます。

C#
Action<string> print = message => Console.WriteLine(message);

戻り値がある場合はFuncと組み合わせることが多いです。

C#
Func<int, int, int> add = (a, b) => a + b;

ラムダ式は、delegateに代入できる処理を短く書くための便利な書き方です。

5-3. delegateをラムダ式で簡潔に書く方法

自作delegateにもラムダ式を代入できます。

C#
delegate int CalculateDelegate(int a, int b);

class Program
{
static void Main()
{
CalculateDelegate add = (a, b) => a + b;
CalculateDelegate subtract = (a, b) => a - b;

Console.WriteLine(add(10, 5));
Console.WriteLine(subtract(10, 5));
}
}

通常のメソッドを別に用意する必要がないため、短い処理であればラムダ式の方が読みやすくなります。

ただし、処理が長くなる場合は、ラムダ式に詰め込みすぎると読みにくくなります。

C#
CalculateDelegate complex = (a, b) =>
{
int result = a + b;
result *= 2;
return result;
};

複雑な処理になってきたら、名前付きメソッドに分ける方がよい場合もあります。

5-4. ラムダ式がAction/Funcと一緒に使われる理由

ラムダ式は、ActionFuncと非常に相性がよいです。

たとえば、戻り値がない処理はActionで受け取れます。

C#
Action<string> log = message => Console.WriteLine($"ログ: {message}");

戻り値がある処理はFuncで受け取れます。

C#
Func<int, bool> isEven = number => number % 2 == 0;

LINQでも、ラムダ式とFuncがよく使われます。

C#
var numbers = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = numbers.Where(x => x % 2 == 0);

Whereに渡しているx => x % 2 == 0は、条件判定の処理です。

このように、C#では「処理を引数として渡す」場面で、delegate、Action、Func、ラムダ式が一緒に使われます。

5-5. 読みやすい書き方と避けたい書き方

ラムダ式は便利ですが、何でもラムダ式で書けばよいわけではありません。

短くて意味が明確な処理ならラムダ式が向いています。

C#
Func<int, bool> isEven = x => x % 2 == 0;

しかし、処理が長くなる場合は読みにくくなります。

C#
Func<Order, bool> check = order =>
{
if (order == null)
{
return false;
}

if (order.Price <= 0)
{
return false;
}

if (order.CustomerName == "")
{
return false;
}

return true;
};

このような場合は、名前付きメソッドにした方が意図が伝わりやすくなります。

C#
bool IsValidOrder(Order order)
{
if (order == null)
{
return false;
}

if (order.Price <= 0)
{
return false;
}

if (order.CustomerName == "")
{
return false;
}

return true;
}

読みやすいコードを書くためには、ラムダ式を使うか、メソッドに分けるかを状況に応じて判断することが大切です。

6. delegateとeventの違い

6-1. eventはdelegateを安全に扱うための仕組み

C#のeventは、delegateをもとにした仕組みです。

delegateはメソッドを参照できますが、そのまま公開すると外部から自由に代入されたり、呼び出されたりする可能性があります。

そこで、イベント処理ではeventを使います。

C#
public event Action OnCompleted;

eventを使うと、外部からは基本的に+=でメソッドを登録し、-=で解除する操作が中心になります。

C#
worker.OnCompleted += ShowMessage;
worker.OnCompleted -= ShowMessage;

一方、イベントを発火する処理は、通常そのクラスの内部で行います。

C#
OnCompleted?.Invoke();

つまり、eventはdelegateをイベント用途で安全に使うための仕組みです。

6-2. delegateだけでイベント処理を書く場合の問題点

delegateをpublicで公開すると、外部から直接代入できてしまいます。

C#
public Action OnCompleted;

この場合、外部から次のように書けます。

C#
worker.OnCompleted = SomeMethod;

さらに別の場所で代入すると、前に登録されていた処理が上書きされる可能性があります。

C#
worker.OnCompleted = AnotherMethod;

また、外部から直接呼び出せる場合もあります。

C#
worker.OnCompleted();

これはイベント処理としては危険です。

イベントは本来、「登録は外部からできるが、発火は内部から行う」という形が望ましいです。

そのため、イベント処理ではdelegateそのものを公開するのではなく、eventを使います。

6-3. eventを使った基本サンプル

eventを使った基本例を見てみましょう。

C#
class Worker
{
public event Action Completed;

public void DoWork()
{
Console.WriteLine("作業中...");

Completed?.Invoke();
}
}

class Program
{
static void Main()
{
Worker worker = new Worker();

worker.Completed += OnCompleted;

worker.DoWork();
}

static void OnCompleted()
{
Console.WriteLine("作業が完了しました");
}
}

実行結果は次のようになります。

C#
作業中...
作業が完了しました

Program側では、CompletedイベントにOnCompletedメソッドを登録しています。

C#
worker.Completed += OnCompleted;

Workerクラスの中で作業が終わると、イベントが発火します。

C#
Completed?.Invoke();

6-4. ボタンクリックや通知処理での使われ方

C#のUIアプリケーションでは、ボタンクリックなどの処理でeventがよく使われます。

C#
button.Click += Button_Click;

void Button_Click(object sender, EventArgs e)
{
Console.WriteLine("ボタンがクリックされました");
}

このようなイベント処理では、「ボタンがクリックされた」という出来事に対して、実行するメソッドを登録します。

通知処理でも同じ考え方が使えます。

C#
class Notifier
{
public event Action<string> Notified;

public void Notify(string message)
{
Notified?.Invoke(message);
}
}

呼び出し側は、通知されたときに実行したい処理を登録します。

C#
Notifier notifier = new Notifier();

notifier.Notified += message => Console.WriteLine($"通知: {message}");

notifier.Notify("新しいメッセージがあります");

このように、eventは「何かが起きたときに処理を実行する」ためによく使われます。

6-5. delegateとeventの使い分け

delegateとeventは似ていますが、使い分けの基準があります。

処理そのものを引数として渡したい場合はdelegate、Action、Funcを使います。

C#
void Execute(Action action)
{
action();
}

一方、何かが起きたことを外部に通知したい場合はeventを使います。

C#
public event Action Completed;

delegateは「処理を渡す」ための仕組みです。

eventは「出来事を通知する」ための仕組みです。

この違いを意識すると、使い分けがしやすくなります。

7. 初心者がつまずきやすいエラーと解決方法

7-1. シグネチャが一致しないエラー

delegateにメソッドを代入するには、引数と戻り値の形が一致している必要があります。

たとえば、次のdelegateを考えます。

C#
delegate void PrintDelegate(string message);

このdelegateには、stringを1つ受け取り、戻り値がないメソッドを代入できます。

C#
void Print(string message)
{
Console.WriteLine(message);
}

しかし、次のメソッドは代入できません。

C#
void Print()
{
Console.WriteLine("Hello");
}

引数が一致していないためです。

また、次のメソッドも代入できません。

C#
void Print(int number)
{
Console.WriteLine(number);
}

引数の数だけでなく、型も一致している必要があります。

エラーが出たら、まずdelegateの宣言と代入しているメソッドの引数を確認しましょう。

7-2. 戻り値の型が違うエラー

戻り値の型も一致している必要があります。

C#
delegate int CalculateDelegate(int a, int b);

このdelegateには、intを返すメソッドを代入できます。

C#
int Add(int a, int b)
{
return a + b;
}

しかし、戻り値がvoidのメソッドは代入できません。

C#
void Add(int a, int b)
{
Console.WriteLine(a + b);
}

また、戻り値の型が違う場合もエラーになります。

C#
string Add(int a, int b)
{
return (a + b).ToString();
}

delegateを使うときは、引数だけでなく戻り値の型も必ず確認しましょう。

7-3. staticメソッドとインスタンスメソッドの混乱

delegateには、staticメソッドもインスタンスメソッドも代入できます。

staticメソッドの場合は、クラス名から参照できます。

C#
Action action = Program.SayHello;

static void SayHello()
{
Console.WriteLine("Hello");
}

インスタンスメソッドの場合は、インスタンスを作ってから代入します。

C#
class Greeter
{
public void SayHello()
{
Console.WriteLine("Hello");
}
}

Greeter greeter = new Greeter();

Action action = greeter.SayHello;
action();

初心者が混乱しやすいのは、インスタンスメソッドをクラス名だけで代入しようとするケースです。

C#
Action action = Greeter.SayHello;

これは、SayHelloがstaticではない場合エラーになります。

インスタンスメソッドを使う場合は、必ずオブジェクトを作成してから参照しましょう。

7-4. nullのdelegateを呼び出してしまう問題

delegate変数に何も代入されていない状態で呼び出すと、例外が発生します。

C#
Action action = null;
action();

このようなコードは危険です。

安全に呼び出すには、nullチェックを行います。

C#
if (action != null)
{
action();
}

または、次のように書きます。

C#
action?.Invoke();

eventでも同じように、発火時にはnullチェックをするのが一般的です。

C#
Completed?.Invoke();

delegateやeventを呼び出すときは、何も登録されていない可能性を考慮しましょう。

7-5. ラムダ式の引数・戻り値で迷うケース

ラムダ式を使うときは、ActionなのかFuncなのかを確認することが大切です。

戻り値がない場合はActionです。

C#
Action<string> print = message => Console.WriteLine(message);

戻り値がある場合はFuncです。

C#
Func<int, int> doubleValue = x => x * 2;

次のように、戻り値が必要なFuncなのに値を返していないとエラーになります。

C#
Func<int, int> doubleValue = x =>
{
Console.WriteLine(x * 2);
};

この場合、returnが必要です。

C#
Func<int, int> doubleValue = x =>
{
return x * 2;
};

または1行で書きます。

C#
Func<int, int> doubleValue = x => x * 2;

ラムダ式で迷ったら、「引数は何か」「戻り値はあるか」を確認しましょう。

8. delegateを使うべき場面・使わなくてよい場面

8-1. 処理を外から差し替えたいとき

delegateを使うべき代表的な場面は、処理を外から差し替えたいときです。

C#
void Execute(Action action)
{
Console.WriteLine("開始");
action();
Console.WriteLine("終了");
}

呼び出し側は、実行したい処理を自由に渡せます。

C#
Execute(() => Console.WriteLine("処理A"));
Execute(() => Console.WriteLine("処理B"));

このように、共通の流れは変えず、一部の処理だけを差し替えたい場合にdelegateは便利です。

8-2. 共通処理の一部だけ変えたいとき

たとえば、リストの中から条件に合うデータだけを処理したい場合を考えます。

C#
void PrintFilteredNumbers(int[] numbers, Func<int, bool> condition)
{
foreach (int number in numbers)
{
if (condition(number))
{
Console.WriteLine(number);
}
}
}

呼び出し側では、条件だけを変えられます。

C#
int[] numbers = { 1, 2, 3, 4, 5 };

PrintFilteredNumbers(numbers, x => x % 2 == 0);
PrintFilteredNumbers(numbers, x => x >= 3);

このように、処理の流れは同じで、判定条件だけ違う場合にもdelegateは向いています。

8-3. コールバックを実装したいとき

処理が終わったあとに別の処理を呼び出したい場合も、delegateが使えます。

C#
void LoadData(Action onCompleted)
{
Console.WriteLine("データを読み込み中...");

onCompleted?.Invoke();
}

呼び出し側では、完了時の処理を渡します。

C#
LoadData(() => Console.WriteLine("読み込み完了"));

このようなコールバック処理は、非同期処理やイベント処理の考え方にもつながります。

8-4. Action/Funcで十分な場面

短い処理や一時的な処理であれば、自作delegateを作らずにActionやFuncを使えば十分です。

C#
Action<string> log = message => Console.WriteLine(message);
Func<int, int, int> add = (a, b) => a + b;

特に、メソッドの引数として処理を渡すだけなら、ActionやFuncの方が簡潔に書けます。

C#
void Process(Func<int, bool> condition)
{
// 条件に応じた処理
}

ただし、意味のある名前をつけたい場合は、自作delegateを検討してもよいでしょう。

C#
delegate bool UserFilter(User user);

8-5. interfaceや抽象クラスを使った方がよい場面

delegateは便利ですが、すべての設計に向いているわけではありません。

処理が1つだけならdelegateで十分なことが多いです。

C#
Func<int, int, int> calculate;

しかし、関連する複数の処理をまとめて扱いたい場合は、interfaceの方が向いています。

C#
interface IPaymentService
{
void Pay(int amount);
void Cancel();
void Refund();
}

このように、複数のメソッドや状態をまとめて扱う場合は、delegateよりもinterfaceや抽象クラスの方が設計しやすくなります。

delegateは「ひとつの処理を渡す」ときに便利です。

interfaceは「まとまった役割や振る舞いを表す」ときに便利です。

9. C# delegatesを理解するための設計イメージ

9-1. delegateは「処理そのもの」を渡す考え方

C# delegatesを理解するうえで大切なのは、delegateを「処理そのものを渡す仕組み」と考えることです。

通常、メソッドには値を渡します。

C#
Print("Hello");

delegateを使うと、値ではなく処理を渡せます。

C#
Execute(() => Console.WriteLine("Hello"));

これは、プログラムの設計において非常に重要な考え方です。

処理を固定せず、外から渡せるようにすると、コードの再利用性が高くなります。

9-2. Strategyパターンとの関係

delegateは、Strategyパターンに近い考え方で使えます。

Strategyパターンとは、アルゴリズムや処理方法を切り替えられるようにする設計パターンです。

たとえば、割引計算を切り替える例を考えます。

C#
decimal CalculatePrice(decimal price, Func<decimal, decimal> discountStrategy)
{
return discountStrategy(price);
}

呼び出し側では、割引方法を変えられます。

C#
decimal normal = CalculatePrice(1000, price => price);
decimal tenPercentOff = CalculatePrice(1000, price => price * 0.9m);
decimal fixedDiscount = CalculatePrice(1000, price => price - 100);

このように、delegateを使うと、処理の切り替えを簡潔に表現できます。

ただし、処理が複雑になったり、複数のメソッドをまとめて扱いたくなったりした場合は、interfaceを使ったStrategyパターンの方が適していることもあります。

9-3. LINQでFuncがよく使われる理由

C#のLINQでは、Funcが非常によく使われます。

たとえば、Whereは条件に合う要素を取り出すために使います。

C#
var numbers = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = numbers.Where(x => x % 2 == 0);

このx => x % 2 == 0は、各要素を受け取ってboolを返す処理です。

つまり、イメージとしては次のようなFuncです。

C#
Func<int, bool> condition = x => x % 2 == 0;

Selectでは、要素を別の形に変換する処理を渡します。

C#
var doubled = numbers.Select(x => x * 2);

これも、値を受け取って別の値を返すFuncです。

LINQでは、「どの条件で絞り込むか」「どのように変換するか」を外から渡す必要があります。そのため、Funcとラムダ式が多用されます。

9-4. 実務コードでdelegateを読むときのコツ

実務コードでdelegate、Action、Funcが出てきたときは、まず型を見ることが大切です。

C#
Action<string> log

これは、stringを受け取り、戻り値がない処理です。

C#
Func<int, bool> condition

これは、intを受け取り、boolを返す処理です。

C#
Func<int, int, int> calculate

これは、intを2つ受け取り、intを返す処理です。

Funcの場合、最後の型が戻り値です。

また、メソッドの引数にActionやFuncがある場合は、「このメソッドは外から処理を受け取るのだな」と考えると理解しやすくなります。

C#
void Process(Action onCompleted)

この場合、Processの中で何らかのタイミングでonCompletedが呼ばれる可能性があります。

C#
IEnumerable<User> FilterUsers(Func<User, bool> condition)

この場合、Userを受け取ってboolを返す条件判定を外から渡す設計だとわかります。

delegateを読むときは、名前よりも先に「引数」と「戻り値」を確認すると、コードの意図をつかみやすくなります。

まとめ

C# delegatesは、メソッドを変数のように扱い、処理そのものを渡すための仕組みです。

最初は難しく感じるかもしれませんが、基本は「delegate型を宣言する」「メソッドを代入する」「delegate経由で呼び出す」という流れです。

C#
delegate void MessageDelegate();

MessageDelegate message = SayHello;
message();

delegateを使うと、処理を外から差し替えたり、共通処理の一部だけを変更したり、コールバックやイベント処理を実装したりできます。

また、C#では自作delegateだけでなく、ActionFuncがよく使われます。

戻り値がない処理はAction、戻り値がある処理はFunc、条件判定はPredicateFunc<T, bool>を使うと考えるとわかりやすいです。

ラムダ式を使えば、delegateに渡す処理を簡潔に書けます。

C#
Action<string> print = message => Console.WriteLine(message);
Func<int, int, int> add = (a, b) => a + b;

さらに、eventはdelegateをイベント用途で安全に扱うための仕組みです。

delegateは「処理を渡す」、eventは「出来事を通知する」と考えると整理しやすくなります。

C# delegatesを理解すると、Action、Func、ラムダ式、LINQ、event、非同期処理など、C#の重要な機能が一気につながって見えてきます。まずは簡単なサンプルから、処理を変数に入れて呼び出す感覚をつかんでいきましょう。