プリミティブ: .NET 用拡張機能ライブラリ

この記事では、Microsoft.Extensions.Primitives ライブラリについて説明します。 この記事のプリミティブは、BCL や C# 言語の .NET プリミティブ型と混同 "しないでください"。 プリミティブのライブラリ内の型は、それとは異なり、次のような周辺の .NET NuGet パッケージの構成要素として機能します。

変更通知

変更が発生したときの通知の伝達は、プログラミングの基本的な概念です。 観察されるオブジェクトの状態は、多くの場合、変化する可能性があります。 変更が発生した場合、Microsoft.Extensions.Primitives.IChangeToken インターフェイスの実装を使用して、関係者にその変更を通知することができます。 使用できる実装は次のとおりです。

開発者は、独自の型を自由に実装することもできます。 IChangeToken インターフェイスにはいくつかのプロパティが定義されています。

  • IChangeToken.HasChanged: 変更が発生したかどうかを示す値を取得します。
  • IChangeToken.ActiveChangeCallbacks: このトークンによってコールバックが事前に生成されるかどうかを示します。 false の場合、トークン コンシューマーは HasChanged をポーリングして変更を検出する必要があります。

インスタンスベースの機能

次のような CancellationChangeToken の使用例を考えてみましょう。

CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);

Console.WriteLine($"HasChanged: {cancellationChangeToken.HasChanged}");

static void callback(object? _) =>
    Console.WriteLine("The callback was invoked.");

using (IDisposable subscription =
    cancellationChangeToken.RegisterChangeCallback(callback, null))
{
    cancellationTokenSource.Cancel();
}

Console.WriteLine($"HasChanged: {cancellationChangeToken.HasChanged}\n");

// Outputs:
//     HasChanged: False
//     The callback was invoked.
//     HasChanged: True

前の例では、CancellationTokenSource のインスタンスが作成され、その TokenCancellationChangeToken コンストラクターに渡されています。 HasChanged の初期状態がコンソールに書き込まれます。 コールバックが呼び出されたときにコンソールに書き込まれる Action<object?> callback が作成されます。 callback を使用すると、トークンの RegisterChangeCallback(Action<Object>, Object) メソッドが呼び出されます。 using ステートメント内で、cancellationTokenSource が取り消されます。 これによりコールバックがトリガーされ、HasChanged の状態が再びコンソールに書き込まれます。

複数の変更元からアクションを実行する必要がある場合は、CompositeChangeToken を使用します。 この実装では、1 つ以上の変更トークンを集計し、変更がトリガーされた回数にかかわらず、登録された各コールバックが 1 回だけトリガーされます。 次の例を考えてみましょう。

CancellationTokenSource firstCancellationTokenSource = new();
CancellationChangeToken firstCancellationChangeToken = new(firstCancellationTokenSource.Token);

CancellationTokenSource secondCancellationTokenSource = new();
CancellationChangeToken secondCancellationChangeToken = new(secondCancellationTokenSource.Token);

CancellationTokenSource thirdCancellationTokenSource = new();
CancellationChangeToken thirdCancellationChangeToken = new(thirdCancellationTokenSource.Token);

var compositeChangeToken =
    new CompositeChangeToken(
        new IChangeToken[]
        {
            firstCancellationChangeToken,
            secondCancellationChangeToken,
            thirdCancellationChangeToken
        });

static void callback(object? state) =>
    Console.WriteLine($"The {state} callback was invoked.");

// 1st, 2nd, 3rd, and 4th.
compositeChangeToken.RegisterChangeCallback(callback, "1st");
compositeChangeToken.RegisterChangeCallback(callback, "2nd");
compositeChangeToken.RegisterChangeCallback(callback, "3rd");
compositeChangeToken.RegisterChangeCallback(callback, "4th");

// It doesn't matter which cancellation source triggers the change.
// If more than one trigger the change, each callback is only fired once.
Random random = new();
int index = random.Next(3);
CancellationTokenSource[] sources = new[]
{
    firstCancellationTokenSource,
    secondCancellationTokenSource,
    thirdCancellationTokenSource
};
sources[index].Cancel();

