LoginSignup
2
3

More than 3 years have passed since last update.

forとforeach、どちらのループを使用するのがいいのか考えてみた!

Last updated at Posted at 2021-04-17


正直プログラム経験がまだまだ浅いから間違いはあるかも。ご指摘下さればありがたいです。
Qiitaに不慣れなため、見にくかったらごめんなさい!

きっかけ

https://akinow.livedoor.blog/archives/52474053.html

そもそものきっかけは、上記の記事でforeachの使用を控えるような内容を見たからだった。
(後々分かったが、これはUnity5.5で解決済みだった)


軽量化に力を入れていきたい身としては、重いとかメモリの効率が~なんて話は聞き捨てならない。
たとえ解決済みだとしても、気になる。一度気になりだしたら止まらない。

・・・まあ、結果。ハマりましたよ。
見事にループ処理の沼にハマったので、同じような疑問を感じた人と、未来の自分用のメモとして、結局何がどういいのかまとめてみる。


コンパイル結果は、sharplabにお世話になりました。
内部分のコードなんかはactual implementationを参照させていただきました。

配列編


コンパイル用に以下のようなコードを作成したので、コンパイルしてみる。
sumに配列の中身を足していく処理だ。

コンパイルしたいだけなので、sumがリセットされてないとか、配列に何も入っていないとか。
その辺は見逃してほしい・・・。

int[] TestArray = new int[100];
int sum = 0;

//forの処理
for (int i = 0; i < TestArray.Length; i++)
{
    sum += TestArray[i];
}

//foreachの処理
foreach (int i in TestArray)
{
    sum += i;
}

↓ コンパイル

int[] TestArray = new int[100];
int sum = 0;

//forの処理
int num = 0;
while (num < TestArray.Length)
{
    sum += array[num];
    num++;
}

//foreachの処理
int[] array2 = TestArray;
int num2 = 0;
while (num2 < array2.Length)
{
    int num3 = array2[num2];
    sum += num3;
    num2++;
}


上記のような結果が出力された。



・・・おんなじやん!


配列におけるループの内部処理は、単純にint型の変数を一つ用意して、ループ毎に足し、配列の大きさより大きくなったら、whileから抜けるだけのようだ。

まあ、強いて言えばここで配列が参照されているぐらいか。

int[] array2 = TestArray;

ちなみに配列は参照型なので、コピーはされない。上記のように書いても、渡されるのは配列のアドレス情報のみ。この程度なら問題はない。

もしも知らない!気になった!って方はその辺は論点がずれてしまうので、以下サイト様がわかりやすいと思う。

https://ufcpp.net/study/csharp/oo_reference.html#type-category


さて、結果としては、やってることが同じならforを使うメリットは無い。
そもそも、foreachは糖衣構文( 要は読み書きのしやすさを重要視されている処理 )だから、優先してforeachを使いたい。

List編

Listを回す際に、どちらを使用すべきか。

配列に対してのコンパイル結果は、分かりやすく非常に簡単な結果であったが、ListはEnumeratorというものが関わってくるため、少々厄介だったりする。

まずはコンパイルしてみる。内容はさっきと同じ。

List<int> TestList = new List<int>();
int sum = 0;

//forの処理
for (int i = 0; i < TestList.Count; i++)
{
    sum += TestList[i];
}

//foreachの処理
foreach (int i in TestList)
{
    sum += i;
}

↓ コンパイル

List<int> list = new List<int>();
int sum = 0;

//forの処理
int num = 0;
while (num < list.Count)
{
    sum += list[num];
    num++;
}

//foreachの処理
List<int>.Enumerator enumerator = list.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
      int current = enumerator.Current;
      sum += current;
    }
}
finally
{
    ((IDisposable)enumerator).Dispose();
}

ちょっとごちゃごちゃしてきたので、混乱するかもしれないが順番に紐解いていこう。
まず、forの部分は先ほどの配列とやり方は変わっていないので、言わなくても分かると思う。


問題はforeachの方だろう。

まず、Listは幾つかのインターフェースを継承しているのだが、以下の行でその中の一つであるIEnumerable内の唯一のメソッドであるGetEnumerator()というメソッドにアクセスしている。

List<int>.Enumerator enumerator = list.GetEnumerator();

GetEnumerator()が一体何をするメソッドなのか。

では、GetEnumerator()とはなんなのか。

簡単にまとめるとGetEnumerator()というメソッドは、List内にあるEnumeratorという構造体に自身の情報を設定して返すだけのメソッドだ。

もう少し詳しく言うならば、GetEnumerator()を呼び出した際に、Enumeratorのコンストラクタにアクセスし、渡されたList、初期index、渡された時点での_version、currentを設定し、設定したEnumeratorを返すメソッドだ。

IEnumerator内のメソッド

次に以下の処理に注目してもらいたい。

    while (enumerator.MoveNext())
    {
      int current = enumerator.Current;
      sum += current;
    }

次に疑問に思うのは、MoveNext()とは何者なのかという点だろう。
MoveNext()はEnumeratorが継承するインターフェースの一つであるIEnumerator内のbool型のメソッドだ。
こいつは簡単に言うと、List内の要素数が超えたらループを抜けて、要素を超えていない場合はループを続けるという処理が内部的に行われている。

もう少し深掘りしてみよう。
MoveNext()を覗いてみると、以下のようになっている。

public bool MoveNext() 
{
    List<T> localList = list;

    if (version == localList._version && ((uint)index < (uint)localList._size)) 
    {                                                     
        current = localList._items[index];                    
        index++;
        return true;
    }
    return MoveNextRare();
}

private bool MoveNextRare()
{                
    if (version != list._version) 
    {
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }

    index = list._size + 1;
    current = default(T);
    return false;                
}

これだけでは、訳が分からないかもしれないので、順番に見ていこう。

まず、以下の部分で、Listのバージョンが合っているか。現在探索している要素がList内の要素数を超えていないか。を調べている。
もし、条件が合っていた場合、current変数に現在の要素を設定し、探索位置をずらし、ループは続くのでtrueを返している。

    if (version == localList._version && ((uint)index < (uint)localList._size)) 
    {                                                     
        current = localList._items[index];                    
        index++;
        return true;
    }

では、条件が満たされなかった場合はどうなるのか。
その場合は、MoveNextRare()に入る。

MoveNextRare()の処理は、まずversionが合っているか確認し、合っていなかった場合はエラーを吐く。
もしあっていた場合は、currentを初期化してfalseを返すことでループを終了します。

private bool MoveNextRare()
{                
    if (version != list._version) 
    {
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }

    index = list._size + 1;
    current = default(T);
    return false;                
}

これがだいたいのforeachのList探索の概要です。


では、結局どっちをつかうのがいいのか?
以下のサイト様を見た所、速度面ではforの方がforeachよりも早い。

https://takap-tech.com/entry/2020/10/20/234610

メモリ面から見ても、GetEnumerator()でEnumeratorを生成している分、forの方がいいように感じられるが、私個人の見解としては、そこまで気にするほどではないように感じた。

よっぽど、速度を重要視する場合や、メモリ効率を考えたい!といった場合でない限りは、糖衣構文であるforeachの使用を避ける程ではないように思う。
まあ、この程度であれば好みの範疇かな?

結果

2021/04/21修正:配列の結果がforになってました!!!ごめんんささい!!

配列:foreach
List:基本的にforeachでいい。よっぽど気にするようであればfor

以上!



ご指摘、ご意見おまちしておりまs・・・



めtyくちゃ参考にさせていただきましたサイト様

https://qiita.com/NCT48/items/d8394d587e9fee969ca9

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3