Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

動的配列

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

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

公式リファレンスList<T>のコンストラクタ部分を見てみます.

2019-06-24_11h34_58.png

int型の引数をとっているそれっぽいコンストラクタがあります.
中身を詳しく見てみます.

2019-06-24_12h05_48.png

このコンストラクタでは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();

LINQEnumerableクラスでは様々なメソッドが実装されています.
その中の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_

方法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より,LINQToListメソッドを利用するほうがListのコンストラクタに直接渡すより若干ですが速くなりました.
方法3(E)では,AddRangeメソッドを使用することにより方法2より速くなりました.AddRangeメソッドでは内部的にInsertメソッドを呼びだしているらしいです.
方法4(F)ではシンプルな実装で目的の動作を得ることができる上,速度も高速です.

まとめ

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

2019Shun
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away