いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Linq入門記-23 (LINQ To Object, SelectMany)


今回は、SelectMany拡張メソッドについてです。


SelectMany拡張メソッドは、Select拡張メソッドと同じように
特定のシーケンスに対して射影を行いますが、最後に結果のシーケンスを
平坦化して返してくれるメソッドです。


なんか文字で書くと、書きづらいのですので、実際のクエリ定義と交えて説明します。
以下のような、データクラスが存在するとします。

        class Team
        {
            public string              Name    { get; set; }
            public IEnumerable<string> Members { get; set; }
        }

上記のクラスは、チームメンバの名前としてIEnumerable型のプロパティを持っています。
これを、

from   team in teams
select team.Members;

という風に、クエリ式を定義して取得するとクエリ結果は

IEnumerable<IEnumerable<string>>

となります。Membersプロパティの戻り値がIEnumerableなので
そのとおりですね。
で、フラットな形で取得するには、以下のようにクエリを定義します。

from   team   in teams
from   member in team.Members
select member;

上記のクエリの場合、2段階でfromを利用して各メンバの段階まで
抽出をしているので、クエリ結果は、

IEnumerable<string>

となります。SelectMany拡張メソッドは、まさに上記のクエリのように

IEnumerable<IEnumerable<string>>

となっているような構造を、

IEnumerable<string>

のようにフラットな状態にしてシーケンスを返してくれます。


実は、LINQではクエリを定義する際にfromを2つ以上利用している場合
クエリの最後にて結果をSelectする部分では、SelectMany拡張メソッドに
置換されます。(fromが一つの場合は、Select拡張メソッドが利用されます。)


なので、上記のクエリ

from   team   in teams
from   member in team.Members
select member;

は以下のものと同じです。

teams.SelectMany(team => team.Members);

SelectMany拡張メソッドは、4つのオーバーロードを持っており
以下の書式となっています。

public static IEnumerable<TResult> SelectMany<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, IEnumerable<TResult>> selector
)
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, IEnumerable<TCollection>> collectionSelector,
    Func<TSource, TCollection, TResult> resultSelector
)
public static IEnumerable<TResult> SelectMany<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, int, IEnumerable<TResult>> selector
)
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, int, IEnumerable<TCollection>> collectionSelector,
    Func<TSource, TCollection, TResult> resultSelector
)


4つオーバーロードが存在しますが、大別すると2パターンです。
selectorを指定してシーケンスを取得するものと、 collectionSelectorとresultSelectorを指定して
シーケンスを取得するものです。


後は、それぞれのパターンに対して、シーケンス内のインデックスを引数に追加してくれる
ものがオーバーライドとして用意されています。


処理によっては、インデックス値が必要な場合もありますので、その時に
このオーバーロードを利用します。


collectionSelectorとresultSelectorの2つを指定するオーバーロードの場合、
collectionSelectorにて、必要な要素を決定し、さらに結果を構築する際に元のシーケンスと
抽出されたデータを元に、最終結果を作成する場合などに利用します。
サンプルの中に、シンプルなパターンを書いていますのでご参照下さい。


以下、サンプルです。

    #region LinqSamples-21
    public class LinqSamples21 : IExecutable
    {
        class Team
        {
            public string              Name    { get; set; }
            public IEnumerable<string> Members { get; set; }
        }
        
        public void Execute()
        {
            var teams = new List<Team>
                        {
                            new Team { Name = "Team A", Members = new List<string>{ "gsf_zero1", "gsf_zero2" } },
                            new Team { Name = "Team B", Members = new List<string>{ "gsf_zero3", "gsf_zero4" } }
                        };
            
            //
            // SelectMany拡張メソッドは、コレクションを平坦化する際に利用できる。
            // 例えば、上記で作成したteams変数は以下のような構造になっている。
            //
            //     teams -- [0]:Teamオブジェクト
            //                      └Members:IEnumerable<string>
            //              [1]:Teamオブジェクト
            //                      └Members:IEnumerable<string>
            //
            // 各Teamオブジェクトは、Membersプロパティを持っているので
            // SelectManyではなく、Select拡張メソッドを利用して
            //    teams.Select(team => team.Members)
            // とすると、結果は、IEnumerable<IEnumerable<string>>となる。
            //
            // このような状態で、SelectMany拡張メソッドを利用して
            //    teams.SelectMany(team => team.Members)
            // とすると、結果は、IEnumerable<string>となる。
            // つまり、SelectMany拡張メソッドは、各Selectorが返すシーケンスを最終的に
            // 平坦化してから結果を返してくれる。
            //
            // 尚、SelectMany拡張メソッドは、クエリ式にて2段以上のfrom句を利用している場合
            // 暗黙的に利用されている。上記のteams.SelectMany(team => team.Members)は
            // 以下のクエリ式と同じである。
            //
            //     from   team   in teams
            //     from   member in team.Members
            //     select member
            //
            // 実行時には、最後のselectの部分がSelectManyに置換される。
            //
            Console.WriteLine("===== Func<TSource, IEnumerable<TResult>>のサンプル =====");
            foreach (var member in teams.SelectMany(team => team.Members))
            {
                Console.WriteLine(member);
            }
            
            Console.WriteLine("===== Func<TSource, int, IEnumerable<TResult>>のサンプル =====");
            foreach (var member in teams.SelectMany((team, index) => (index % 2 == 0) ? team.Members : new List<string>()))
            {
                Console.WriteLine(member);
            }
            
            Console.WriteLine("===== collectionSelectorとresultSelectorを利用しているサンプル (1) =====");
            var query = teams.SelectMany
                        (
                            team => team.Members,                                       // collectionSelector
                            (team, member) => new { Team = team.Name, Name = member }   // resultSelector
                        );
            
            foreach (var item in query)
            {
                Console.WriteLine(item);
            }
            
            Console.WriteLine("===== collectionSelectorとresultSelectorを利用しているサンプル (2) =====");
            var query2 = teams.SelectMany
                         (
                             (team, index)  => (index % 2 != 0) ? team.Members : new List<string>(),  // collectionSelector
                             (team, member) => new { Team = team.Name, Name = member }                // resultSelector
                         );
            
            foreach (var item in query2)
            {
                Console.WriteLine(item);
            }
        }
    }
    #endregion


出力結果は、以下のようになります。

  ===== Func>のサンプル =====
  gsf_zero1
  gsf_zero2
  gsf_zero3
  gsf_zero4
  ===== Func>のサンプル =====
  gsf_zero1
  gsf_zero2
  ===== collectionSelectorとresultSelectorを利用しているサンプル (1) =====
  { Team = Team A, Name = gsf_zero1 }
  { Team = Team A, Name = gsf_zero2 }
  { Team = Team B, Name = gsf_zero3 }
  { Team = Team B, Name = gsf_zero4 }
  ===== collectionSelectorとresultSelectorを利用しているサンプル (2) =====
  { Team = Team B, Name = gsf_zero3 }
  { Team = Team B, Name = gsf_zero4 }


================================
過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。