Console.WriteLine();

// Outputs:
//     The 4th callback was invoked.
//     The 3rd callback was invoked.
//     The 2nd callback was invoked.
//     The 1st callback was invoked.

前の C# コードでは、3 つの CancellationTokenSource オブジェクト インスタンスが作成され、対応する CancellationChangeToken インスタンスとペアになっています。 CompositeChangeToken コンストラクターにトークンの配列を渡すことで、複合トークンのインスタンスが作成されます。 Action<object?> callback が作成されますが、今回は state オブジェクトが使用され、書式が設定されたメッセージとしてコンソールに書き込まれます。 コールバックは 4 回登録されますが、状態オブジェクト引数はそれぞれ少しずつ異なります。 このコードを使用すると、疑似乱数ジェネレーターを使用して変更トークン ソースのいずれかを選択し (どれでも構いません)、その Cancel() メソッドを呼び出すことができます。 これにより、変更がトリガーされ、登録されている各コールバックが一度だけ呼び出されます。

代替の static の方法

RegisterChangeCallback を呼び出す代わりに、Microsoft.Extensions.Primitives.ChangeToken 静的クラスを使用することもできます。 次のような消費パターンを考えてみましょう。

CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);

IChangeToken producer()
{
    // The producer factory should always return a new change token.
    // If the token's already fired, get a new token.
    if (cancellationTokenSource.IsCancellationRequested)
    {
        cancellationTokenSource = new();
        cancellationChangeToken = new(cancellationTokenSource.Token);
    }

    return cancellationChangeToken;
}

void consumer() => Console.WriteLine("The callback was invoked.");

using (ChangeToken.OnChange(producer, consumer))
{
    cancellationTokenSource.Cancel();
}

// Outputs:
//     The callback was invoked.

これまでの例と同様に、changeTokenProducer によって生成される IChangeToken の実装が必要です。 この生成機能は Func<IChangeToken> と定義され、呼び出しごとに新しいトークンが返されることが想定されています。 consumer は、state を使用しない場合は Action であり、ジェネリック型 TState が変更通知を通過する場合は Action<TState> です。

文字列のトークナイザー、セグメント、値

アプリケーション開発で文字列を操作することはよくあります。 文字列のさまざまな表現に対して解析、分割、反復処理が行われます。 プリミティブ ライブラリには、文字列の操作をさらに最適化、効率化するために役立ついくつかの型が用意されています。 次の型を検討してください。

  • StringSegment: 部分文字列の最適化された表現。
  • StringTokenizer: stringStringSegment インスタンスにトークン化します。
  • StringValues: null、0、1、または多数の文字列を効率的な方法で表します。

StringSegment

このセクションでは、部分文字列の最適化された表現方法である StringSegmentstruct 型について説明します。 StringSegment プロパティと AsSpan メソッドの一部を示す次の C# コード例を考えてみましょう。

var segment =
    new StringSegment(
        "This a string, within a single segment representation.",
        14, 25);

Console.WriteLine($"Buffer: \"{segment.Buffer}\"");
Console.WriteLine($"Offset: {segment.Offset}");
Console.WriteLine($"Length: {segment.Length}");
Console.WriteLine($"Value: \"{segment.Value}\"");

Console.Write("Span: \"");
foreach (char @char in segment.AsSpan())
{
    Console.Write(@char);
}
Console.Write("\"\n");

// Outputs:
//     Buffer: "This a string, within a single segment representation."
//     Offset: 14
//     Length: 25
//     Value: " within a single segment "
//     " within a single segment "

前のコードでは、string 値、offsetlength を使用して StringSegment のインスタンスを作成しています。 StringSegment.Buffer は元の文字列引数であり、StringSegment.ValueStringSegment.OffsetStringSegment.Length の値に基づいた部分文字列です。

StringSegment 構造体には、セグメントを操作するための多くのメソッドが用意されています。

StringTokenizer

