foreach文でインデックスにアクセスしたい場合
C#ではforeachを使用したとき、インデックスでアクセスする方法は大きく2通りある。
(1) 一時変数を使用する
string[] arr = new string[] {"led", "blue", "green"};
int index = -1;
foreach (var item in arr)
{
index++;
System.Diagnostics.Debug.WriteLine(arr[index]);
}
カウントアップ用変数をforeachの外側で用意してやる。
ちなみに初期値を-1にして最初にインクリメントしてるのは、foreach内でcontinueが実行されたとき
抜けなくインデックスのカウントアップを行うためだ。
少し前にどっかのブログか記事かに書いてあった。
(2) Selectでインデックスと値のペアをつくる
string[] arr = new string[] {"led", "blue", "green"};
foreach (var item in arr.Select((value, index) => new { value, index }))
{
System.Diagnostics.Debug.WriteLine(arr.item.index);
}
この場合はindexがまさしくインデックス。valueが実際の値(この例では"led", "blue", "green")
結局どっちがいいのかパフォーマンスを測ってみた
なんとなくエンジニア界隈からすると(1)のパターンは受けが悪い。一時変数使うのダサいよねーみたいな。
なんとなくLINQで匿名型を使う(2)の方がテクニカルっぽく見える。見えるんだけどこれも匿名型を無駄に生成しているところがパフォーマンス的に気になる。
印象とかそう思うでは埒があかないので実測してみた。
比較は文字列のListに対して、
(1) for文で処理した場合
(2) foreachを一時変数を使用して処理した場合
(3) foreachを一時変数を使用しないで処理した場合
で比較してみる。
環境は
Windows 8.1
Visual Studio 2017
メモリ8G
i5の3.2GHzのPCを使用する。
var list = new List<string>();
for (int i = 0; i < 100000; i++)
{
list.Add(string.Format($"string{i}"));
}
Stopwatch sw = new Stopwatch();
// for
sw.Start();
for (int i = 0; i < list.Count; i++)
{
System.Diagnostics.Debug.WriteLine(list[i]);
}
sw.Stop();
Console.WriteLine(string.Format($"for文:{sw.Elapsed}"));
sw.Reset();
System.Threading.Thread.Sleep(1000);
//foreach:一時変数
sw.Start();
int index = -1;
foreach (var item in list)
{
index++;
System.Diagnostics.Debug.WriteLine(list[index]);
}
sw.Stop();
Console.WriteLine(string.Format($"foreach文(一時変数):{sw.Elapsed}"));
sw.Reset();
System.Threading.Thread.Sleep(1000);
//foreach
sw.Start();
foreach (var item in list.Select((v, i) => new { v, i }))
{
System.Diagnostics.Debug.WriteLine(list[item.i]);
}
sw.Stop();
Console.WriteLine(string.Format($"foreach文:{sw.Elapsed}"));
sw.Reset();
Console.ReadKey();
一時変数を使うパターンの方が速いという結果が出た。
(2) 1000件の場合
ここでも一時変数を使うパターンの方が速い
(3) 10000件の場合
少し差が出たみたいだ。ほんの僅かだが一時変数を使うパターンの方が速い
(4) 100000件の場合
ここにきてほんの僅かだが一時変数を使用しないパターンが初めて上回った。
オブジェクトの場合のパフォーマンスはどうなるか
先ほどの実験は文字列に対してだった。
実際の業務アプリではリストには何らかのオブジェクトを詰めていることもあるだろう。
簡単なクラスを定義してリストに詰めて同じように繰り返し処理を行ってみる。
var list2 = new List<Test>();
for (int i = 0; i < 100; i++)
{
var entity = new Test();
entity.ID = i;
entity.Name = string.Format($"TestName{i}");
entity.Birthday = "1980/10/17";
entity.AreaID = i * 10;
list2.Add(entity);
}
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < list2.Count; i++)
{
System.Diagnostics.Debug.WriteLine(list2[i]);
}
sw.Stop();
Console.WriteLine(string.Format($"for文:{sw.Elapsed}"));
sw.Reset();
System.Threading.Thread.Sleep(1000);
//foreach:一時変数
sw.Start();
int index = -1;
foreach (var item in list2)
{
index++;
System.Diagnostics.Debug.WriteLine(list2[index]);
}
sw.Stop();
Console.WriteLine(string.Format($"foreach文(一時変数):{sw.Elapsed}"));
sw.Reset();
System.Threading.Thread.Sleep(1000);
//foreach
sw.Start();
foreach (var item in list2.Select((value, index) => new { value, index }))
{
System.Diagnostics.Debug.WriteLine(list2[item.index]);
}
sw.Stop();
Console.WriteLine(string.Format($"foreach文:{sw.Elapsed}"));
sw.Reset();
Console.ReadKey();
class Test
{
public int ID { get; set; }
public string Name { get; set; }
public string Birthday { get; set; }
public int AreaID { get; set; }
public Test() { }
}
余談だが、(3)のパターンで実際のインスタンスのプロパティにアクセスするにはvalueからアクセスすればいい。
item.value.ID, item.value.Nameみたいにとれる。(value=Testオブジェクトなので)
では測定。
(1) 100件の場合
一時変数を使用しないパターンが一番速い。
(2) 1000件の場合
一時変数を使用しないパターンが一番速い。
(3) 10000件の場合
一時変数を使用しないパターンが一番速い。
(4) 100000件の場合
一時変数を使用しないパターンが初めて上回った。
結論みたいな締め
ひょっとしたらキャッシュとかきちんとクリアしてやんなきゃダメだったかな…
多分本格的に検証するならもっといろいろ気をつけるところあるんだろーなー
とりあえず今回言えることは**別にどっちもパフォーマンス的には大差ないから好きな方で書いてよし!**くらいしか言えねえ。
あとは実際に使用するオブジェクトの構造とか想定されるデータ量とかメモリ量とか、あとはレビュアーとか他のメンバーのレベルとかコーディング規約とかと照らし合わせて書きましょうとかいう何の意味もない結論しか出ねえ。
いまだにいますからね。LINQ知らないおじさんとか。
あとはC#7.0の機能かな。Tupple型を使うといいよねって話。
https://ufcpp.net/blog/2016/12/tipsindexedforeach/
今後はC#もガンガンTupple使っていくといいのかもしれません。