はじめに
第一回です!
ていうか、初めてのQiita投稿です!
最近、自分のC#の書き方や設計が一般的なものとかけ離れていないか心配になりました。また、業務ではC#6.0・7.0や.NET Coreを使ったことがありません。(ただ、直近の新しいプロジェクトでは使う予定です。)
そこで、改めてC#を勉強するために、.NET Coreのソースでも読もうかと思い、本投稿を始めました。
最初は、C#では、なくてはならないListクラスを読んでみます。
状態を持つクラスの設計が学べるかなと期待しています。
ソースリンク
ソースを読む!
クラス宣言
public class List<T> : IList<T>, IList, IReadOnlyList<T>
ジェネリックではない、IListを実装していますね。下位互換を考えているのですね。
IReadOnlyListにて、読み取り専用として参照できるようにしているのですね。
IReadOnlyListは、比較的新しく.NET Framework4.5で追加されたみたいですね。
でも、IEnumerableで参照することが多く、IEnumerable自体は読み取りしかないので、IReadOnlyListはあんまり使おうと思わないかな。
定数
private const int DefaultCapacity = 4;
定数はパスカルケースですね。
C#は、定数をパスカルケースで書くのが一般的ですね。
ただ、Java出身の私はいつも「DEFAULT_CAPACITY」みたいなアッパーケースで書いてしまう癖があるので、これからはちゃんとパスカルケースで書かないとね。
private static readonly T[] s_emptyArray = new T[0];
おっと、constでなければ、スネークケース?
ジェネリックで動的に型が決まるから定数ではないということかな?
この定数は、コンストラクタで使います。
インスタンス変数
private T[] _items; // Do not rename (binary serialization)
private int _size; // Do not rename (binary serialization)
private int _version; // Do not rename (binary serialization)
[NonSerialized]
private object _syncRoot;
インスタンス変数は「_」始まりですね。
コメントに「Do not rename (binary serialization)」とありますね。きっと互換性のためでしょうね。ここの変数名を変えてしまうと、シリアライズした過去のバージョンのクラスがデシリアライズ出来なくなるからでしょうね。結構気使ってくれておりますね。
_versions変数って、なんか、内容が変更されるたびに増やしているみたいですね。
ForEachメソッドの中では、ForEachメソッドを実行中に変更があった場合に、InvalidOperationExceptionを発生させるために使っていますね。
スレッドセーフではないからこのような制御があるようですね。
めんどくさいですね・・・。
シリアライズ対象外の_syncRoot変数は、SyncRootプロパティで使ってます。
スレッドセーフな処理をするために使うようなので、シリアライズ対象外のようです。
コンストラクタ
public List()
{
_items = s_emptyArray;
}
デフォルトコンストラクタですね。
_items変数は、空っぽの配列で初期化するんですね。
nullで初期化だと、色々チェックがあるから面倒なんでしょうね。
C#8.0の「nullable reference types」ってのは、参照型がデフォルトでnull非許容になるみたいですね。これは素晴らしい!(しかし、めちゃくちゃ大きな変更だな・・・)
public List(int capacity)
{
if (capacity < 0)
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
if (capacity == 0)
_items = s_emptyArray;
else
_items = new T[capacity];
}
あらかじめ入る予定の数を決めておくためのコンストラクタですね。
ここで、10,000と指定したら10,000のサイズになるのではなく、内部的に10,000の配列をあらかじめ用意するのですね。
デフォルトのcapacityは「4」のようです。
最初からデータ量が多いと分かっている場合は、ここで想定のサイズを指定しておくのが定石ですね。
ここで指定すれば、Addの時に配列を何度も作り直す事がないから、サイズが大きいときに指定すると性能が向上するのですね。
public List(IEnumerable<T> collection)
{
if (collection == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
if (collection is ICollection<T> c)
{
int count = c.Count;
if (count == 0)
{
_items = s_emptyArray;
}
else
{
_items = new T[count];
c.CopyTo(_items, 0);
_size = count;
}
}
else
{
_size = 0;
_items = s_emptyArray;
AddEnumerable(collection);
}
}
「if (collection is ICollection c)」って、collectionがICollection型だったら、trueとなり、かつ、c変数にキャスト後の値を入れるんですね。
これは、C#7.0からの機能ですね。知りませんでした。
ICollection型ならばCopyToを使うのは、きっと性能のためなんでしょうね。
IEnumerableなクラスを自作した場合は、ICollectionも実装すれば、このコンストラクタで性能向上が出来そうですね。(まあ、いざ実装するときには、この件は忘れてしまいそうですが・・・)
プロパティとメソッドは、また次回!
ライセンス
色々ソースを載せているので、念のためライセンスへリンクしておきます。