プリミティブ: .NET 用拡張機能ライブラリ
この記事では、Microsoft.Extensions.Primitives ライブラリについて説明します。 この記事のプリミティブは、BCL や C# 言語の .NET プリミティブ型と混同 "しないでください"。 プリミティブのライブラリ内の型は、それとは異なり、次のような周辺の .NET NuGet パッケージの構成要素として機能します。
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.FileExtensions
Microsoft.Extensions.FileProviders.Composite
Microsoft.Extensions.FileProviders.Physical
Microsoft.Extensions.Logging.EventSource
Microsoft.Extensions.Options
System.Text.Json
変更通知
変更が発生したときの通知の伝達は、プログラミングの基本的な概念です。 観察されるオブジェクトの状態は、多くの場合、変化する可能性があります。 変更が発生した場合、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 のインスタンスが作成され、その Token が CancellationChangeToken コンストラクターに渡されています。 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:
string
をStringSegment
インスタンスにトークン化します。 - StringValues:
null
、0、1、または多数の文字列を効率的な方法で表します。
StringSegment
型
このセクションでは、部分文字列の最適化された表現方法である StringSegment struct
型について説明します。 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
値、offset
、length
を使用して StringSegment
のインスタンスを作成しています。 StringSegment.Buffer は元の文字列引数であり、StringSegment.Value は StringSegment.Offset と StringSegment.Length の値に基づいた部分文字列です。
StringSegment
構造体には、セグメントを操作するための多くのメソッドが用意されています。
StringTokenizer
型
StringTokenizer オブジェクトは、string
を StringSegment
インスタンスにトークン化する構造体型です。 大きな文字列のトークン化には、通常、文字列を分割し、それに対して反復処理を実行することが必要です。 そのような場合は、おそらく 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
を操作できるようになります。
StringTokenizer
と string.Split
を比較するベンチマーク
文字列のスライスとダイスにはさまざまな方法がありますが、2 つの方法をベンチマークで比較するのが適切でしょう。 BenchmarkDotNet NuGet パッケージを使用して、次の 2 つのベンチマーク メソッドを考えてみましょう。
StringTokenizer の使用:
StringBuilder buffer = new(); var tokenizer = new StringTokenizer( s_nineHundredAutoGeneratedParagraphsOfLoremIpsum, new[] { ' ', '.' }); foreach (StringSegment segment in tokenizer) { buffer.Append(segment.Value); }
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
を操作することができます。
関連項目
.NET