うら干物書き

ゲームを作りたい!

【C#,LINQ】Aggregate~配列やリスト内の情報を集計したいとき~

 C#のLINQの関数であるAggregate()の使い方についてです。
 配列やリスト内の各要素に対して集計用の処理をすることが出来ます。
f:id:urahimono:20180721064911p:plain


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

集計してみよう

 LINQにはAggregate()という面白い関数があります。
 Aggregateは集計という意味のようです。

 Aggregate()は、配列やリスト内の各要素に対して、指定した処理をすることができます。
 実際に試してみましょう。

 以下のコードでは、配列内の要素を全て足した数値を返す処理をラムダ式にて記述しています。

public static TSource Aggregate<TSource>( this IEnumerable<TSource> source, Func<TSource, TSource, TSource> func );
https://msdn.microsoft.com/ja-jp/library/bb548651.aspx

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 数字データ
        int[] numbers   = new int[] { 1, 2, 3, 4 };

        // 数字をすべて足したものを返すよ。
        // 第一引数の"sum"に足された数値が入っていくよ。
        int total = numbers.Aggregate( ( sum, next ) => sum += next );


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

        // 入力待ち用
        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

numbers:[1], [2], [3], [4],
total :10

 Aggregate()のちょっとわかりにくいところが、引数で指定する処理の見方です。
 上記のコードでAggregate()に指定している処理、( sum, next ) => sum += nextを見ていきましょう。

 第二引数に、各要素が入ってきます。
 今回の場合は、next1 ~ 4の値が入ってくるわけですね。
 そして、第一引数には、ひとつ前の要素の処理のときに返した値が入ってきます。
 値とは言っていますが、指定した型次第では、文字列だったり、クラスだったりします。

 今回の場合のAggregate()の引数の流れを見ていくと

  • sum = 0, next = 1
  • sum = 1, next = 2
  • sum = 3, next = 3
  • sum = 6, next = 4

 こんな感じになります。
 そして、最後の要素の処理で返す値が、Aggregate()で返す値になります。

 単純に足すだけではなく、以下のように比較した値を返すことだってできます。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 数字データ
        int[] numbers   = new int[] { 1, 2, 3, 4 };

        // 数字データの中で一番大きい数値を返すよ。
        // 第一引数の"max"に最大の数値が入っていくよ。
        int maxNumber   = numbers.Aggregate( ( max, next ) => max < next ? next : max );


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

        // 入力待ち用
        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

numbers:[1], [2], [3], [4],
max :4

 上記の処理では、一番大きい数値を返す処理を記述しています。
 この場合のAggregate()の引数の流れは以下の通りになります。

  • max = 0, next = 1
  • max = 1, next = 2
  • max = 2, next = 3
  • max = 3, next = 4

シードを追加してみよう

 さて先ほどのコードでは、Aggregate()の最初の要素に対して呼ばれる際の第一引数には、各型のデフォルト値が入っていました。
 その値をこちら側で指定することができます。 

public static TAccumulate Aggregate<TSource, TAccumulate>( this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func );
https://msdn.microsoft.com/ja-jp/library/bb549218.aspx

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 数字データ
        int[] numbers   = new int[] { 1, 2, 3, 4 };
        int   seed      = 15;

        // 数字をすべて足したものを返すよ。
        // 第一引数の"sum"に足された数値が入っていくよ。
        int total = numbers.Aggregate( seed, ( sum, next ) => sum += next );


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

        // 入力待ち用
        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

numbers:[1], [2], [3], [4],
seed :15
total :25

 今回の例では、初期の値として15を渡しています。
 そのためAggregate()の引数の流れを見ていくと、このような形になります。

  • sum = 15, next = 1
  • sum = 16, next = 2
  • sum = 18, next = 3
  • sum = 21, next = 4

 さて、ここでAggregate()では面白いことができます。
 この初期の値(シード値)には、Aggregate()を呼ぶシーケンスとは違う型の値を指定することができます。
 その場合はAggregate()で返ってくる型も、そのシード値と同じ型になります。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 数字データ
        int[]   numbers   = new int[] { 1, 2, 3, 4 };
        string  seed      = "0";

        // 数字を文字列にして、"+数値"を後ろに足したテキストを返すよ。
        // 第一引数の"text"に足された文字列が返ってくるよ。
        string numberText   = numbers.Aggregate( seed, ( text, next ) => string.Format( "{0}+{1}", text, next.ToString() ) );

        // 結果発表
        System.Console.WriteLine( "numbers:{0}", numbers.Text() );
        System.Console.WriteLine( "seed   :{0}", seed );
        System.Console.WriteLine( "text   :{0}", numberText );

        // 入力待ち用
        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

numbers:[1], [2], [3], [4],
seed :0
text :0+1+2+3+4

 上記の例では、数値の配列に対して、文字列のシードを渡しています。
 そのため、Aggregate()の引数の流れを見ていくと、このような形になります。

  • text = "0", next = 1
  • text = "0+1", next = 2
  • text = "0+1+2", next = 3
  • text = "0+1+2+3", next = 4

 このように単に集計すること以上のことができます。

集計した情報を変換してみよう

 先ほどまでは、最後の要素に対して行われた処理の返り値がAggregate()の返り値として使われていました。
 Aggregate()では、この「最後の要素に対して行われた処理の返り値」に対して、更に指定した処理を付け加えることもできます。

public static TResult Aggregate<TSource, TAccumulate, TResult>( this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, Func<TAccumulate, TResult> resultSelector );
https://msdn.microsoft.com/ja-jp/library/bb548744.aspx

 Aggregate()の第三引数に、「最後の要素に対して行われた処理の返り値」に対して行う処理を記述でき、ここで返す値が最終的にAggregate()で返る値になります。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 数字データ
        int[]   numbers     = new int[] { 1, 2, 3, 4 };
        int     seed        = 15;

        // Seed値と数値をすべて足して二乗したものを返すよ。
        // 第一引数の"sum"に足された数値が返ってくるよ。
        int totalSquare     = numbers.Aggregate( 
                                        seed,
                                        ( sum, next ) => sum += next,
                                        ( sum ) => sum * sum );

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

        // 入力待ち用
        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

numbers :[1], [2], [3], [4],
seed :15
totalSquare:625

 上記の例では、「15 + 全ての要素の合計」に対して、更に二乗の処理を加えています。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 数字データ
        int[]   numbers = new int[] { 1, 2, 3, 4 };
        string  seed    = "";

        // 数字を文字列にして、後ろに足したテキストを、数値に変換したものを返すよ。
        // 第一引数の"text"に足された文字列が返ってくるよ。
        int number      = numbers.Aggregate( 
                                        seed,
                                        ( text, next ) => string.Format( "{0}{1}", text, next.ToString() ),
                                        ( text ) => int.Parse( text ) );

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

        // 入力待ち用
        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

numbers:[1], [2], [3], [4],
seed :
number :1234

 この例では、文字列として各要素の数値を足して、最後にその文字列を数値に変換する処理を記述しています。

 このようにAggregate()はアイディア次第でいろいろなことをすることができます。
 こんな感じでAggregate()を七兆回ほど使ってみてください。

LINQのリンク