forループで配列走査することの気持ち悪さ

# はじめに
配列にアクセスしたいとき、forループで配列走査するコードを書くことがよくありますが、

個人的に**forループを使うのは気持ち悪い**という感情を抱くことがよくあります。

今回の記事では、forループを使うことに対して自分が**なぜ気持ち悪いと思ったのか**について書いていきたいと思います。

**独断と偏見**で書いてる部分が大きいので、この記事を読んでもあまり役に立たないかもしれません。
ご容赦ください。

# 記事中の言語について
C# (Unity)

# 記事の内容
・**配列長で**forループを回すことの意図、及びその危険性について　(方法１)
・配列長を**変数に入れて**forループを回すことの意図、及びその危険性について（方法２）
・配列の初期化に使用した**定数で**forループを回すことの意図、及びその危険性について（方法３）

# 今回考えるコード
今回の記事では以下のような特徴を持ったクラスを考えることにします。
・int型の定数Nと、配列a,bを持つ
・定数Nを使って配列aと配列bを初期化している
・forループで配列aと配列bへのアクセスを行う

```Test.cs
using UnityEngine;

public class Test
{
    const int N = 4; // とある数
    int[] a = new int[N]; // 配列
    int[] b = new int[N]; // 配列

    void Hoge() // 適当な名前の関数
    {
        for (int i = 0; i < a.Length; i++)
        {
            // aとbを取得して何かを行う
            Debug.Log(a[i]);
            Debug.Log(b[i]);
        }
    }
}
```

# 方法１: 配列の長さで直接回す
配列走査したいとき、配列aに関して**配列の長さa.Length**を使ってforループを回すという方法が１つ目に考えられます。

```csharp:全ての要素についてforループを回す
for (int i = 0; i < a.Length; i++) // aのすべての要素について処理を回す
{
    // aとbを取得して何かを行う
    Debug.Log(a[i]);
    Debug.Log(b[i]);
}
```

「配列の**全ての要素について処理を回したい**」という意図を強調したい場合、個人的にはこのような書き方をします。

## クラス全体
```Test.cs
using UnityEngine;

public class Test
{
    const int N = 4; // とある数
    int[] a = new int[N]; // 配列
    int[] b = new int[N]; // 配列

    void Hoge() // 適当な名前の関数
    {
        for (int i = 0; i < a.Length; i++)
        {
            // aとbを取得して何かを行う
            Debug.Log(a[i]);
            Debug.Log(b[i]);
        }
    }
}
```

## 方法１が保証するもの
方法１では配列aの**全ての要素についてforループが回る**ということが保証されます。

```csharp
for (int i = 0; i < a.Length; i++)
```

この1行を見るだけで、配列aの全ての要素が走査されることが読み取れます。

## 方法１の不具合発生リスク
方法１だと、**bの長さに変更がかかった場合に配列外アクセスが発生する**というリスクが存在します。

例えば、以下のようにして配列bの長さを変更したとします。

``` csharp:変更前
const int N = 4; // とある数
int[] a = new int[N]; // 配列
int[] b = new int[N]; // 配列
```

``` csharp:変更後
const int N = 4; // とある数
const int M = 3; // とある数
int[] a = new int[N]; // 配列
int[] b = new int[M]; // 配列
```

このとき、forループ内を修正し忘れると配列外アクセスが発生してしまいます。

```csharp:配列へのアクセス
for (int i = 0; i < a.Length; i++)
{
    // aとbを取得して何かを行う
    Debug.Log(a[i]);
    Debug.Log(b[i]); // 配列外アクセス
}
```

# 方法２：aの長さを変数化してforループを回す
２つ目の方法は、配列aの**長さを変数に入れて**その変数を使ってforループを回す、という書き方です。

```csharp:個数でforループを回す
int num = a.Length;
for (int i = 0; i < num; i++) // 数についてforループを回す
{
    // aとbを取得して何かを行う
    Debug.Log(a[i]);
    Debug.Log(b[i]);
}
```