StringTokenizer オブジェクトは、stringStringSegment インスタンスにトークン化する構造体型です。 大きな文字列のトークン化には、通常、文字列を分割し、それに対して反復処理を実行することが必要です。 そのような場合は、おそらく String.Split が頭に浮かぶのではないでしょうか。 これらの API は似ていますが、一般には StringTokenizer の方が優れたパフォーマンスです。 まず、次の例を考えてみましょう。

var tokenizer =
    new StringTokenizer(
        s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
        new[] { ' ' });

foreach (StringSegment segment in tokenizer)
{
    // Interact with segment
}

前のコードでは、自動生成された 900 個の段落がある Lorem Ipsum のテキストと、空白文字 ' ' の 1 つの値を持つ配列を使用して StringTokenizer 型のインスタンスが作成されています。 トークナイザー内の各値は StringSegment と表されます。 このコードによってセグメントが反復処理され、コンシューマーから各 segment を操作できるようになります。

StringTokenizerstring.Split を比較するベンチマーク

文字列のスライスとダイスにはさまざまな方法がありますが、2 つの方法をベンチマークで比較するのが適切でしょう。 BenchmarkDotNet NuGet パッケージを使用して、次の 2 つのベンチマーク メソッドを考えてみましょう。

  1. StringTokenizer の使用:

    StringBuilder buffer = new();
    
    var tokenizer =
        new StringTokenizer(
            s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
            new[] { ' ', '.' });
    
    foreach (StringSegment segment in tokenizer)
    {
        buffer.Append(segment.Value);
    }
    
  2. String.Split の使用:

    StringBuilder buffer = new();
    
    string[] tokenizer =
        s_nineHundredAutoGeneratedParagraphsOfLoremIpsum.Split(
            new[] { ' ', '.' });
    
    foreach (string segment in tokenizer)
    {
        buffer.Append(segment);
    }
    

どちらのメソッドも API の表面領域は似ており、大きな文字列をチャンクに分割することができます。 次のベンチマーク結果によると StringTokenizer の方法が約 3 倍高速ですが、結果は異なる場合があります。 あらゆるパフォーマンスの考慮事項と同様に、具体的なユース ケースを評価する必要があります。

Method 平均 エラー StdDev 比率
Tokenizer 3.315 ms 0.0659 ms 0.0705 ms 0.32
Split 10.257 ms 0.2018 ms 0.2552 ms 1.00

凡例

  • 平均: すべての測定値の算術平均
  • エラー: 99.9% 信頼区間の半分
  • 標準偏差: すべての測定値の標準偏差
  • 中央値: すべての測定値の上位半分を区切る値 (50 パーセンタイル)
  • 比率: 比率分布の平均値 (現在/ベースライン)
  • 比率標準偏差: 比率分布の標準偏差 (現在/ベースライン)
  • 1 ms: 1 ミリ秒 (0.001 秒)

.NET でのベンチマークの詳細については、BenchmarkDotNet を参照してください。

StringValues

StringValues オブジェクトは、null、0、1、または多数の文字列を効率的に表す struct 型です。 StringValues 型は、string? または string?[]? のいずれかの構文を使用して構築できます。 前の例のテキストを使用して、次の C# コードを考えてみましょう。

StringValues values =
    new(s_nineHundredAutoGeneratedParagraphsOfLoremIpsum.Split(
        new[] { '\n' }));

Console.WriteLine($"Count = {values.Count:#,#}");

foreach (string? value in values)
{
    // Interact with the value
}
// Outputs:
//     Count = 1,799

前のコードを実行すると、文字列値の配列を使用して StringValues オブジェクトのインスタンスが作成されます。 StringValues.Count はコンソールに書き込まれます。

StringValues 型は、次のコレクション型の実装です。

  • IList<string>
  • ICollection<string>
  • IEnumerable<string>
  • IEnumerable
  • IReadOnlyList<string>
  • IReadOnlyCollection<string>

そのため、必要に応じて反復処理を行い、各 value を操作することができます。

関連項目