Primitive: die Erweiterungsbibliothek für .NET

In diesem Artikel erfahren Sie mehr über die Bibliothek Microsoft.Extensions.Primitives. Die Primitive in diesem Artikel dürfen nicht mit primitiven .NET-Typen der BCL oder der C#-Sprache verwechselt werden. Die Typen in der Bibliothek „Primitives“ dienen als Bausteine für einige der .NET-NuGet-Peripheriepakete, z. B.:

Änderungsbenachrichtigungen

Das Propagieren von Benachrichtigungen bei einer Änderung ist ein grundlegendes Konzept bei der Programmierung. Der beobachtete Zustand eines Objekts kann sich häufiger ändern. Wenn eine Änderung auftritt, können Implementierungen der Microsoft.Extensions.Primitives.IChangeToken-Schnittstelle verwendet werden, um diejenigen über diese Änderung zu benachrichtigen, für die diese Änderung eine Rolle spielt. Folgende Implementierungen sind verfügbar:

Als Entwickler können Sie auch einen eigenen Typ implementieren. Die IChangeToken-Schnittstelle definiert einige Eigenschaften:

Instanzbasierte Funktionalität

Betrachten Sie die folgende Beispielverwendung von 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

Im vorherigen Beispiel wird eine CancellationTokenSource instanziiert und ihr Token an den CancellationChangeToken-Konstruktor übergeben. Der ursprüngliche Zustand von HasChanged wird in die Konsole geschrieben. Ein Action<object?> callback wird erstellt, der einen Schreibvorgang durchführt, wenn der Rückruf in die Konsole aufgerufen wird. Die RegisterChangeCallback(Action<Object>, Object)-Methode des Tokens wird aufgerufen und der callback übergeben. In der using-Anweisung wird die cancellationTokenSource abgebrochen. Dadurch wird der Rückruf ausgelöst, und der Zustand von HasChanged wird erneut in die Konsole geschrieben.

Wenn Sie Änderungen aus mehreren Änderungsquellen berücksichtigen müssen, verwenden Sie CompositeChangeToken. Diese Implementierung aggregiert mindestens ein Änderungstoken und löst jeden registrierten Rückruf genau einmal aus, unabhängig davon, wie oft eine Änderung ausgelöst wird. Betrachten Sie das folgenden Beispiel:

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.

Im obigen C#-Code werden drei Instanzen von CancellationTokenSource-Objekten erstellt und mit entsprechenden CancellationChangeToken-Instanzen gekoppelt. Das zusammengesetzte Token wird instanziiert, indem ein Array aus den Token an den CompositeChangeToken-Konstruktor übergeben wird. Der Action<object?> callback wird erstellt, aber dieses Mal wird das state-Objekt verwendet und als formatierte Meldung in die Konsole geschrieben. Der Rückruf wird viermal registriert, jeweils mit einem etwas anderen Zustandsobjektargument. Der Code verwendet einen Pseudozufallszahlen-Generator, um eine der Änderungstokenquellen (es spielt keine Rolle, welche das ist) auszuwählen und deren Cancel()-Methode aufzurufen. Dadurch wird die Änderung ausgelöst, und jeder registrierte Rückruf wird genau einmal aufgerufen.

Beachten Sie, dass ActiveChangeCallbacks des CompositeChangeToken is true ist, wenn mindestens ein internes Token aktive Callbacks unterstützt. Das zusammengesetzte Token abonniert nur die aktiven inneren Token. Um eine Änderung an einem passiven inneren Token zu erkennen, fragen Sie die HasChanged-Eigenschaft des zusammengesetzten Tokens ab.

Alternative Ansatz static

Als Alternative zum Aufrufen von RegisterChangeCallback können Sie die statische Klasse Microsoft.Extensions.Primitives.ChangeToken verwenden. Betrachten Sie das folgende Nutzungsmuster:

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.

Ähnlich wie in früheren Beispielen benötigen Sie eine IChangeToken-Implementierung, die von changeTokenProducer zurückgegeben wird. Der Produzent ist ein Func<IChangeToken?>. Nach einer Änderung sollte ein neues Token für die nächste Registrierung zurückgegeben werden. Wenn null zurückgegeben wird, registriert ChangeToken.OnChange keinen Callback. Geben Sie die von OnChange zurückgegebene IDisposable frei, um die Registrierung des Consumers aufzuheben.

Für einen synchronen Benutzer übergeben Sie eine Action oder Action<TState>. Ausnahmen vom Produzenten oder Konsumenten werden an den Aufrufer weitergegeben, der das Token registriert oder auslöst.

Ab .NET 11 können Sie für einen asynchronen Consumer ein Func<Task> oder Func<TState, Task> übergeben. ChangeToken.OnChange wartet, bis die zurückgegebene Aufgabe abgeschlossen ist, bevor der Consumer erneut aufgerufen werden kann. Dieses Verhalten verhindert gleichzeitige Aufrufe durch den Consumer, kann jedoch mehrere Änderungen, die während der Ausführung der Aufgabe auftreten, in einem einzigen späteren Callback zusammenfassen. Eine Ausnahme, die ausgelöst wird, bevor der Consumer seine Aufgabe zurückgibt, wird an den Code weitergegeben, der das Token registriert oder auslöst. Ausnahmen, die auftreten, nachdem die Aufgabe zurückgegeben wurde, bleiben unbeobachtet von ChangeToken.OnChange.

Note

