概要
List<T>
に同じ値をいくつも挿入することを考えます。
標準でInsertRange
メソッドが用意されており、まずはその利用が浮かびます。
List<int> list = [0, 1, 2];
list.InsertRange(1, [-1, -1, -1]);
// .NET 9 以降は params のシグネチャもある
list.InsertRange(1, -1, -1, -1);
ここで、挿入する個数が多かった場合はどうでしょうか。すべて書くのも大変なので、配列を用意し、そこに値を埋めて渡すことになりそうです。
その場合、値を埋めた配列を用意するコードが必要になります。また、外部領域を確保してそこに値を書き込んでからさらにリストにコピーするわけですが、インプレースに行えればそちらの方が良さそうです。
というわけで、以下のシグネチャの拡張メソッドとして、専用の処理を考えてみます。
public static void InsertMany<T>(this List<T> list, int index, T value, int count)
{
// ...
}
// list.InsertMany(1, -1, 10000);
スローヘルパーによる例外スロー
まず、引数を検証し、例外を投げることを考えます。
現在は標準で各種のスローヘルパーが用意されているので、それを使えます。
// スローヘルパーによる例外スロー
ArgumentNullException.ThrowIfNull(list, nameof(list));
ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)index, (uint)list.Count, nameof(index));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
Microsoft Learn によれば、ArgumentNullException.ThrowIfNull
メソッドは .NET 7、ArgumentOutOfRangeException.ThrowIfGreaterThan<T>
メソッドは .NET 8での追加です。
CollectionsMarshal.SetCount
メソッド
List<T>
型は、内部で要素の個数を変数として保持し、その値は標準のリスト操作の中で暗黙に更新されます。
自前でリスト操作を行うとき、それをどう更新するかが関心事となりますが、現在は専用の方法が提供されています。
// Count をセット + 内部バッファを確保
CollectionsMarshal.SetCount(list, list.Count + count);
必要なら内部バッファの拡張も同時に行われるため、別途EnsureCapacity
メソッドを呼ぶ必要はありません。
CollectionsMarshal.SetCount
メソッドは、.NET 8での追加です。
List<T>
に対するスパン操作
現在は、List<T>
の内容をSpan<T>
として操作することが可能です。
Span<T>
として操作することにより、直感的なスライシングや、Fill
メソッドのようなスパンのメンバーを利用することができます。
// span を取得
var span = CollectionsMarshal.AsSpan(list);
// 挿入インデックスから後ろを count だけずらしてコピー
// オーバーラップしていても考慮してくれる
var src = span[index..^count];
var dst = span[(index + count)..];
src.CopyTo(dst);
// 挿入範囲に value を Fill
span.Slice(index, count).Fill(value);
Microsoft Learn によれば、CollectionsMarshal.AsSpan
メソッドは .NET 5での追加です。
UnsafeAccessor
標準で用意されているリスト操作では、内部でバージョンの更新を行います。これは例えば、イテレーター内部でコレクションの変更が行われてしまった場合にInvalidOperationException
として気付くことができる、といった意味があります。
バージョンはprivate
に管理されているため、更新するにはリフレクションを利用する必要が …… 現在ではありません。UnsafeAccessor
を通して触ることができます。
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_version")]
private static extern ref int _version<T>(List<T> list);
public static void InsertMany<T>(this List<T> list, int index, T value, int count)
{
// ...
// バージョン更新
_version(list)++;
}
UnsafeAccessor
自体は .NET 8 からですが、ジェネリック型への対応は、記事現在最新の .NET 9 からになります。
まとめ
以上をまとめると、以下のようになります。
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_version")]
private static extern ref int _version<T>(List<T> list);
public static void InsertMany<T>(this List<T> list, int index, T value, int count)
{
// スローヘルパーによる例外スロー
ArgumentNullException.ThrowIfNull(list, nameof(list));
ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)index, (uint)list.Count, nameof(index));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
// Count をセット + 内部バッファを確保
CollectionsMarshal.SetCount(list, list.Count + count);
// span を取得
var span = CollectionsMarshal.AsSpan(list);
// 挿入インデックスから後ろを count だけずらしてコピー
// オーバーラップしていても考慮してくれる
var src = span[index..^count];
var dst = span[(index + count)..];
src.CopyTo(dst);
// 挿入範囲に value を Fill
span.Slice(index, count).Fill(value);
// バージョン更新
_version(list)++;
}
リストに同じ値をいくつも挿入するという素朴な処理ではありますが、新しくは .NET 9 までの更新内容を利用しました。
一方で、処理の内容自体は、極端に言えばList<T>
が登場したランタイムバージョン(おそらく .NET Framework 2.0)でも書くことはできるはず。そう考えると、上から下まで違う書き方になるのが興味深いと感じました。
(後方互換性は基本的に保たれつつも)アップデートにつれ同じことを書くのにも変化していくのは C# の賛否あるとされることもある部分です。しかし、それに伴ってより直感的に書けるようになったり、パフォーマンスに考慮したものとなったりと、停滞しない姿勢が表れているように感じます。