Listで使える拡張メソッドを参考にデリケートやラムダ式などに触れてみる

C#

以下の食べ物のリストを用意します。

List<Food> foods = new List<Food>()
{
    new Food{Name="Apple",Type="fruit"},
    new Food{Name="Cabbage",Type="vegetable"},
    new Food{Name="Broccoli",Type="vegetable"},
    new Food{Name="Beef",Type="meat"}
};

このリストから野菜(vegetable)だけを取得するには以下のように記載できます。

var vegetables = foods.Where(food => food.Type == "vegetable");

ちゃんと野菜のみ取得できたかをログに出力してみます。

foreach (var vegetable in vegetables)
{
    _logger.LogInformation(vegetable.Name);
}

取得できていますね。

たった1行で簡単に記述できます。初めのうちは、この「」を使った不思議な書き方に困惑しましたが、慣れれば直感的に書けるようになります。

それでも、なぜこのような書き方なのか、どういう仕組みで動いているのか気になったため、少し深堀してみたいと思います。以下のキーワードを中心に見ていきます。

  • デリゲート
  • 匿名メソッド
  • ラムダ式
  • 拡張メソッド

前提

oswindows11
dotnet8.0.302

実践

まずは何も使わずに実装

とりあえずfoodsから野菜のみを抽出する処理を、先に挙げたキーワードを使わずに実装します。

// 指定された種類の食べ物だけを抽出するメソッド
private List<Food> GetSpecificFoods(IReadOnlyCollection<Food> foods, string foodType)
{
    var list = new List<Food>();
    foreach (var food in foods)
    {
        if (food.Type == foodType) list.Add(food);
    }
    return list;
}

List<Food> vegetables = GetSpecificFoods(foods, "vegetable");

野菜のみを抽出するメソッドを作成して、そのメソッドに「foods」を渡しています。特に何の変哲もないただのメソッドです。

拡張メソッドの作成

先ほどの実装で問題ないのですが、「Where」関数のように、「foods.GetSpecificFoods()」と記載出来たほうが直感的だと思います。では、このような形で記述できるように変更してみましょう。

public static class FoodExtensions
{
    public static List<Food> GetSpecificFoods(this IReadOnlyCollection<Food> foods, string foodType)
    {
        var list = new List<Food>();
        foreach (var food in foods)
        {
            if (food.Type == foodType) list.Add(food);
        }
        return list;
    }
}

var vegetables = foods.GetSpecificFoods("vegetable");

上記のメソッドは拡張メソッドと呼ばれます。既存の型(ここではList<Food>)に独自のメソッドを追加できます。詳細はマイクロソフトのドキュメントを確認いただくとして、簡単に作成する際の注意点を記載します。

  • 静的(static)クラス内に静的メソッドとして定義する
  • 第一引数はメソッドを追加する先の型を「this」を付けて指定する
  • 第二引数以降に実際にメソッドに渡す引数を指定する
  • あとは大体普通のメソッドと同じ

List<T>は、IReadOnlyCollection<T>を実装しています。そのため、IReadOnlyCollection<T>型に対してList<T>型の変数を渡すことができます。作成した拡張メソッド内ではコレクションの中身を変更しないため読み取り専用として受け取っています。もちろん、List<T>として受け取っても問題ありません。

デリゲート

すこし横道にそれますが、ここでデリゲートを紹介します。本当にざっくりと誤解を恐れずに言うと、型の1つです。「int」とか「string」など様々な型がありますが、それらと同じような感じです。intには1や100などの数値を入れることができます。デリケートにはメソッドを入れることができます。以下のように定義します。ここでは「MyPredicate」がデリゲート(型)になります。

public delegate bool MyPredicate<T>(T obj);

引数に何かオブジェクトを受け取り、真偽値を返すメソッドを表します。Food以外のオブジェクトも受け取れるようにしたいので、ジェネリック(T)を使っています。ただし、今までの説明に合わせて以下のように書くともう少し分かりやすいかもしれません。

public delegate bool MyPredicate<Food>(Food foods);

では、ここで定義したデリゲートはどのように使うのでしょうか。以下のコードをご覧ください。まずは定義したデリゲート(型)に合わせて、引数に何かオブジェクトを受け取り、真偽値を返すメソッドを作成します。そして、デリゲート(型)の変数にこのメソッドを代入します。「foodPredicate」は「GetVegetableメソッド」を参照していますので、「foodPredicate」を呼び出すことで、「GetVegetableメソッド」の処理が実行されます。

public static bool GetVegetable(Food food)
{
    return food.Type == "vegetable";
}
MyPredicate<Food> foodPredicate = GetVegetable;

// GetSpecificFood(・・・)が呼ばれて、tureが返る
// foodPredicate.Invoke(・・・)でも可
bool isVegetable = foodPredicate(new Food() { Name = "carot", Type = "vegetable" });

MyPredicate<Food>というデリゲートを定義しましたが、予め用意されているデリケートが存在します。それが「Func」です。Funcを使って書き換えてみます。

