うら干物書き

ゲームを作りたい!

【C#,LINQ】Select,SelectMany~配列やリスト内の要素の形を変形したいとき~

 LINQは便利です。
 その中でもSelect()を使いこなせれば、大抵の場面で大体何とかなります。
 というわけで、Select()の使い方をメモしておきます。
f:id:urahimono:20180605233222p:plain


この記事には.NET Framework 4.6.1を使用しています。

Select()を使う

Enumerable.Select(TSource, TResult) メソッド (IEnumerable(TSource), Func(TSource, TResult)) (System.Linq)

 そもそもSelect()とは何なのか。
 MSDNには以下のように記述されています。

シーケンスの各要素を新しいフォームに射影します。

 うん、何言っているのかわからない。
 「射影」なんて日常では絶対に使わない単語を使われても困るのです。

 まあ、他の形に変える、みたいな感じでしょうか。

 言葉で説明できる自信がないので、もうコード書いちゃいましょう。
 それを見てもらいましょう。

int配列の中身の値を二倍にしちゃうコード

 まずintの配列があります。
 その中身の値をSelect()を使って二倍にしたコレクションを返す、そんなコードを書いてみましょう。

Program.cs

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

class Program
{
    static void Main( string[] args )
    {
        int[] numbers = new int[] { 1, 2, 4, 7, 9 };

        // 全ての数値を2倍する。
        IEnumerable<int> results = numbers.Select( value => value * 2 );


        // 表示用の文字列作成
        string text = string.Empty;
        foreach( int value in results )
        {
            text += string.Format( "{0}, ", value );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

2, 4, 8, 14, 26,

 こんな感じに書けます。

int配列の中身の値を文字列にしちゃうコード

 続けます。
 今度は先ほどのintの配列の中の値を、指定したフォーマットのstring型に変えちゃいましょう。

Program.cs

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

class Program
{
    static void Main( string[] args )
    {
        int[] numbers = new int[] { 1, 2, 4, 7, 13 };

        // 全ての数値を"[ 数字 ]"のフォーマットの文字列に変換する。
        IEnumerable<string> results = numbers.Select( value => string.Format( "[{0}]", value.ToString( "D2" ) ) );


        // 表示用の文字列作成
        string text = string.Empty;
        foreach( string value in results )
        {
            text += string.Format( "{0}, ", value );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

[01], [02], [04], [07], [13],

クラス配列の中身の特定のプロパティだけにしちゃうコード

 なんとなく分かってきたのではないでしょうか。
 今度は何かしらのプロパティを持つクラスの配列を用意します。
 このクラスの中の特定のプロパティだけのコレクションを作ってみましょう。

Program.cs

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

class Program
{
    private class Parameter
    {
        public int      ID      { get; set; }
        public float    Rate    { get; set; }
        public string   Name    { get; set; }
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { ID =  5, Rate = 0.0f, Name = "正一郎" },
            new Parameter() { ID = 13, Rate = 0.1f, Name = "清次郎" },
            new Parameter() { ID = 25, Rate = 0.0f, Name = "誠三郎" },
            new Parameter() { ID = 42, Rate = 0.3f, Name = "征史郎" },
        };

        // Nameのプロパティのみを抜き取る。
        IEnumerable<string> results = parameters.Select( value => value.Name );


        // 表示用の文字列作成
        string text = string.Empty;
        foreach( string value in results )
        {
            text += string.Format( "{0}, ", value );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

正一郎, 清次郎, 誠三郎, 征史郎,

クラス配列の中身の特定のプロパティを持つ匿名型にしちゃうコード

 先ほどは特定のプロパティ一種類だけでしたが、複数のプロパティが必要な場合もありますよね。
 今度は匿名型を使って、クラス内の任意のプロパティを持つデータを取得しちゃいましょう。
 
Program.cs

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

class Program
{
    private class Parameter
    {
        public int      ID      { get; set; }
        public float    Rate    { get; set; }
        public string   Name    { get; set; }
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { ID =  5, Rate = 0.0f, Name = "正一郎" },
            new Parameter() { ID = 13, Rate = 0.1f, Name = "清次郎" },
            new Parameter() { ID = 25, Rate = 0.0f, Name = "誠三郎" },
            new Parameter() { ID = 42, Rate = 0.3f, Name = "征史郎" },
        };

        // IDの2倍の値とNameのプロパティのみを抜き取って、匿名型に変換する。
        var results = parameters.Select( value => new { Number = value.ID * 2, Name = value.Name } );


        // 表示用の文字列作成
        string text = string.Empty;
        foreach( var value in results )
        {
            text += string.Format( "[{0}]:{1}, ", value.Number, value.Name );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

 [10]:正一郎, [26]:清次郎, [50]:誠三郎, [84]:征史郎,

 これだけでSelect()の便利さが分かります。

Select()を使って配列のインデックスを取得しちゃう

 foreach()って便利ですよね。
 ですが、配列のインデックスが欲しい場合は、foreach()ではちょっとわからないですよね。
 そういう場合はやむを得ず、for()を使うこともあると思います。

Program.cs

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

class Program
{
    static void Main( string[] args )
    {
        string[] names = new string[] { "正一郎", "清次郎", "誠三郎", "征史郎" };

        // 表示用の文字列作成
        string text = string.Empty;

        // foreachじゃ配列のインデックスが取得できないので諦める……
//         foreach( string name in names )
//         {
//             text += string.Format( "[{0}]:{1}, ", index, name );
//         }

        // 古典的ではあるが、配列のインデックスは取得できるんだ
        for( int i = 0; i < names.Length; ++i )
        {
            text += string.Format( "[{0}]:{1}, ", i, names[ i ] );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

 [0]:正一郎, [1]:清次郎, [2]:誠三郎, [3]:征史郎,

 ですが、Select()を使うことで配列のインデックスを取得することが出来るのです。
 先ほどまで使ってSelect()とは、引数が違うので気を付けてくださいね。

Enumerable.Select(TSource, TResult) メソッド (IEnumerable(TSource), Func(TSource, Int32, TResult)) (System.Linq)

Program.cs

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

class Program
{
    static void Main( string[] args )
    {
        string[] names = new string[] { "正一郎", "清次郎", "誠三郎", "征史郎" };

        // 配列のインデックスと名前を取得し、匿名型に変換する。
        // 二番目の引数に配列のインデックスが入ってくる。
        var results = names.Select( ( value, index ) => new { Number = index, Name = value } );


        // 表示用の文字列作成
        string text = string.Empty;
        foreach( var value in results )
        {
            text += string.Format( "[{0}]:{1}, ", value.Number, value.Name );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

 [0]:正一郎, [1]:清次郎, [2]:誠三郎, [3]:征史郎,

 Select()はこんなこともできるのです。便利です。

SelectMany()を使う

 Select()は便利なのですが、配列のデータの中に配列のデータがある場合は、ちょっと面倒くさいのですよ。

Program.cs

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

class Program
{
    private class Parameter
    {
        public string   Name    { get; set; }
        public int[]    Numbers { get; set; }       
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { Name = "正一郎", Numbers = new int[] { 1, 2, 3 } },
            new Parameter() { Name = "清次郎", Numbers = new int[] { 1, 3, 5 } },
            new Parameter() { Name = "誠三郎", Numbers = new int[] { 2, 4, 6 } },
            new Parameter() { Name = "征史郎", Numbers = new int[] { 9, 8, 7 } },
        };

        // int[]のNumbersの情報を取得する。
        IEnumerable<int[]> results = parameters.Select( value => value.Numbers );


        // 表示用の文字列作成
        string text = string.Empty;

        // コレクションの中にコレクションがある形なので、二重でforeachを回す必要がある。
        foreach( int[] values in results )
        {
            foreach( int number in values )
            {
                text += string.Format( "{0}, ", number );
            }
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

1, 2, 3, 1, 3, 5, 2, 4, 6, 9, 8, 7,

 foreach()を二重で回さなくちゃいけないのは、ちょっと面倒ですね。
 こんなときには、SelectMany()というものを使うと便利なのです。
 SelectMany()を使うことで、コレクションの中のコレクションを、一つのコレクションに纏めることが出来るのです。

 この行為を平坦化というらしいです。
 また日常生活では使わない単語が出てきました。

Enumerable.SelectMany(TSource, TResult) メソッド (IEnumerable(TSource), Func(TSource, IEnumerable(TResult))) (System.Linq)

 とりあえず使ってみましょう。

Program.cs

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

class Program
{
    private class Parameter
    {
        public string   Name    { get; set; }
        public int[]    Numbers { get; set; }       
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { Name = "正一郎", Numbers = new int[] { 1, 2, 3 } },
            new Parameter() { Name = "清次郎", Numbers = new int[] { 1, 3, 5 } },
            new Parameter() { Name = "誠三郎", Numbers = new int[] { 2, 4, 6 } },
            new Parameter() { Name = "征史郎", Numbers = new int[] { 9, 8, 7 } },
        };

        // 全てのNumbersの情報が一つのコレクションにまとまって取得できる。
        IEnumerable<int> results = parameters.SelectMany( value => value.Numbers );


        // 表示用の文字列作成
        string text = string.Empty;

        foreach( int value in results )
        {
            text += string.Format( "{0}, ", value );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

1, 2, 3, 1, 3, 5, 2, 4, 6, 9, 8, 7,

 あら、簡単。

SelectMany()でも配列のインデックスを取得しちゃう

 Select()で配列のインデックスが取得できるのですから、SelectMany()でも取得できちゃうのです。

Enumerable.SelectMany(TSource, TResult) メソッド (IEnumerable(TSource), Func(TSource, Int32, IEnumerable(TResult))) (System.Linq)

Program.cs

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

class Program
{
    private class Parameter
    {
        public string   Name    { get; set; }
        public int[]    Numbers { get; set; }       
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { Name = "正一郎", Numbers = new int[] { 1, 2, 3 } },
            new Parameter() { Name = "清次郎", Numbers = new int[] { 1, 3, 5 } },
            new Parameter() { Name = "誠三郎", Numbers = new int[] { 2, 4, 6 } },
            new Parameter() { Name = "征史郎", Numbers = new int[] { 9, 8, 7 } },
        };

        // parametersの配列のインデックスをIndexAに、Numbersの配列のインデックスをIndexBに変換した、匿名型を取得する。
        var results = parameters.SelectMany( ( value, indexA ) => 
            value.Numbers.Select( ( number, indexB ) => 
                new { Number = number, IndexA = indexA, IndexB = indexB } ) );


        // 表示用の文字列作成
        string text = string.Empty;

        foreach( var value in results )
        {
            text += string.Format( "[{0}:{1}]{2}, ", value.IndexA, value.IndexB, value.Number );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

[0:0]1, [0:1]2, [0:2]3, [1:0]1, [1:1]3, [1:2]5, [2:0]2, [2:1]4, [2:2]6, [3:0]9, [3:1]8, [3:2]7,

平坦化したプロパティと他のプロパティを組み合わせちゃう

 SelectMany()を使うことで、コレクションデータを平坦化できることがわかりました。
 では平坦化されたプロパティと他のプロパティを組み合わせることはできないかな。

 SelectMany()には、更にオーバーロードしたものがあります。
 第二引数に平坦化されたプロパティで受ける処理を記述することができるのです。

Enumerable.SelectMany(TSource, TCollection, TResult) メソッド (IEnumerable(TSource), Func(TSource, IEnumerable(TCollection)), Func(TSource, TCollection, TResult)) (System.Linq)

 ちなみにここからは、コード若干見にくくなってきます。

Program.cs

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

class Program
{
    private class Parameter
    {
        public string   Name    { get; set; }
        public int[]    Numbers { get; set; }       
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { Name = "正一郎", Numbers = new int[] { 1, 2, 3 } },
            new Parameter() { Name = "清次郎", Numbers = new int[] { 1, 3, 5 } },
            new Parameter() { Name = "誠三郎", Numbers = new int[] { 2, 4, 6 } },
            new Parameter() { Name = "征史郎", Numbers = new int[] { 9, 8, 7 } },
        };

        // ちょっと複雑だけど、平坦化されたNumbersの各数値とNameを合わせた匿名型を取得する。
        var results = parameters.SelectMany( param => param.Numbers, 
            ( value, number ) => new { Name = value.Name, Number = number } );


        // 表示用の文字列作成
        string text = string.Empty;

        foreach( var value in results )
        {
            text += string.Format( "{0}:{1}, ", value.Name, value.Number );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

結果

正一郎:1, 正一郎:2, 正一郎:3, 清次郎:1, 清次郎:3, 清次郎:5, 誠三郎:2, 誠三郎:4, 誠三郎:6, 征史郎:9, 征史郎:8, 征史郎:7,

 もちろん、こちらも配列のインデックスを取得することも可能です。

Enumerable.SelectMany(TSource, TCollection, TResult) メソッド (IEnumerable(TSource), Func(TSource, Int32, IEnumerable(TCollection)), Func(TSource, TCollection, TResult)) (System.Linq)

Program.cs

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

class Program
{
    private class Parameter
    {
        public string   Name    { get; set; }
        public int[]    Numbers { get; set; }       
    }

    static void Main( string[] args )
    {
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { Name = "正一郎", Numbers = new int[] { 1, 2, 3 } },
            new Parameter() { Name = "清次郎", Numbers = new int[] { 1, 3, 5 } },
            new Parameter() { Name = "誠三郎", Numbers = new int[] { 2, 4, 6 } },
            new Parameter() { Name = "征史郎", Numbers = new int[] { 9, 8, 7 } },
        };

        // かなり複雑になったけど、
        // 配列のインデックスと平坦化されたNumbersの各数値とその配列のインデックスを匿名型として取得する。
        // 上記の匿名型をparamプロパティとして、それをNameと合わせた匿名型として作成し取得する。
        var results = parameters.SelectMany( ( value, indexA ) =>
            value.Numbers.Select( ( number, indexB ) => new { Number = number, IndexA = indexA, IndexB = indexB } ),
            ( value, param ) => new { Name = value.Name, Param = param } );


        // 表示用の文字列作成
        string text = string.Empty;

        foreach( var value in results )
        {
            text += string.Format( "[{0}:{1}]{2}, ", value.Param.IndexA, value.Param.IndexB, value.Name );
        }

        System.Console.WriteLine( text );

        // 入力待ち用
        System.Console.ReadKey();
    }

} // class Program

 [0:0]正一郎, [0:1]正一郎, [0:2]正一郎, [1:0]清次郎, [1:1]清次郎, [1:2]清次郎, [2:0]誠三郎, [2:1]誠三郎, [2:2]誠三郎, [3:0]征史郎, [3:1]征史郎, [3:2]征史郎,

 ……さすがにラムダ式の制御があまりできなくなってきました。
 ですがSelect()を使うことで、ほぼほぼなんでもできる気がしてきました。

 ただやりすぎにはご注意を。
 上記のコードのように、可読性がものすごく悪くなってしまう可能性があるので……。

 それでは、Select()SelectMany()を七兆回使ってみてくださいねー。

LINQのリンク