読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

C# 便利なEnumerable その2

C-Sharp

今回のエンジニアブログを担当する加賀です。

EnumerableクラスのAggregateメソッドが思っていた以上に使えたので、その一例を紹介します。
今回のコードはVisual Studio 2010 Pro SP1、C# 3.5で確認しています。

EnumerableクラスのAggregateメソッドは、シーケンスにアキュムレータ関数を適用するメソッドです。
説明が難しいので、実際に使ってみた例を元に説明できたらいいなと思います。

Aggregateメソッドは以下の3つのオーバーロードがあります。

TSource Aggregate (  
    this IEnumerable source,  
    System.Func<TSource, TSource, TSource> func  
)  
TAccumulate Aggregate<TSource, TAccumulate> (  
    this IEnumerable source,  
    TAccumulate seed,  
    System.Func<TAccumulate, TSource, TAccumulate> func  
)  
TResult Aggregate<TSource, TAccumulate> (  
    this IEnumerable source,  
    TAccumulate seed,  
    System.Func<TAccumulate, TSource, TAccumulate> func,  
    System.Func<TAccumulate, TResult> resultSelector  
)  

各オーバーロードの第1引数のsourceは、実際に使用するときは指定しません。
リストや配列などのインスタンスメソッドのように使用するときは、自動でそのインスタンスが指定されます。
その他の引数の説明は以下の通りです。

引数名 説明
func 今までの要素に対してfuncを実行したときの、前回のfuncの戻り値が第1引数に、 今回計算対象になる要素が第2引数に入ってきます。 この引数に指定するメソッドが、Aggregateのメインになります。 必ず第1引数と同じ型のオブジェクトを返す必要があります。
seed 初期値の指定や、匿名型を使用する場合、2つ目、3つ目のメソッドを使用します。 その際に、実際に初期値や匿名型の初期値を指定します。
resultSelector 計算結果に対して戻り値を返す関数です。 計算途中で匿名型を使用したが、戻り値としては普通の型で返却したいときなどに指定します。

seedを指定しない1つ目のメソッドでは、初回のfunc呼び出しの際に、第1引数にシーケンスの先頭の要素が入った状態で、2番目の要素からfuncが実行される形になります。
2つ目、3つ目のメソッドでは、初回のfunc呼び出しの際の第1引数は、seedの値がそのまま入って、先頭の要素から実行されます。

匿名型について簡単に説明すると、C# 3.0から使用できる、明示的に型宣言をしなくても使用できる型で、
以下のような特徴を持っています。

  • 読み取り専用(newでの初期化に指定した値から変更できない)
  • プロパティの数と型、名前、順序がすべて同じであれば、同じ型として扱われる
  • すべてのプロパティが等しい場合のみ、同一匿名型の2つのインスタンスは等しい

匿名型のインスタンスを代入できる型はvarかobject型だけです。

ロジックの1ラインに使用する最低マス数の計算

ロジックのルールを簡単に上げると、以下のようになります。
・数字は、その色で何マス連続で塗りつぶすかを示している
・数字の並んでいる順に塗りつぶす
・同じ色の間には空白マスが1マス以上必要
・違う色の間では空白マスが無くてもいい
・端が空白マスでもいい

1ラインに最低何マス必要?

黒4と黒2のライン、黒3と黒1と赤1のライン
ロジックの問題の一部分を切り出して、例とします。
たとえば、上図のように黒4と黒2のライン、黒3と黒1と赤1のラインで、それぞれ空白マスも含めて最低何マス必要になるでしょうか。

 

黒だけのラインでは、黒4と黒2が同じ色なので、この間に空白マスが最低1マス入ります。

赤もあるラインでは、黒3と黒1が同じ色なので、この間に空白マスが最低1マス入ります。
その次の赤1では、直前の黒1と違う色なので、間に空白マスを入れなくても問題ありません。

以上の2ラインを、■を塗りつぶすマス、□を空白マスとして並べてみると、以下のようになります。
黒だけのライン:■■■■□■■
赤もあるライン:■■■□■

黒だけのラインは7マス、赤もあるラインは6マス必要になります。

これを計算する処理の一例です。必要部分だけピックアップします。

// fillNumListの要素のパラメータ説明  
//   fillNum : int(Int32)型。塗りつぶす数を保持している。  
//   color   : Color型。塗りつぶす色を保持している。  
int requireNum = 0;  
if (colorNum == 1) {  
  //1色しかこの列に存在しない場合  
  requireNum = fillNumList.Select (item => item.fillNum)  
      .Aggregate ((total, next) => total + 1 + next);  
} else {  
  //2色以上この列に存在する場合  
  requireNum = fillNumList.Select (item => new {item.fillNum, item.color})  
      .Aggregate ((total, next) => new {  
        fillNum = total.fillNum + next.fillNum + (total.color == next.color ? 1 : 0);  
        color = next.color  
      }).fillNum;  
}  

1色だけの場合は、1つ目のオーバーロードのAggregateメソッドです。
今までの計算結果がtotalに入ってくるので、そこに現在の要素のマス数と、間に入れるXマーク1マスを次々に足しています。
2色以上の場合は、直前の要素の色の情報が必要になるので、匿名型を使用して、色と計算結果を同時に次の計算に渡しています。
直前と同じ色であれば+1マス、違うマスであれば+0マスとして計算しています。

同時に最大値、最小値を計算

それぞれ別々に計算するときは、MaxメソッドやMinメソッドを使用すればよいですが、2回走査しないと両方取得できません。
Aggregateを使用すれば1回の走査で計算できます。
戻り値の型が匿名型なので、varを使用して結果を受け取ります。

var result = intList.Aggregate (new {  
  max = int.MinValue,  
  min = int.MaxValue  
}, (total, next) => new {  
  max = (total.max <= next) ? next : total.max,   min = (total.min >= next) ? next : total.min  
});  
int maxVal = result.max;  
int minVal = result.min;  
終わり

初めはAggregateメソッドの役割が全く分からなかったのですが、調べて実際に使ってみると、
思っていた以上に様々な場所で使用できるメソッドだなと感じました。
(匿名型を使うとnew回数がかなり増えるので、メモリに厳しい分野では使いにくいところもありますが…)
これからもこういうものを探求していきたいと思います。