徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【C#,LINQ】ToLookup~配列やリスト内の特定データでキーにしたデータを作りたいとき~

 C#のLINQの関数であるToLookup()の使い方についてです。
 配列やリストの要素を特定のデータをキーにしたシーケンスにまとめることが出来ます。
f:id:urahimono:20180721073432p:plain


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

ILookupにデータを纏めちゃうぞ

 データが配列やリストでたくさんあるとき、特定のプロパティごとにデータをまとめたいとき、きっとありますよね。
 そんなときはLINQのToLookup()を使ってみませんか。
 いい感じにまとめてくれますよ。

public static ILookup<TKey, TSource> ToLookup<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector );
Enumerable.ToLookup(TSource, TKey) メソッド (IEnumerable(TSource), Func(TSource, TKey)) (System.Linq)

 ちなみに返ってくるのはILookupという聞きなれない型です。
 
ILookup
ILookup(TKey, TElement) インターフェイス (System.Linq)

 Dictionaryと似ていますが、キーに複数の要素が設定できるのと、後から要素が追加できない点という違いがありそうですね。

 ToLookup()を使う前に、データ用のクラスを作成しておきましょう。

Program.cs

private class Parameter
{
    public string   Team    { get; set; }
    public string   Name    { get; set; }
    public int      Age     { get; set; }

    public override string ToString()
    {
        return string.Format( "Team:{0}, Name:{1}, Age:{2}", Team, Name, Age );
    }
}

 このクラスの配列を用意して、ToLookup()を使ってTeamのプロパティ毎にまとめてみましょうか。
 ToLookup()の使い方は、第一引数にまとめるために使用するプロパティを指定する処理を記述するだけです。
 今回の場合は、ラムダ式を用いています。

Program.cs

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

public static class Program
{
    private class Parameter
    {
        public string   Team    { get; set; }
        public string   Name    { get; set; }
        public int      Age     { get; set; }

        public override string ToString()
        {
            return string.Format( "Team:{0}, Name:{1}, Age:{2}", Team, Name, Age );
        }
    }

