C#入門:Funcで学ぶ「戻り値を返すデリゲート」

〜Actionとの違い、使いどころ、LINQ/WinForms応用まで〜

はじめに

前回は Action(戻り値なし) を使って「メソッドを変数にする」感覚をつかみました。

今回は Func。読み方は「ファンク」。戻り値を返すメソッドを変数に入れて扱える“箱”です。

  • Action<T1, T2, …>:引数あり・戻り値なし
  • Func<T1, T2, …, TResult>:引数あり・戻り値あり(最後が戻り値の型)

1. Func の基本:シグネチャの読み方

1-1. 型パラメータの最後が戻り値

  • Func<int> … 引数0個、戻り値 int
  • Func<int, string> … 引数1個(int)、戻り値 string
  • Func<int, int, bool> … 引数2個(int,int)、戻り値 bool

1-2. いちばん小さな例

using System;

class Program
{
    static void Main()
    {
        Func<int> getRandom = () => new Random().Next(1, 7); // 1〜6のサイコロ
        int x = getRandom();
        Console.WriteLine($"出目: {x}");
    }
}

2. クイック比較:Action vs Func

目的代表型
戻り値なしで通知・副作用を起こすAction<string>ログ出力、イベント通知
戻り値ありで値を計算・変換するFunc<int, int>ダメージ計算、数値変換

覚え方

  • 「何かする」= Action(副作用)
  • 「何か出す」= Func(返り値)

3. 図解で理解:Func は「値を返す受け渡し口」

3-1. クラス図(概念)

3-2. シーケンス図:呼び出しの流れ


4. 典型パターン:定義・代入・呼び出し

4-1. メソッド参照で代入

int Double(int n) => n * 2;

Func<int, int> doubler = Double;
Console.WriteLine(doubler(21)); // 42

4-2. ラムダ式で代入

Func<string, int> length = s => s.Length;
Console.WriteLine(length("hello")); // 5

4-3. 無名メソッド(delegate)で代入

Func<int, bool> isEven = delegate (int n) { return n % 2 == 0; };
Console.WriteLine(isEven(4)); // True

5. Func の合成と高階関数

5-1. 関数合成(g∘f:まず f、次に g)

Func<int, int> f = x => x + 3;
Func<int, int> g = x => x * 2;

int Compose(Func<int,int> g1, Func<int,int> f1, int x) => g1(f1(x));
Console.WriteLine(Compose(g, f, 4)); // g(f(4)) = (4+3)*2 = 14

5-2. 戻り値で関数を返す(カリー化の入口)

Func<int, Func<int, int>> add = a => (b => a + b);
var add10 = add(10);
Console.WriteLine(add10(5)); // 15

LINQ の多くの拡張メソッドは Func を受け取る ことで有名です。

using System;
using System.Linq;

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

// 変換: Select(Func<TSource, TResult>)
var squares = numbers.Select(n => n * 2);

// 抽出: Where(Func<TSource, bool>)
var evens = numbers.Where(n => n % 2 == 0);

// キー選択: OrderBy(Func<TSource, TKey>)
var sorted = numbers.OrderBy(n => -n);

Console.WriteLine(string.Join(",", squares)); // 2,4,6,8,10,12
Console.WriteLine(string.Join(",", evens));   // 2,4,6
Console.WriteLine(string.Join(",", sorted));  // 6,5,4,3,2,1

ポイント:Where は Func<T,bool> を受け取り、真偽でフィルタします。

Select は Func<T,TResult> を受け取り、変換します。


7. WinForms 応用:Func<int, int> でダメージ計算、Action<string> で表示

前回のログ通知サンプルに、「ダメージを計算して返す」Func を組み合わせます。

7-1. 構成図

7-2. コード例

using System;
using System.Windows.Forms;

namespace FuncWinFormsSample
{
    public partial class Form1 : Form
    {
        private Action<string>? Log;
        private Func<int, int>? DamageCalc;

        private int playerAtk = 8;
        private int enemyHp   = 35;

        public Form1()
        {
            InitializeComponent();

            // 表示は Action<string>
            Log = msg => txtLog.AppendText(msg + Environment.NewLine);

            // 計算は Func<int,int>(引数: 攻撃力 → 戻り値: ダメージ)
            var rnd = new Random();
            DamageCalc = atk =>
            {
                // 例:±25%の乱数補正 + クリティカル10%
                double factor = 0.75 + 0.5 * rnd.NextDouble(); // 0.75〜1.25
                int baseDmg = (int)Math.Round(atk * factor);
                bool crit = rnd.NextDouble() < 0.10;
                return crit ? baseDmg * 2 : baseDmg;
            };
        }

        private void btnAttack_Click(object sender, EventArgs e)
        {
            if (DamageCalc is null) return;

            int dmg = Math.Max(1, DamageCalc(playerAtk));
            enemyHp = Math.Max(0, enemyHp - dmg);

            Log?.Invoke($"攻撃! {dmg} ダメージ  残りHP: {enemyHp}");
            if (enemyHp == 0) Log?.Invoke("敵を倒した!");
        }
    }
}

