読みやすいコードのためにLINQを利用する。

最初に断っておきたいこと

C#3.0の話です。
LINQやラムダ式自体の説明はしていません。
簡単なサンプルにするため仕様がシンプルで効果を感じられないかもしれませんが、
もしもっと複雑な仕様だったら等、想像してみてください。

まず最初に、下記のサンプルを見てください。

簡単な仕様として、

受け取った在庫リストの集計対象のものだけ集計したい。
集計対象とは、数量が10より大きくて、数量を1000で割った時の余りが0のものの事である。

を実現します。

class サンプル1
{
private class 在庫計
{
public int 数量計 { get; set; }
public int 金額計 { get; set; }
}
public void サンプルメソッド(在庫[] 在庫リスト)
{
在庫計 在庫計情報 = new 在庫計();
foreach (在庫 在庫情報 in 在庫リスト)
{
if (在庫情報.数量 > 10)
{
if ((在庫情報.数量 % 1000 == 0))
{
在庫計情報.数量計 += 在庫情報.数量;
在庫計情報.金額計 += 在庫情報.金額;
}
}
}
}
}

この例の場合、たしかに仕様を実現していますが、if文が実現している仕様についてコード上から読み取ることができません。

そこで、ラムダ式を利用し、集計対象の時にtrueを返す匿名メソッドを作成して実装してみます。

class サンプル2
{
private class 在庫計
{
public int 数量計 { get; set; }
public int 金額計 { get; set; }
}
public void サンプルメソッド(在庫[] 在庫リスト)
{
Func<在庫, Boolean> 集計対象 = 在庫情報 =>
{
if (在庫情報.数量 <= 10)
{
return false;
}
if (在庫情報.数量 % 1000 != 0)
{
return false;
}
return true;
};
在庫計 在庫計情報 = new 在庫計();
foreach (在庫 在庫情報 in 在庫リスト)
{
if (集計対象(在庫情報))
{
在庫計情報.数量計 += 在庫情報.数量;
在庫計情報.金額計 += 在庫情報.金額;
}
}
}

その結果、集計対象の在庫のみ集計するという部分と、集計対象である条件の詳細部分が分離されました。

ここでさらにLINQを利用して、ループの部分が実際に何をしているのかをコードに残すようにします。

public class サンプル3
{
public void サンプルメソッド(在庫[] 在庫リスト)
{
Func<在庫, bool> 集計対象 = 在庫情報 =>
{
if (在庫情報.数量 <= 10)
{
return false;
}
if (在庫情報.数量 % 1000 != 0)
{
return false;
}
return true;
};
var 在庫計情報 = new
{
数量計 = 在庫リスト
.Where(集計対象)
.Sum(x => x.数量),
金額計 = 在庫リスト
.Where(集計対象)
.Sum(x => x.金額)
};
}
}

もしくは、

public class サンプル4
{
public void サンプルメソッド(在庫[] 在庫リスト)
{
Func<在庫, bool> 抽出条件 = 在庫情報 =>
{
if (在庫情報.数量 <= 10)
{
return false;
}
if (在庫情報.数量 % 1000 != 0)
{
return false;
}
return true;
};
var 在庫計情報 = new
{
数量計 = (from 在庫情報 in 在庫リスト
where 抽出条件(在庫情報)
select 在庫情報.数量).Sum()
,
金額計 = (from 在庫情報 in 在庫リスト
where 抽出条件(在庫情報)
select 在庫情報.金額).Sum()
};
}
}

その結果、ループがなくなりましたが、在庫リストから集計対象のもののみを抽出(Where)し、集計(Sum)している事がコードに埋め込まれました。

また、在庫計情報をループで加算しないため、サンプル2までは在庫計というinner classを作成していましたが、その必要がなくなり、その場限りの匿名型で済むようになりました。

ところでサンプルコードだけを見ているとわかりませんが、集計した結果がint型の範囲を超えた時、サンプル2まで(ループで集計している)とサンプル3以降(LINQを利用している。)で実行時の挙動が違います。

サンプル2までは、例外が発生せず、マイナス値になってしまいます。

サンプル3以降は、ちゃんとOverflowExceptionが発生します。

もしサンプル2までのコードで例外が発生するようにする場合は

単純に

//抜粋
foreach (在庫 在庫情報 in 在庫リスト)
{
if (集計対象(在庫情報))
{
if (在庫計情報.金額計 + 在庫情報.金額 > int.MaxValue)
{
throw new OverflowException();
}
在庫計情報.数量計 += 在庫情報.数量;
在庫計情報.金額計 += 在庫情報.金額;
}
}

とすると、intの範囲を超えた場合

在庫計情報.金額計 + 在庫情報.金額

がマイナス値になるため

在庫計情報.金額計 + 在庫情報.金額 > int.MaxValue

の条件がtrueになることが無いため、いつまでも例外が発生しません。

そのためより値のとれる範囲の大きいlongに型変換したうえで、

//抜粋
foreach (在庫 在庫情報 in 在庫リスト)
{
if (集計対象(在庫情報))
{
if ((long)在庫計情報.金額計 + (long)在庫情報.金額 > int.MaxValue)
{
throw new OverflowException();
}
在庫計情報.数量計 += 在庫情報.数量;
在庫計情報.金額計 += 在庫情報.金額;
}
}

とするか、金額にマイナス値が無い前提であれば、

//抜粋
foreach (在庫 在庫情報 in 在庫リスト)
{
if (集計対象(在庫情報))
{
if (在庫計情報.金額計 + 在庫情報.金額 < 0)
{
throw new OverflowException();
}
在庫計情報.数量計 += 在庫情報.数量;
在庫計情報.金額計 += 在庫情報.金額;
}
}

とする必要があります。

つまり、うっかり間違った実装をしてしまうと、オーバフロー時に例外を発行させているつもりで、実は発行していないという状況になってしまいますので、そういう点でも、このくらいの集計ならLINQを利用した方が安全です。

Share