[C#] 30. Linq関数式を使う方法


Study / C#    作成日付 : 2019/07/17 23:06:42   修正日付 : 2021/09/15 19:30:49

こんにちは。明月です。


この投稿はC#のLinq関数式を使う方法に関する説明です。


以前の投稿でLinq式に関して簡単な説明とLinqクエリ式に関して説明したことがあります。

link - [C#] 28. リスト(List)とディクショナリ(Dictionary)、そしてLinq式を使い方

link - [C#] 29. Linqクエリ式を使い方


Linq式に関してはプログラム内のオブジェクト(Object)を効果的にフィルター、建さんしてデータを分類する文法です。

クエリ式の場合はSQLクエリみたいに作成してデータを分類する方法です。

クエリ式の利点はSQLクエリ式に慣れている方にはこの文法が慣れしやすいですが、少しプログラム文法と違和感があるし、途中でデバッグ状況を確認することが難しいので使うことでお勧めではありません。


それでLinq式にはプログラム関数と同じLinq関数式があります。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // 例クラス
  class Node
  {
    // 値をコンストラクタで格納する。
    public Node(int data)
    {
      // プロパティのDataに値を格納
      this.Data = data;
    }
    // Dataプロパティ
    public int Data
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リスト宣言(リストのオブジェクトはNodeクラス)
      var list = new List<Node>();
      // iが0から9まで繰り返し
      for (int i = 0; i < 10; i++)
      {
        // リストにデータを挿入
        list.Add(new Node(i));
      }
      // NodeクラスのData値が6以上のインスタンスをList<int>タイプで分類する。
      var filterList = list.Where(x => x.Data > 5).Select(x => x.Data);
      // フィルターされたリストを繰り返しで抽出
      foreach (var node in filterList)
      {
        // コンソール出力
        Console.WriteLine(node);
      }
 
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


上のソースではクエリ式のfrom node in list where node.Data > 5 select node.Data;をlist.Where(x => x.Data > 5).Select(x => x.Data);で作成しました。

ここでWhere関数とSelect関数の中ではラムダ式を使ってフィルターと検索条件を生成しました。

そしてWhere関数の返却値とSelect関数の返却値はIEnumerableタイプなので、関数を連結するみたいにチェインパターンで生成することができます。


つまり、上みたいにチェインパターンではなく、一行目ずつ作成することもできます。(参考、Selectの場合はReturn値によりジェネリックタイプが変わるので注意)


基本的にLinq関数式の場合はWhereとSelectをよく使いますが、仕様により、Joinと整列、グループ別に分離することもできます。

First, FirstOrDefault, Single, SingleOrDefault, Last, LastOrDefault

この関数はリストの結果を一つの結果に受け取る場合に使います。最終の返却値はListではなく、Listのジェネリックタイプにより結果を返却します。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // 例クラス
  class Node
  {
    // 値をコンストラクタで格納する。
    public Node(int data)
    {
      // プロパティのDataに値を格納
      this.Data = data;
    }
    // Dataプロパティ
    public int Data
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リスト宣言(リストのオブジェクトはNodeクラス)
      var list = new List<Node>();
      // iが0から9まで繰り返し
      for (int i = 0; i < 10; i++)
      {
        // リストにデータを挿入
        list.Add(new Node(i));
      }
      // listで6以上のデータを分類
      var where = list.Where(x => x.Data > 5);
      // listで一番先のデータを抽出
      Node first = where.First();
      // listで一番後のデータを抽出
      Node last = where.Last();
      // コンソール出力
      Console.WriteLine("First - " + first.Data);
      Console.WriteLine("Last - " + last.Data);
      // 上の6以上の検索節から7と一致するデータを分類
      where = where.Where(x => x.Data == 7);
      // listのデータが一つなら抽出(2個以上ならException)
      Node single = where.Single();
      // コンソール出力
      Console.WriteLine("Single - " + single.Data);
      // 上の6以上の検索節かつ7と一致するデータから10と一致するデータを分類(必ず分類されるデータはなし)
      where = where.Where(x => x.Data == 10);
      // listからデータが一つなら抽出、なければnullをリターン
      Node default1 = where.SingleOrDefault();
      // コンソール出力
      Console.WriteLine("Default - " + (default1 == null ? "Null" : "Not null"));

      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


First、Lastの場合はリストから始めの値と最後のの値を返却する関数式です。でもデータがなければExceptionが発生します。

でも、ここでorDefaultが付けると、つまり、FirstOrDefault、LastOrDefaultなら検索されたデータがなければnullをリターンします。エラーが発生しません。

Singleの関数はリストの結果が一つの場合、リターンする関数式です。もし、結果値が二つ以上ならエラーを発生します。

SingleOrDefaultの関数も結果がなければnullをリターンします。

OrderBy, OrderByDescending

データを整列する関数です。

OrderByの場合は昇順、OrderByDescendingの場合は降順になります。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // 例クラス
  class Node
  {
    // 値をコンストラクタで格納する。
    public Node(int data)
    {
      // プロパティのDataに値を格納
      this.Data = data;
    }
    // Dataプロパティ
    public int Data
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リスト宣言(リストのオブジェクトはNodeクラス)
      var list = new List<Node>();
      // iが0から9まで繰り返し
      for (int i = 0; i < 10; i++)
      {
        // リストにデータを挿入
        list.Add(new Node(i));
      }
      // listで6以上のデータを分類
      var where = list.Where(x => x.Data > 5);
      // 降順に整列
      where = where.OrderByDescending(x => x.Data);
      // フィルターされたリストを繰り返しで抽出
      foreach (var node in filterList)
      {
        // コンソール出力
        Console.WriteLine(node);
      }

      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


Select, SelectMany

Selectはデータを置換することだし、SelectManyは二つのリストを合併することです。

ここで合併するのはリストのJoinをする意味ではなく、Aのリストが二つあり、Bのリストが三つがあれば、総6個のリストを作るという意味です。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リスト宣言
      var list1 = new List<String>();
      var list2 = new List<int>();
      // list1は文字タイプ
      list1.Add("1");
      list1.Add("2");
      // list2は整数タイプ
      list2.Add(3);
      list2.Add(4);
      list2.Add(5);

      // list1をSelectにより整数タイプに置換してSelectManyにより二つのリストを合併します。
      var ret = list1.Select(x => Convert.ToInt32(x)).SelectMany(x => list2, (x, y) => x + " * " + y + " = " + (x * y));
      foreach (var i in ret)
      {
        // コンソール出力
        Console.WriteLine(i);
      }

      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


Join

Joinは二つのリストを合併する関数です。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  // 例クラス
  class Node
  {
    // 値をコンストラクタで格納する。
    public Node(int key, string value)
    {
      // プロパティに値を格納
      this.Key = key;
      this.Value = value;
    }
    // Keyプロパティ
    public int Key
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
    // Valueプロパティ
    public string Value
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リスト宣言(リストのオブジェクトはNodeクラス)
      var list1 = new List<Node>();
      var list2 = new List<Node>();
      // iが0から9まで繰り返し
      for (int i = 0; i < 10; i++)
      {
        // リストにデータを挿入
        list1.Add(new Node(i, "List1:" + i));
        list2.Add(new Node(i, "List2:" + i));
      }
      // Join関数のパラメータは合併するリスト、list1に関する比較キー、list2に関する比較キー、Joinされた結果リターン値
      var ret = list1.Join(list2.Where(x => x.Key > 5), x => x.Key, y => y.Key, (x, y) => x.Value + " " + y.Value);
      // フィルターされたリストを繰り返しで抽出
      foreach (var item in filterList)
      {
        // コンソール出力
        Console.WriteLine("Key - " + item.Key + " Value - " + item.Value);
      }
 
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


Group

リストをGroup別に分ける関数です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Example
{
  // 例クラス
  class Node
  {
    // 値をコンストラクタで格納する。
    public Node(int key, string value)
    {
      // プロパティに値を格納
      this.Key = key;
      this.Value = value;
    }
    // Keyプロパティ
    public int Key
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
    // Valueプロパティ
    public string Value
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
    // クラスのStringをリターン
    public override string ToString()
    {
      return "Key - " + Key + " Value - " + Value;
    }
  }
  // Groupされたクラス
  class Group
  {
    // 値をコンストラクタで格納する。
    public Group(int key, IEnumerable<Node> value)
    {
      // プロパティに値を格納
      this.Key = key;
      this.Value = value;
    }
    // Keyプロパティ
    public int Key
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
    // Valueプロパティ
    public IEnumerable<Node> Value
    {
      // 入力はコンストラクタから受け取る。
      get; private set;
    }
    // クラスのStringをリターン
    public override string ToString()
    {
      // リストになっているvalueデータのString値を作成
      var sb = new StringBuilder();
      foreach (var item in Value)
      {
        sb.AppendLine(item.ToString());
      }
      return sb.ToString();
    }
  }
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リスト宣言(リストのオブジェクトはNodeクラス)
      var list = new List<Node>();
      // iが0から9まで繰り返し
      for (int i = 0; i < 10; i++)
      {
        // リストにデータを挿入
        list.Add(new Node(i, "List:" + i));
      }
      // 偶数、奇数別でグループを分けてそのキーでNodeを再整列する。
      var filerList = list.GroupBy(x => x.Key % 2, x => x).Select(x => new Group(x.Key, x.ToList()));
      // filerListのキー順番とおりに繰り返し
      foreach (var item in filerList)
      {
        // コンソール出力
        Console.WriteLine("Group Key - " + item.Key);
        Console.WriteLine(item.ToString());
      }
 
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


Sum, Average, Max, Min

Linq関数式にはクエリ式にはない関数がありますが、それがSum、Average、Max、Minです。Sumはリストの結果合、Averageは平均、Maxは一番大きい値、Minは小さい値の結果になります。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // リスト宣言
      var list = new List<int>();
      // iが0から9まで繰り返し
      for (int i = 0; i < 10; i++)
      {
        // リストにデータを挿入
        list.Add(i);
      }
      // リストの中で一番大きい値を救う。
      var max = list.Max(x => x);
      // リストの中で一番小さい値を救う。
      var min = list.Min(x => x);
      // リストの合を救う。
      var sum = list.Sum(x => x);
      // リストの平均値を救う。
      var avg = list.Average(x => x);
      // コンソール出力
      Console.WriteLine(max);
      Console.WriteLine(min);
      Console.WriteLine(sum);
      Console.WriteLine(avg);

      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


Linq関数式はクエリ式よりソース読みやすさが良いです。

そしてすべての関数式はIEnumerableインタフェースでリターンするので、チェインパターン式で一行目で様々な関数を呼び出すことができます。

C#の代表的なORMのEntityプレームワークもLinqの関数式を継承して使うので様々な仕様でも簡単に使うことができます。


でも、Linq式とは関数自体のアルゴリズムは凄く効率的ですが、使用方法や仕様により性能を低下する可能性があるので、注意しなければならないです。

例えば、重複フィルターや一つの条件式で処理することをいろんなWhereとSelectを分けるのはよくないです。


ここまでC#のLinq関数式を使う方法に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

#C#
最新投稿