【C#】List<T>を任意の値・要素数で初期化する

# はじめに

### 動的配列

動的配列(C++でいう`vector<T>`)を扱う際，C#では`List<T>`を使用します．
そこでタイトルにあるように任意の値・要素数で初期化したい時に，C++ではコンストラクタで`(要素数)`または`(要素数，値)`と与えることで目的の動作を得ることができます．

C#でも同じようなことを行いたいと思って調べたんですが，C++のように単純にはいかないようです．

[公式リファレンス](https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.generic.list-1?view=netframework-4.8#%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF%E3%83%BC)の`List<T>`のコンストラクタ部分を見てみます．

>![2019-06-24_11h34_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427721/6a4a4126-bb66-ae84-ac2b-fc4b316f259e.png)

int型の引数をとっているそれっぽいコンストラクタがあります．
[中身](https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.generic.list-1.-ctor?view=netframework-4.8#System_Collections_Generic_List_1__ctor_System_Int32_)を詳しく見てみます．

>![2019-06-24_12h05_48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/427721/3da816aa-dcaa-f1bd-da13-bb9091a4b148.png)

このコンストラクタでは`Capacity`というプロパティを設定しているようです．
`Capacity`に関しては[この記事](https://qiita.com/NCT48/items/5ca2df4e3dbd053cd9f7)がわかりやすいです．

要するに`Capacity`を設定することにより，高速にデータの追加を行うことができます．
ここで注意なのがこの`Capacity`は**そのリストのサイズではない**ということです(ここでは説明を割愛します)．

なので次のようなコードでは，`a[i] = 0`部分で例外をはいて止まります．

```cs
var capacity = 10;
var a = new List<int>(capacity);
for(var i = 0; i < capacity; i++)
{
    a[i] = i;
    // a.Add(i); <- これは大丈夫
} 
```

コメントに示したように`Add`メソッドを使用した場合は正しく動作します．

### 目標

この記事の目標としては，なんらかの方法を用いてListを生成し，以下のプログラムが通るようにしたいです．

```cs
var capacity = 10;
var a = /*なんらかのList生成処理．できれば一行に収めたい*/
for(var i = 0; i < capacity; i++)
{
    a[i] = i; //a[i]に対する任意の処理
} 
```

競プロなどでは，0やINFで埋めた配列に対して何らかの操作を行うことが多いので，デフォルトでこの機能を実装してもらいたいです(この記事で挙げる以外の方法でなにか公式な方法があるんですかね，stackoverflowにも同じような質問をしている人がいました)．

ここではいくつかの実装方法を挙げて，後に実行速度を測定したいと思います．

# 実装

### 方法１：配列を利用する(初期化される値は0限定)

```cs
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以外の値を用いて一気に初期化することができません．

### 方法２：Enumerableクラスのメソッドを利用する

```cs
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も生成することが可能です．

参考URL:https://docs.microsoft.com/ja-jp/dotnet/api/system.linq.enumerable.range?view=netframework-4.8#System_Linq_Enumerable_Range_System_Int32_System_Int32_

### 方法３：List\<T>のAddRangeメソッドを利用する

こちらの方法はコメントにて@NCT48 さんから教えていただきました．

```cs
var capacity = 10_000_000;
var value = 10_000_000;

var a = new List<int>(capacity);
a.AddRange(Enumerable.Repeat(value, capacity));
```

ワンライナーではなくなってしまいますが方法１，方法２より高速な方法になります．
詳細な解説はコメントを参照して下さい．


### 方法４：forを用いる

あとで速度測定を行いますが，一番高速な方法です．

```cs
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`を使用して測定しました．

```cs
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 |

方法１(A,B)では，0以外の値で初期化できないというデメリットはありますが，実行速度だけでみるととても高速に動いています．
方法２(C,D)では，`Enumerable`クラスのメソッドを使用して一行で配列を生成していますが，`List`のコンストラクタの仕様上結局内部で`Add`を呼び出しているだけ(コメント参照)らしいので，速度はあんまりです．
方法１と方法２より，`LINQ`の`ToList`メソッドを利用するほうが`List`のコンストラクタに直接渡すより若干ですが速くなりました．
方法３(E)では，`AddRange`メソッドを使用することにより方法２より速くなりました．`AddRange`メソッドでは内部的に`Insert`メソッドを呼びだしているらしいです．
方法４(F)ではシンプルな実装で目的の動作を得ることができる上，速度も高速です．

# まとめ

通常使う場合は**方法４**で実装するといいかもしれません．
見た目はアレですが，速度の面で圧倒的に有利ですし，シンプルで見た人がわかりやすいです．
`for`で逐次的に要素を追加することにより，他の方法のように確保するメモリも節約できると考えられるため，その点も良いです.