Teilen über


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 eigene Typen 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.

Alternativer Ansatz mitstatic

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 bei den vorherigen Beispielen benötigen Sie eine Implementierung von IChangeToken, die vom changeTokenProducer erzeugt wird. Der Producer ist als Func<IChangeToken> definiert, und es wird erwartet, dass bei jedem Aufruf ein neues Token zurückgegeben wird. Der consumer ist entweder eine Action, wenn nicht state verwendet wird, oder ein Action<TState>, bei dem der generische Typ TState die Änderungsbenachrichtigung durchläuft.

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 Teilzeichenfolge, die als StringSegment-Typ struct bezeichnet wird. 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 Durchlaufen der 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
Tokenizer 3,315 ms 0,0659 ms 0,0705 ms 0,32
Split 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