はじめに
動的配列
動的配列(C++でいうvector<T>)を扱う際,C#ではList<T>を使用します.
そこでタイトルにあるように任意の値・要素数で初期化したい時に,C++ではコンストラクタで(要素数)または(要素数,値)と与えることで目的の動作を得ることができます.
C#でも同じようなことを行いたいと思って調べたんですが,C++のように単純にはいかないようです.
公式リファレンスのList<T>のコンストラクタ部分を見てみます.
int型の引数をとっているそれっぽいコンストラクタがあります.
中身を詳しく見てみます.
このコンストラクタではCapacityというプロパティを設定しているようです.
Capacityに関してはこの記事がわかりやすいです.
要するにCapacityを設定することにより,高速にデータの追加を行うことができます.
ここで注意なのがこのCapacityはそのリストのサイズではないということです(ここでは説明を割愛します).
なので次のようなコードでは,a[i] = 0部分で例外をはいて止まります.
var capacity = 10;
var a = new List<int>(capacity);
for(var i = 0; i < capacity; i++)
{
a[i] = i;
// a.Add(i); <- これは大丈夫
}
コメントに示したようにAddメソッドを使用した場合は正しく動作します.
目標
この記事の目標としては,なんらかの方法を用いてListを生成し,以下のプログラムが通るようにしたいです.
var capacity = 10;
var a = /*なんらかのList生成処理.できれば一行に収めたい*/
for(var i = 0; i < capacity; i++)
{
a[i] = i; //a[i]に対する任意の処理
}
競プロなどでは,0やINFで埋めた配列に対して何らかの操作を行うことが多いので,デフォルトでこの機能を実装してもらいたいです(この記事で挙げる以外の方法でなにか公式な方法があるんですかね,stackoverflowにも同じような質問をしている人がいました).
ここではいくつかの実装方法を挙げて,後に実行速度を測定したいと思います.
実装
方法1:配列を利用する(初期化される値は0限定)
var capacity = 10_000_000;
var a = new List<int>(new int[capacity]);
var b = new int[capacity].ToList();
aはListのコンストラクタを使用しています.Listのコンストラクタでは,IEnumerableインターフェイスを実装しているクラスを引数に取ることができるので,指定要素数を備えた配列を引数に渡すことでListを生成する事ができます.
bではLINQを使用して配列をListに変換しています.
またnewすることで配列の中身は0に初期化されますが,逆に0以外の値を用いて一気に初期化することができません.
方法2:Enumerableクラスのメソッドを利用する
var capacity = 10_000_000;
var value = 10_000_000;
var a = new List<int>(Enumerable.Repeat(value, capacity));
var b = Enumerable.Repeat(value, capacity).ToList();
LINQのEnumerableクラスでは様々なメソッドが実装されています.
その中のRepeatメソッドを使用することで,指定した値で指定した要素数もつ配列を取得することができます.
それをListのコンストラクタに渡すか,LINQでListに変換することにより,目的の動作を得ます.
またEnumerableクラスではRangeメソッドも実装されているので,始点と終点の値を与えることで連続した数列を持つListも生成することが可能です.
方法3:List<T>のAddRangeメソッドを利用する
こちらの方法はコメントにて@NCT48 さんから教えていただきました.
var capacity = 10_000_000;
var value = 10_000_000;
var a = new List<int>(capacity);
a.AddRange(Enumerable.Repeat(value, capacity));
ワンライナーではなくなってしまいますが方法1,方法2より高速な方法になります.
詳細な解説はコメントを参照して下さい.
方法4:forを用いる
あとで速度測定を行いますが,一番高速な方法です.
var capacity = 10_000_000;
var value = 10_000_000;
var a = new List<int>(capacity);
for (int i = 0; i < capacity; i++) a.Add(value);
シンプルにforで要素を追加していきます.
スマートではないです.
その他の方法
拡張メソッドやリフレクションを使用する方法をコメントで教えていただきました.
気になる方はコメント欄も参照してみてください.
速度比較
それぞれの方法の速度をBenchmarkDotNetを使用して測定しました.
public class InitializeList
{
private readonly int capacity = 10_000_000;
private readonly int value = 10_000_000;
//方法1
[Benchmark]
public List<int> A() => new List<int>(new int[capacity]);
[Benchmark]
public List<int> B() => new int[capacity].ToList();
//方法2
[Benchmark]
public List<int> C() => new List<int>(Enumerable.Repeat(value, capacity));
[Benchmark]
public List<int> D() => Enumerable.Repeat(value, capacity).ToList();
//方法3
[Benchmark]
public List<int> E()
{
var a = new List<int>(capacity);
a.AddRange(Enumerable.Repeat(value, capacity));
return a;
}
//方法4
[Benchmark]
public List<int> F()
{
var a = new List<int>(capacity);
for (int i = 0; i < capacity; i++) a.Add(value);
return a;
}
}
結果
| Method | Mean | Error | StdDev | Median |
|---|---|---|---|---|
| A | 51.14 ms | 1.0135 ms | 2.670 ms | 50.12 ms |
| B | 49.74 ms | 0.9928 ms | 1.182 ms | 49.33 ms |
| C | 193.98 ms | 3.8219 ms | 7.178 ms | 193.63 ms |
| D | 191.14 ms | 3.8135 ms | 4.683 ms | 190.80 ms |
| E | 148.82 ms | 2.9274 ms | 3.371 ms | 149.65 ms |
| F | 58.48 ms | 1.0987 ms | 1.079 ms | 58.38 ms |
方法1(A,B)では,0以外の値で初期化できないというデメリットはありますが,実行速度だけでみるととても高速に動いています.
方法2(C,D)では,Enumerableクラスのメソッドを使用して一行で配列を生成していますが,Listのコンストラクタの仕様上結局内部でAddを呼び出しているだけ(コメント参照)らしいので,速度はあんまりです.
方法1と方法2より,LINQのToListメソッドを利用するほうがListのコンストラクタに直接渡すより若干ですが速くなりました.
方法3(E)では,AddRangeメソッドを使用することにより方法2より速くなりました.AddRangeメソッドでは内部的にInsertメソッドを呼びだしているらしいです.
方法4(F)ではシンプルな実装で目的の動作を得ることができる上,速度も高速です.
まとめ
通常使う場合は方法4で実装するといいかもしれません.
見た目はアレですが,速度の面で圧倒的に有利ですし,シンプルで見た人がわかりやすいです.
forで逐次的に要素を追加することにより,他の方法のように確保するメモリも節約できると考えられるため,その点も良いです.