利点

  • 見た目(表示) と ロジック(計算) を Action/Func で分離。
  • ダメージ式の差し替え(バフ・デバフ・難易度変更)が容易。
  • テスト時は DamageCalc = atk => 10; のように固定化できる。

8. よくあるつまずきと対策

  1. null 呼び出し
  • DamageCalc(playerAtk) の前に null チェック(if (DamageCalc is null) return;)
  • あるいは 必ず代入しておく設計(コンストラクタで必ず設定)
  1. 例外の握り潰し
  • Func 内で例外が起きたら、呼び出し側に伝播します。必要なら try-catch で握る。
  1. クロージャの罠
  • ループ内でラムダが 同じ変数をキャプチャして意図しない動作になることがあります。ループ変数を一旦ローカルにコピーしてからラムダへ渡すのが安全。
  1. 長時間処理
  • Func が重い計算をするなら、UIスレッドをブロックしない(Task.Run などを検討)。まずは同期で体験 → 必要になったら非同期へ。

9. まとめ:設計の指針

  • 副作用を起こすなら Action、値を返すなら Func
  • UI とロジックを分離して テスタブル にする
  • LINQ では Func が“第一言語”レベルで使われる
  • 小さく定義して 合成 することで、保守性・再利用性が上がる

指針

  • 「ここは結果が欲しいだけ」→ Func
  • 「ここはやって欲しいだけ」→ Action

10. 追加図解:Func<T, bool> は「判定器(Predicate)」の別名パターン

List<T>.Find(Predicate<T>) などは Predicate<T> を要求しますが、ラムダ x => 条件(Func<T,bool>)をそのまま渡せる場面が多いです。


付録:小さな演習

  1. Func<double, double> で 消費税計算(税率を外から差し替え)
  2. Func<string, string> で 入力整形パイプライン(Trim → ToUpper → Suffix 付与)
  3. Func<int,int,bool> で 矩形内判定((x,y) を引数に取りたい場合は Func<Point,bool> のようにラップ)

1. Func<double,double> で消費税計算

要件:税率を差し替え可能にしたい

using System;

class Program
{
    static void Main()
    {
        // 税率を外から差し替えられる
        double taxRate = 0.1; // 10%
        Func<double, double> calcTax = price => price * (1 + taxRate);

        Console.WriteLine(calcTax(1000)); // 1100
        Console.WriteLine(calcTax(250));  // 275

        // 税率を変更
        taxRate = 0.08; // 8%
        Console.WriteLine(calcTax(1000)); // 1080
    }
}

👉 ポイント

  • 「商品価格 → 税込み価格」という 計算関数を変数化
  • 税率を外から変えるだけでロジックを差し替えられる。

2. Func<string, string> で入力整形パイプライン

要件:ユーザー入力を「Trim → 大文字化 → サフィックス追加」と順番に処理したい

using System;

class Program
{
    static void Main()
    {
        // ステップごとの関数
        Func<string, string> trim   = s => s.Trim();
        Func<string, string> upper  = s => s.ToUpper();
        Func<string, string> suffix = s => s + "_USER";

        // パイプライン合成
        string Process(string input)
        {
            string result = input;
            result = trim(result);
            result = upper(result);
            result = suffix(result);
            return result;
        }

        Console.WriteLine(Process("  hello ")); // "HELLO_USER"
    }
}

👉 ポイント

  • 処理を「小さな部品(Func)」に分割
  • 順に適用することで 見通しの良いデータ変換パイプライン ができる

3. Func<int,int,bool> で矩形内判定

要件:座標 (x,y) が矩形の中にあるかどうか判定

using System;
using System.Drawing; // Point 構造体を使う場合はこれを追加

class Program
{
    static void Main()
    {
        // (x,y) を直接受け取るバージョン
        Func<int, int, bool> isInside = (x, y) =>
        {
            int left = 10, top = 10, right = 50, bottom = 30;
            return (left <= x && x <= right &&
                    top <= y && y <= bottom);
        };

        Console.WriteLine(isInside(20, 20)); // True
        Console.WriteLine(isInside(5, 5));   // False

        // Point でラップしたバージョン
        Func<Point, bool> isInsidePoint = p =>
        {
            int left = 10, top = 10, right = 50, bottom = 30;
            return (left <= p.X && p.X <= right &&
                    top <= p.Y && p.Y <= bottom);
        };

        Console.WriteLine(isInsidePoint(new Point(20, 20))); // True
        Console.WriteLine(isInsidePoint(new Point(5, 5)));   // False
    }
}

👉 ポイント

  • Func<int,int,bool> は「座標判定関数」として使える。
  • Func<Point,bool> にすると 引数をまとめて扱える のでスッキリする。

演習まとめ

  • 計算処理(税計算) → Func<double,double>
  • 文字列変換(入力整形) → Func<string,string> のパイプライン
  • 条件判定(座標チェック) → Func<int,int,bool> や Func<Point,bool>

これらを押さえると、Action は副作用、Func は変換/判定 という役割がしっかり見えてきます。

訪問数 2 回, 今日の訪問数 3回

C#,デリゲート

Posted by hidepon