C#の基本的なコレクションとして、Listと配列=Array(int[]とか)があります。
例として、Listとint[]の違いについて考えてみます。
ネットをいろいろ見て回ると、Listの方が便利だから、全部Listでいんじゃね?みたいな論も見かけたりします。
確かにListで困らない人のいるんだろうなぁとは思いますが、私はなるべく使い分ける派です。
まず、基本から。
int[]とかの配列は、長さが固定です。固定長配列です。
配列は、オブジェクトとして作成されますので、ヒープメモリにメモリ確保されます。これはListも同じです。
長さが固定ということは、長さが変えられないということです。足りなくなった時は、長い配列を新しく作って、そちらにコピーするとかしか方法がありません。
次に、Listは、動的配列です。Addしていくと、長さが長くなっていきます。
あらかじめ長さがわかっていない時に、便利ですね。
でもちょっと待ってください。コンピューターでは、連続した場所でメモリを確保しないと効率が悪くなります。
動的配列って、言うのは簡単だけど、実際に作るのは結構厄介なのです。
じゃあ、Listがどうしているかというと、上でint[]で書いたのと同じです。
あらかじめある程度大きめの配列を作っておきます。Addして中身が増えていくと空きが減ります。
空きが0になってからAddされると、2倍の大きさの配列を作り直して、そちらにコピーします。それ以降は、新しい方の配列を使います。
List<int>は内部にint[]を保有しているわけです。
こんな仕組みで、一見容量制限のない動的配列を作っています。
外部から見た様は、int[]は固定配列、Listは動的配列。あらかじめ数が決まっている時はint[]で、変わる時はListです。
内部的には、どちらもint[]なので、大きな差はありません。Listは足りなくなった時に、勝手に増やしてくれます。
Listでは、確保した容量(capacity)と、Addされた容量(Count)が別に管理されていますので、int[]の変わるに使うと、int一つ分余計だとは言えます。
さて、実際に使っていく上で、他に違いは無いのでしょうか。
実は、foreachで回した時、int[]とListではパフォーマンスが異なります。int[]の方が速いです。
int[]をforeachで回すと、for(int i=0;i<array.Length;i++) と同じ形に展開されます。(実際にはforもforeachもwhileに展開される様です)
Listをforeachで回すと、list.GetEnumerator();とかが出てきて、違うコードになります。
実は、Listをforeachで回す時は、変更管理がされています。途中で配列の値を変えると、Exceptionが発生します。
ちょっとテストコードを書いてみます。
[TestMethod()]
public void ListTest()
{
var list = new List<int>() { 0, 1, 2, 3, 4 };
foreach(var item in list)
{
System.Diagnostics.Debug.WriteLine($"{item}");
if(item == 2)
{
list.Add(5);
}
}
}
// 結果
// 0
// 1
// 2
// 例外がスローされました:・・・
途中で変更すると、foreachが止まるんですね。
ちなみに、下のようにちょっと変えてみた場合も、結果は同じです。
[TestMethod()]
public void ListTest2()
{
var list = new List<int>() { 0, 1, 2, 3, 4 };
foreach (var item in list)
{
System.Diagnostics.Debug.WriteLine($"{item}");
if (item == 2)
{
list[4] = 99;
}
}
}
// 結果
// 0
// 1
// 2
// 例外がスローされました:・・・
ちなみに、上記の2つのケースは、List<int>をやめて、int[]を作った場合は、どちらもエラーになりません。
この変更があった時の挙動の違いは、int[],List<int>の使い分けに関係するかもしれませんね。
まあ、マルチスレッドじゃない場合は、あまり関係ない場合が多いとは思いますが。
最後に、パフォーマンス面を考えた時の注意事項を書いておきたいと思います。
Listは、大きなサイズの場合に、容量拡張のコピーが発生すると、結構時間がかかります。
容量拡張のコストは避けられるなら避けたいところです。
そこで、初期容量を指定することで、効率が良くなる場合があります。
初期容量を指定しないと、サイズは4になります。
例えば、種類が10あるけど、場合によっていくつか無効になるから、8個とか9個とかの数を管理したいなんて場合があるとします。
この場合、List(10)で作っておくと、コピーは起きないので効率が良いです。
また、初期化の時は数がわからないけど、初期化後は数が変わらないなんて場合もあります。
そんな時、初期化中は、Listで作っておいて、初期化の最後に、array = list.ToArray()で配列化してしまうという方法も私は良く使います。