はじめに
配列にアクセスしたいとき、forループで配列走査するコードを書くことがよくありますが、
個人的にforループを使うのは気持ち悪いという感情を抱くことがよくあります。
今回の記事では、forループを使うことに対して自分がなぜ気持ち悪いと思ったのかについて書いていきたいと思います。
独断と偏見で書いてる部分が大きいので、この記事を読んでもあまり役に立たないかもしれません。
ご容赦ください。
記事中の言語について
C# (Unity)
記事の内容
・配列長でforループを回すことの意図、及びその危険性について (方法1)
・配列長を変数に入れてforループを回すことの意図、及びその危険性について(方法2)
・配列の初期化に使用した定数でforループを回すことの意図、及びその危険性について(方法3)
今回考えるコード
今回の記事では以下のような特徴を持ったクラスを考えることにします。
・int型の定数Nと、配列a,bを持つ
・定数Nを使って配列aと配列bを初期化している
・forループで配列aと配列bへのアクセスを行う
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]);
}
}
}
方法1: 配列の長さで直接回す
配列走査したいとき、配列aに関して配列の長さa.Lengthを使ってforループを回すという方法が1つ目に考えられます。
for (int i = 0; i < a.Length; i++) // aのすべての要素について処理を回す
{
// aとbを取得して何かを行う
Debug.Log(a[i]);
Debug.Log(b[i]);
}
「配列の全ての要素について処理を回したい」という意図を強調したい場合、個人的にはこのような書き方をします。
クラス全体
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]);
}
}
}
方法1が保証するもの
方法1では配列aの全ての要素についてforループが回るということが保証されます。
for (int i = 0; i < a.Length; i++)
この1行を見るだけで、配列aの全ての要素が走査されることが読み取れます。
方法1の不具合発生リスク
方法1だと、bの長さに変更がかかった場合に配列外アクセスが発生するというリスクが存在します。
例えば、以下のようにして配列bの長さを変更したとします。
const int N = 4; // とある数
int[] a = new int[N]; // 配列
int[] b = new int[N]; // 配列
const int N = 4; // とある数
const int M = 3; // とある数
int[] a = new int[N]; // 配列
int[] b = new int[M]; // 配列
このとき、forループ内を修正し忘れると配列外アクセスが発生してしまいます。
for (int i = 0; i < a.Length; i++)
{
// aとbを取得して何かを行う
Debug.Log(a[i]);
Debug.Log(b[i]); // 配列外アクセス
}
方法2:aの長さを変数化してforループを回す
2つ目の方法は、配列aの長さを変数に入れてその変数を使ってforループを回す、という書き方です。
int num = a.Length;
for (int i = 0; i < num; i++) // 数についてforループを回す
{
// aとbを取得して何かを行う
Debug.Log(a[i]);
Debug.Log(b[i]);
}
全ての要素というよりは「配列の個数に注目して処理を回したい」という意図を強調したい場合、個人的にこのような書き方をすることがあります。
クラス全体
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]);
}
}
}
方法2は方法1よりも不具合混入のリスクが高い
方法1では配列aの全ての要素についてforループが回るということが保証されていましたが、
方法2だと全ての要素について走査されないケースが出てきます。
for (int i = 0; i < num; i++)
例えば、ヒューマンエラーによってnumの初期化とforの間にnumを変更する処理が混入してしまった場合などが考えられます。 この場合、配列走査が正しく行われないという不具合が発生してしまいます。
int num = a.Length;
num = 2; // numを変更する処理
for (int i = 0; i < num; i++)
{
...
また、方法1と同じく方法2でもbの長さに変更がかかった場合に配列外アクセスが発生する危険性が存在します。
方法3 : 配列aの初期化に使った定数でforループを回す
3つ目の方法は、配列aの初期化に使った定数Nを使ってforループを回すという方法です。
for (int i = 0; i < N; i++)
{
// aとbを取得して何かを行う
Debug.Log(a[i]);
Debug.Log(b[i]);
}
個人的に、「クラス中で定義された**定数に注目して処理を回したい」**という意図を強調したい場合、個人的にこの書き方をします。
クラス全体
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]);
}
}
}
方法3の不具合発生リスク
for (int i = 0; i < N; i++)
このforループで配列の全ての要素が走査されるためには、
「配列aの初期化に定数Nが使用されている」
という約束が守られている必要があります。
ヒューマンエラーによって配列の初期化に別の定数が使用されていた場合、不具合が発生してしまいます。
方法3の不具合の例
例えば、bの初期化にNとは別の定数Mを使用するように変更したとします。
const int N = 4; // とある個数
int[] a = new int[N]; // 配列
int[] b = new int[N]; // 配列
const int N = 4; // とある個数
const int M = 2;
int[] a = new int[N]; // 配列
int[] b = new int[M]; // 配列
forループ内を修正しなかった場合に、配列外アクセスが発生してしまいます。
for (int i = 0; i < N; i++)
{
// aとbを取得して何かを行う
Debug.Log(a[i]);
Debug.Log(b[i]); // 配列外アクセス
}
まとめ
・forループを使った配列走査はヒューマンエラーによる不具合が発生しそうで気持ち悪い
・foreachを使えばヒューマンエラーは減らせそうだけど、処理負荷があがるのがネック
・コードを入念にチェックすればコード変更時の不具合は未然に防げるけど、不具合が発生するリスクはできるだけ抑えたい。
やっぱりforループは気持ち悪い
ループ内での配列アクセスは不具合を招きそうなので気持ち悪い