全ての要素というよりは「配列の**個数に注目して**処理を回したい」という意図を強調したい場合、個人的にこのような書き方をすることがあります。

## クラス全体
```Test.cs
using UnityEngine;

public class Test
{
    const int N = 4; // とある個数
    int[] a = new int[N]; // 配列
    int[] b = new int[N]; // 配列

    void Hoge() // 適当な名前の関数
    {
        int num = a.Length;
        for (int i = 0; i < num; i++)
        {
            // aとbを取得して何かを行う
            Debug.Log(a[i]);
            Debug.Log(b[i]);
        }
    }
}
```

##方法２は方法１よりも不具合混入のリスクが高い 
方法１では配列aの全ての要素についてforループが回るということが保証されていましたが、
方法２だと全ての要素について**走査されない**ケースが出てきます。

```
for (int i = 0; i < num; i++)
```

<br>

例えば、ヒューマンエラーによってnumの初期化とforの間に**numを変更する処理が混入**してしまった場合などが考えられます。 この場合、配列走査が正しく行われないという不具合が発生してしまいます。

```Test.cs
int num = a.Length;
num = 2; // numを変更する処理
for (int i = 0; i < num; i++)
{
...
```

また、方法１と同じく方法２でも**bの長さに変更がかかった場合に**配列外アクセスが発生する危険性が存在します。

# 方法３ : 配列aの初期化に使った定数でforループを回す
３つ目の方法は、配列aの**初期化に使った定数N**を使ってforループを回すという方法です。

```csharp:定数でforループを回す
for (int i = 0; i < N; i++)
{
    // aとbを取得して何かを行う
    Debug.Log(a[i]);
    Debug.Log(b[i]);
}
```

個人的に、「クラス中で定義された**定数に注目して処理を回したい」**という意図を強調したい場合、個人的にこの書き方をします。


## クラス全体
```Test.cs
using UnityEngine;

public class Test
{
    const int N = 4; // とある個数
    int[] a = new int[N]; // 配列
    int[] b = new int[N]; // 配列

    void Hoge() // 適当な名前の関数
    {
        for (int i = 0; i < N; i++)
        {
            // aとbを取得して何かを行う
            Debug.Log(a[i]);
            Debug.Log(b[i]);
        }
    }
}
```
## 方法３の不具合発生リスク
```csharp:
for (int i = 0; i < N; i++)
```

このforループで配列の全ての要素が走査されるためには、
「配列aの初期化に**定数Nが使用されている**」
という約束が守られている必要があります。<br>

ヒューマンエラーによって配列の初期化に**別の定数が使用されていた場合**、不具合が発生してしまいます。


## 方法３の不具合の例
例えば、bの初期化にNとは別の定数Mを使用するように変更したとします。

```csharp:変更前
const int N = 4; // とある個数
int[] a = new int[N]; // 配列
int[] b = new int[N]; // 配列
```

```csharp:変更後
const int N = 4; // とある個数
const int M = 2;
int[] a = new int[N]; // 配列
int[] b = new int[M]; // 配列
```


forループ内を修正しなかった場合に、配列外アクセスが発生してしまいます。

```csharp:配列へのアクセス
for (int i = 0; i < N; i++)
{
    // aとbを取得して何かを行う
    Debug.Log(a[i]);
    Debug.Log(b[i]); // 配列外アクセス
}
```

# まとめ
・forループを使った配列走査は**ヒューマンエラーによる不具合**が発生しそうで気持ち悪い

・foreachを使えばヒューマンエラーは減らせそうだけど、処理負荷があがるのがネック

・コードを入念にチェックすればコード変更時の不具合は未然に防げるけど、不具合が発生するリスクはできるだけ抑えたい。
~~**やっぱりforループは気持ち悪い**~~

**ループ内での配列アクセスは不具合を招きそうなので気持ち悪い**