院長のメモ帖
2012年5月15日 火曜日
IEnumerableとForEach拡張メソッド
久しぶりの更新です。
またまた動物病院とは関係のない話です。
C#のコーディングについての覚書です。
顧客管理ソフトを自作した話は、前に少しふれましたが、一応動くものは昨年末に出来上がりましたが、それから半年ぐらいたった今でも、ちょこちょこコードをいじってバグとりやら新機能を加えたりしてます。
ふつう一人で書いたコードは一貫性があるものですが、C#4.0での初めての本格プログラムだったので、手さぐりしながら書いたこともあり、最初のほうのコードと現時点で書いているコードの質が随分と違ってしまいました。
とくに、ラムダ式とかLINQなんかは、徐々に使い方にも慣れてきてだいぶ効率よく書けるようになりました。
そんな中で、これはやってはいけないというクエリーを大量に書いていることに気が付きました。
それは、IEnumerableのシーケンスの各要素にループ操作を行う時、一旦ToList()してインスタンス化してから処理していたことです。
以下のメソッドAとBを見てください
public static int A()
{
int ret=0;
Enumerable.Range(0, 1000).ToList().ForEach(r => ret += r);
return ret;
}
public static int B()
{
int ret = 0;
IEnumerable<int> TestData = Enumerable.Range(0, 1000);
foreach (int r in TestData)
ret += r;
return ret;
}
AとBで各要素に対する処理は同じですが、Aのほうが行数が少なく、変数宣言がない分、ソースがすっきりします。BもTestDataを省いて1行減らすことはもちろん可能ですが、クエリは非常に長くなることが多いので、foreachステートメント中にクエリの塊を一気に書くことは可読性を著しく下げます。Aなら改行するだけで可読性を維持できます。
差はないのならよみやすいほうがよいと思って、A()の書き方をLINQを使い始めてからずっとしていました。
ところがです。ベンチマークを取ったところ、AはBの2倍ぐらい時間を食っていました。リスト化は結構重たい処理で、これではLINQを使うことでかえってパフォーマンスを落としてしまいました。
しかしです。Effective C# 4.0を読みかえして重要なことを読み流していたことに気が付きました。それは、IEnumerbleに拡張メソッドとしてForAll<T>を加えようということです。僕は、ForEachのほうがしっくりと思うので、ForAllからForEachにメソッド名を直して、以下の拡張メソッドを自分のライブラリー内の静的クラスに加えました。
public static void ForEach<T>(this IEnumerable<T>
foreach (T item in sequence)
action(item);
}
そうしますとあら不思議、Aメソッドは次のA2メソッドに改変できます。
public static int A2()
{
int ret = 0;
Enumerable.Range(0, 1000).ForEach(r => ret += r);
return ret;
}
ベンチマークもBとA2では誤差範囲の違いしかなく、圧倒的に楽して書けます。これはぬかったと思いました。AからBへ直すべきと思いながらも、めんどくさくてほかっておいたのですが、AからA2へはソリューション全体にToList().ForEach()から、ForEach()への全置換を実行するだけで終了しました。
IEnumerableというかLINQはそれ自体拡張メソッドの塊ですが、使い勝手を良くするためにさらなる拡張メソッドを自作するというのは発想の転換ですね。これでforeachステートメントをつかうことはほとんどなくなりそうですし、この考え方を利用すると、さらにコードの質を高めれそうです。
またまた動物病院とは関係のない話です。
C#のコーディングについての覚書です。
顧客管理ソフトを自作した話は、前に少しふれましたが、一応動くものは昨年末に出来上がりましたが、それから半年ぐらいたった今でも、ちょこちょこコードをいじってバグとりやら新機能を加えたりしてます。
ふつう一人で書いたコードは一貫性があるものですが、C#4.0での初めての本格プログラムだったので、手さぐりしながら書いたこともあり、最初のほうのコードと現時点で書いているコードの質が随分と違ってしまいました。
とくに、ラムダ式とかLINQなんかは、徐々に使い方にも慣れてきてだいぶ効率よく書けるようになりました。
そんな中で、これはやってはいけないというクエリーを大量に書いていることに気が付きました。
それは、IEnumerableのシーケンスの各要素にループ操作を行う時、一旦ToList()してインスタンス化してから処理していたことです。
以下のメソッドAとBを見てください
public static int A()
{
int ret=0;
Enumerable.Range(0, 1000).ToList().ForEach(r => ret += r);
return ret;
}
public static int B()
{
int ret = 0;
IEnumerable<int> TestData = Enumerable.Range(0, 1000);
foreach (int r in TestData)
ret += r;
return ret;
}
AとBで各要素に対する処理は同じですが、Aのほうが行数が少なく、変数宣言がない分、ソースがすっきりします。BもTestDataを省いて1行減らすことはもちろん可能ですが、クエリは非常に長くなることが多いので、foreachステートメント中にクエリの塊を一気に書くことは可読性を著しく下げます。Aなら改行するだけで可読性を維持できます。
差はないのならよみやすいほうがよいと思って、A()の書き方をLINQを使い始めてからずっとしていました。
ところがです。ベンチマークを取ったところ、AはBの2倍ぐらい時間を食っていました。リスト化は結構重たい処理で、これではLINQを使うことでかえってパフォーマンスを落としてしまいました。
しかしです。Effective C# 4.0を読みかえして重要なことを読み流していたことに気が付きました。それは、IEnumerbleに拡張メソッドとしてForAll<T>を加えようということです。僕は、ForEachのほうがしっくりと思うので、ForAllからForEachにメソッド名を直して、以下の拡張メソッドを自分のライブラリー内の静的クラスに加えました。
public static void ForEach<T>(this IEnumerable<T>
sequence, Action<T> action)
{foreach (T item in sequence)
action(item);
}
そうしますとあら不思議、Aメソッドは次のA2メソッドに改変できます。
public static int A2()
{
int ret = 0;
Enumerable.Range(0, 1000).ForEach(r => ret += r);
return ret;
}
ベンチマークもBとA2では誤差範囲の違いしかなく、圧倒的に楽して書けます。これはぬかったと思いました。AからBへ直すべきと思いながらも、めんどくさくてほかっておいたのですが、AからA2へはソリューション全体にToList().ForEach()から、ForEach()への全置換を実行するだけで終了しました。
IEnumerableというかLINQはそれ自体拡張メソッドの塊ですが、使い勝手を良くするためにさらなる拡張メソッドを自作するというのは発想の転換ですね。これでforeachステートメントをつかうことはほとんどなくなりそうですし、この考え方を利用すると、さらにコードの質を高めれそうです。