Wenn Sie vorhandenen Code für .NET 11 neu kompilieren, kann eine async Lambda-Funktion anstelle der synchronen Action Überladung an eine neue Taskrückgabeüberladung gebunden werden. Die neue Bindung ändert den Zeitpunkt des Callback-Aufrufs von async void Verhalten zu taskbasiertem Verhalten. Weitere Informationen finden Sie unter Asynchrone ChangeToken.OnChange-Überladungen binden vorhandene Task-zurückgebende Rückrufe erneut.

Zeichenfolgentokenizer, Segmente und Werte

Die Interaktion mit Zeichenfolgen ist bei der Anwendungsentwicklung ein ganz alltäglicher Vorgang. Verschiedene Darstellungen von Zeichenfolgen werden analysiert, aufgeteilt oder durchlaufen. Die Primitives-Bibliothek enthält einige Typen zur Auswahl, mit denen die Interaktion mit Zeichenfolgen optimiert und effizienter wird. Betrachten Sie die folgenden Typen:

  • StringSegment: Eine optimierte Darstellung einer Teilzeichenfolge.
  • StringTokenizer: Tokenisiert ein string-Element in StringSegment-Instanzen.
  • StringValues: Stellt null, null, eine oder viele Zeichenfolgen auf effiziente Weise dar.

Der Typ StringSegment

In diesem Abschnitt erfahren Sie mehr über die optimierte Darstellung einer als StringSegmentstruct-Typ bezeichneten Teilzeichenfolge. Betrachten Sie das folgende C#-Codebeispiel, das einige der StringSegment-Eigenschaften und die AsSpan-Methode zeigt:

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 "

Der obige Code instanziiert das StringSegment mit angegebenen Werten für string, offset und length. Der StringSegment.Buffer ist das ursprüngliche Zeichenfolgenargument, und der StringSegment.Value ist die Teilzeichenfolge, die auf den Werten StringSegment.Offset und StringSegment.Length basiert.

Die StringSegment-Struktur bietet viele Methoden für die Interaktion mit dem Segment.

Der Typ StringTokenizer

Das StringTokenizer-Objekt ist ein Strukturtyp, der ein string-Element in StringSegment-Instanzen tokenisiert. Die Tokenisierung großer Zeichenfolgen umfasst in der Regel das Aufteilen und das Iterieren über die Zeichenfolge. In diesem Zusammenhang fällt Ihnen möglicherweise String.Split ein. Diese APIs sind ähnlich, im Allgemeinen bietet aber StringTokenizer eine bessere Leistung. Betrachten Sie zunächst das folgende Beispiel:

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

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

Im vorangehenden Code wird eine Instanz des Typs StringTokenizer mit 900 automatisch generierten Lorem Ipsum-Textabsätzen und ein Array mit einem einzelnen Wert eines Leerraumzeichens (' ') erstellt. Jeder Wert im Tokenizer wird als StringSegment dargestellt. Der Code durchläuft die Segmente, sodass der Consumer mit jedem segment interagieren kann.

Benchmark zum Vergleich von StringTokenizer mit string.Split

Angesichts der verschiedenen Methoden zum Slicing und Dicing von Zeichenfolgen ist es sinnvoll, zwei Methoden per Benchmark zu vergleichen. Wenn Sie das NuGet-Paket BenchmarkDotNet verwenden, betrachten Sie die folgenden beiden Benchmarkmethoden:

  1. Verwenden von StringTokenizer :

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

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

Beide Methoden sehen auf der API-Oberfläche ähnlich aus, und beide können eine große Zeichenfolge in kleinere Stücke aufteilen. Die Benchmarkergebnisse unten zeigen, dass der Ansatz mit einem StringTokenizer fast dreimal schneller ist, aber die Ergebnisse variieren können. Wie bei allen Überlegungen zur Leistung sollten Sie Ihren spezifischen Anwendungsfall auswerten.

Methode Mittelwert Fehler StdDev Verhältnis
Tokenisierer 3,315 ms 0,0659 ms 0,0705 ms 0,32
Aufteilen 10,257 ms 0,2018 ms 0,2552 ms 1,00

Legende

  • Mittelwert: arithmetisches Mittel aller Messungen
  • Fehler: Hälfte des Konfidenzintervalls von 99,9 %
  • Standardabweichung: Standardabweichung aller Messungen
  • Median: Wert, der die höhere Hälfte aller Messungen trennt (50. Perzentil)
  • Rate: Mittelwert der Ratenverteilung (aktuell/Baseline)
  • Standardabweichung für Rate: Standardabweichung der Ratenverteilung (aktuell/Baseline)
  • 1 ms: 1 Millisekunde (0,001 Sek.)

Weitere Informationen zu Benchmarks mit .NET finden Sie unter BenchmarkDotNet.

Der Typ StringValues

Das StringValues-Objekt ist ein struct-Typ, der null, null, eine oder viele Zeichenfolgen auf effiziente Weise darstellt. Der StringValues-Typ kann entweder mit der Syntax string? oder der Syntax string?[]? erstellt werden. Betrachten Sie den folgenden C#-Code, in dem der Text aus dem vorherigen Beispiel verwendet wird:

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

Der obige Code instanziiert ein StringValues-Objekt mit einem Array aus Zeichenfolgenwerten. StringValues.Count wird in die Konsole geschrieben.

Der StringValues-Typ ist eine Implementierung der folgenden Auflistungstypen:

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

Daher kann er durchlaufen werden, und mit jedem value kann nach Bedarf interagiert werden.

Siehe auch