Func<Food, bool> foodPredicate = GetVegetable;
var isVegetable = foodPredicate(new Food() { Name = "aaa", Type = "vegetable" });

Func<Food, bool>」は「MyPredicate<Food>」と同じく、引数に何かオブジェクトを受け取り、真偽値を返すメソッドを登録する型です。そのため、GetVegetableメソッドを問題なく格納できます。他にも「Action」もあります。こちらは戻値を持たないメソッド用のものです。

拡張メソッドの修正

ここからは、先ほど作成した拡張メソッド少し変更します。具体的には、引数を「string型」から「Func<T, bool>型」に変更します。デリゲートとして宣言した型です。このような修正を行うことで、今まで文字列を受け取っていましたところが、メソッドを受け取れるようになりました。このメソッドは真偽値を返すため、8行目のif文の条件に指定できます。

public static class FoodExtensions
{
    public static List<T> GetSpecificFoods<T>(this IReadOnlyCollection<T> foods, Func<T, bool> predicate)
    {
        var list = new List<T>();
        foreach (var food in foods)
        {
            if (predicate(food)) list.Add(food);
        }
        return list;
    }
}

では、この拡張メソッドを呼び出してみましょう。

  1. デリゲートを宣言(ActionやFuncを使う場合は省略可)
  2. デリケートと同じ引数と戻り値を持つメソッドを宣言(2~5行目)
  3. メソッドを宣言したデリゲート型の変数に代入
  4. 拡張メソッドをデリゲート型の変数を代入して呼び出す
public delegate bool MyPredicate<T>(T obj); // 省略可
public static bool GetVegetable(Food food)
{
    return food.Type == "vegetable";
}

Func<Food, bool> foodPredicate = GetVegetable;
var vegetables = foods.GetSpecificFoods(foodPredicate);

このコードを実行すると、拡張メソッドGetSpecificFoodsの8行目のif文でGetVegetableメソッドが呼ばれて、種類がvegetableだった場合にリストに追加されて返却されます。

匿名メソッド

  1. デリゲートを宣言する
  2. メソッドを作成する
  3. デリゲートのインスタンスを作成する(メソッドをデリゲート型の変数に代入

この手順を毎回記述するのはやや面倒ですよね。それなら一回でやればいいんじゃない?ということで以下のように書くことができます。引数にメソッドを直接渡していますが、このメソッドには名前(先ほどの例だとGetVegetable)がありません。これを匿名メソッドと言います。

var vegetables = foods.GetSpecificFoods(
    delegate (Food food) { return food.Type == "vegetable"; }
);

このコードを実行しても、今までと同じ結果が得られます。

ラムダ式

匿名メソッドを利用することで、デリケートやメソッドの宣言が不要になりシンプルになりました。しかし、もっとシンプルに記述できると嬉しいですよね。ということで、以下のコードをご覧ください。

var vegetables = foods.GetSpecificFoods(food => food.Type == "vegetable");

なんか変な矢印が登場しましたが、これがラムダ式と呼ばれるものになります。ラムダ式を利用することで匿名メソッドをシンプルに記述することができます。

ちなみに、矢印の左側は引数(Food型)で、右側が関数の中身です。括弧とかreturnとか諸々省略できます。

WhereとFindAll

デリケート、匿名メソッド、ラムダ式などいろいろな要素を経てシンプルに記述できるようになりました。ここで気づいた方もいらっしゃると思いますが、このGetSpecificFoodsという拡張メソッド、どこか既視感ありませんでしょうか。そうです、冒頭に記載したWhereメソッドです。

var vegetables = foods.GetSpecificFoods(food => food.Type == "vegetable");
var vegetables = foods.Where(food => food.Type == "vegetable");

上記2つの文ですが、メソッド名以外全く同じですね。Whereメソッドは以下のように定義されています。もちろん戻り値など型や実装は異なりますが、今回作成した拡張メソッドと似ています。

// 引用: https://learn.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.where?view=net-8.0
public static System.Collections.Generic.IEnumerable<TSource> Where<TSource> 
    (this System.Collections.Generic.IEnumerable<TSource> source, Func<TSource,bool> predicate);

// 今回自作した拡張メソッド
public static List<T> GetSpecificFoods<T>(this IReadOnlyCollection<T> foods, Func<T, bool> predicate)

FindAllというメソッドが存在します。このメソッドもWhereと同じ動きをします。ただし、FindAllは拡張メソッドではありません。Arrayクラスに存在する普通のメソッドです。

var vegetables = foods.FindAll(food => food.Type == "vegetable");

普段プログラミングする際は、デリケートとか匿名メソッドとかを意識することはあまりないと思います。しかし、直感的にシンプルにやりたいことが実現できるWhereメソッドの裏には、今回登場した様々な要素が関係しています。

私自身もまだ完全に理解できていない箇所もありますが、少しでも理解の一助になりましたら幸いです。

コメント