    static void Main( string[] args )
    {
        // 人物データ。
        Parameter[] persons = new Parameter[]
        {
            new Parameter() { Team = "A", Age = 52, Name = "正一郎" },
            new Parameter() { Team = "A", Age = 28, Name = "清次郎" },
            new Parameter() { Team = "C", Age = 20, Name = "誠三郎" },
            new Parameter() { Team = "B", Age = 18, Name = "征史郎" },
        };

        ILookup<string, Parameter> results = persons.ToLookup( param => param.Team );

        // 結果発表
        System.Console.WriteLine( "persons:{0}", persons.Text() );

        foreach( var team in results )
        {
            System.Console.WriteLine( "Team:{0}", team.Key );
            foreach( var person in team )
            {
                System.Console.WriteLine( "\t{0}", person );
            }
        }

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

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text( this IEnumerable i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

persons:[Team:A, Name:正一郎, Age:52], [Team:A, Name:清次郎, Age:28], [Team:C, Name:誠三郎, Age:20], [Team:B, Name:征史 郎, Age:18],
Team:A
Team:A, Name:正一郎, Age:52
Team:A, Name:清次郎, Age:28
Team:C
Team:C, Name:誠三郎, Age:20
Team:B
Team:B, Name:征史郎, Age:18

 Teamのプロパティ毎にまとめてくれました。
 いい感じです。

纏めるデータの型を変更しちゃうぞ

 さきほどはデータの型であったParameterクラスをそのままILookupの要素として使いましたが、この要素の型も指定することが出来ます。

public static ILookup<TKey, TElement> ToLookup<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector );
Enumerable.ToLookup(TSource, TKey, TElement) メソッド (IEnumerable(TSource), Func(TSource, TKey), Func(TSource, TElement)) (System.Linq)

 第二引数に、要素として使う情報を指定する処理を書くことで、ILookupの要素となります。
 以下の例では、ParameterクラスのNameプロパティを要素として使うようにラムダ式を用いて指定しています。

Program.cs

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

public static class Program
{
    private class Parameter
    {
        public string   Team    { get; set; }
        public string   Name    { get; set; }
        public int      Age     { get; set; }

        public override string ToString()
        {
            return string.Format( "Team:{0}, Name:{1}, Age:{2}", Team, Name, Age );
        }
    }

    static void Main( string[] args )
    {
        // 人物データ。
        Parameter[] persons = new Parameter[]
        {
            new Parameter() { Team = "A", Age = 52, Name = "正一郎" },
            new Parameter() { Team = "A", Age = 28, Name = "清次郎" },
            new Parameter() { Team = "C", Age = 20, Name = "誠三郎" },
            new Parameter() { Team = "B", Age = 18, Name = "征史郎" },
        };

        ILookup<string, string> results = persons.ToLookup( 
                                                    param => param.Team,
                                                    param => param.Name );

        // 結果発表
        System.Console.WriteLine( "persons:{0}", persons.Text() );

        foreach( var team in results )
        {
            System.Console.WriteLine( "Team:{0}", team.Key );
            foreach( var person in team )
            {
                System.Console.WriteLine( "\t{0}", person );
            }
        }

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

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text( this IEnumerable i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

persons:[Team:A, Name:正一郎, Age:52], [Team:A, Name:清次郎, Age:28], [Team:C, Name:誠三郎, Age:20], [Team:B, Name:征史 郎, Age:18],
Team:A
正一郎
清次郎
Team:C
誠三郎
Team:B
征史郎

グループ化する際の比較処理を自作するぞ

 さて以下のコードを見てください。

Program.cs

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

public static class Program
{
    private class Parameter
    {
        public string   Team    { get; set; }
        public string   Name    { get; set; }
        public int      Age     { get; set; }

        public override string ToString()
        {
            return string.Format( "Team:{0}, Name:{1}, Age:{2}", Team, Name, Age );
        }
    }

    static void Main( string[] args )
    {
        // 人物データ。
        Parameter[] persons = new Parameter[]
        {
            new Parameter() { Team = "A", Age = 52, Name = "正一郎" },
            new Parameter() { Team = "a", Age = 28, Name = "清次郎" },
            new Parameter() { Team = "C", Age = 20, Name = "誠三郎" },
            new Parameter() { Team = "B", Age = 18, Name = "征史郎" },
        };

        ILookup<string, string> results = persons.ToLookup( 
                                                    param => param.Team,
                                                    param => param.Name );

        // 結果発表
        System.Console.WriteLine( "persons:{0}", persons.Text() );

        foreach( var team in results )
        {
            System.Console.WriteLine( "Team:{0}", team.Key );
            foreach( var person in team )
            {
                System.Console.WriteLine( "\t{0}", person );
            }
        }

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

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text( this IEnumerable i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

persons:[Team:A, Name:正一郎, Age:52], [Team:a, Name:清次郎, Age:28], [Team:C, Name:誠三郎, Age:20], [Team:B, Name:征史 郎, Age:18],
Team:A
正一郎
Team:a
清次郎
Team:C
誠三郎
Team:B
征史郎

 さきほどと同じように、Teamのプロパティでデータを分けているのですが、"A"と"a"が別々のグループとして分かれています。
 これを、大文字でも小文字でも同じグループ扱いにすることはできないでしょうか。

 そんな場合は、IEqualityComparerを使った比較処理を自作することで可能となります。

IEqualityComparer
IEqualityComparer(T) インターフェイス (System.Collections.Generic)

Program.cs

private class GroupComparer : IEqualityComparer<string>
{
    public bool Equals( string i_lhs, string i_rhs )
    {
        return i_lhs.ToUpper() == i_rhs.ToUpper();
    }

    public int GetHashCode( string i_value )
    {
        return i_value.ToUpper().GetHashCode();
    }
}

 このようにIEqualityComparerを継承して、大文字でも小文字でも、一度大文字にして比較する処理を作成してみました。
 これをToLookup()に指定してあげればOKです。

public static ILookup<TKey, TSource> ToLookup<TSource, TKey>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer );
Enumerable.ToLookup(TSource, TKey) メソッド (IEnumerable(TSource), Func(TSource, TKey), IEqualityComparer(TKey)) (System.Linq)

public static ILookup<TKey, TElement> ToLookup<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer );
Enumerable.ToLookup(TSource, TKey, TElement) メソッド (IEnumerable(TSource), Func(TSource, TKey), Func(TSource, TElement), IEqualityComparer(TKey)) (System.Linq)

Program.cs

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

public static class Program
{
    private class Parameter
    {
        public string   Team    { get; set; }
        public string   Name    { get; set; }
        public int      Age     { get; set; }

        public override string ToString()
        {
            return string.Format( "Team:{0}, Name:{1}, Age:{2}", Team, Name, Age );
        }
    }

    private class GroupComparer : IEqualityComparer<string>
    {
        public bool Equals( string i_lhs, string i_rhs )
        {
            return i_lhs.ToUpper() == i_rhs.ToUpper();
        }

        public int GetHashCode( string i_value )
        {
            return i_value.ToUpper().GetHashCode();
        }
    }

    static void Main( string[] args )
    {
        // 人物データ。
        Parameter[] persons = new Parameter[]
        {
            new Parameter() { Team = "A", Age = 52, Name = "正一郎" },
            new Parameter() { Team = "a", Age = 28, Name = "清次郎" },
            new Parameter() { Team = "C", Age = 20, Name = "誠三郎" },
            new Parameter() { Team = "B", Age = 18, Name = "征史郎" },
        };

        GroupComparer compare = new GroupComparer();
        ILookup<string, string> results = persons.ToLookup( 
                                                    param => param.Team,
                                                    param => param.Name,
                                                    compare );

        // 結果発表
        System.Console.WriteLine( "persons:{0}", persons.Text() );

        foreach( var team in results )
        {
            System.Console.WriteLine( "Team:{0}", team.Key );
            foreach( var person in team )
            {
                System.Console.WriteLine( "\t{0}", person );
            }
        }

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

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text( this IEnumerable i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

persons:[Team:A, Name:正一郎, Age:52], [Team:a, Name:清次郎, Age:28], [Team:C, Name:誠三郎, Age:20], [Team:B, Name:征史 郎, Age:18],
Team:A
正一郎
清次郎
Team:C
誠三郎
Team:B
征史郎

 このように、"A"も"a"も同じグループになっています。

 こんな感じでToLookup()を七兆回ほどつかって、データを整理してみてください。

LINQのリンク