6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

新しめの機能を駆使して List<T> に同じ値をいくつも挿入するメソッドを書く

Last updated at Posted at 2024-12-04

概要

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# の賛否あるとされることもある部分です。しかし、それに伴ってより直感的に書けるようになったり、パフォーマンスに考慮したものとなったりと、停滞しない姿勢が表れているように感じます。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?