Condividi tramite


Primitivi: libreria di estensioni per .NET

In questo articolo conoscerai la libreria Microsoft.Extensions.Primitives. I primitivi di questo articolo non devono essere confusi con i tipi primitivi .NET del BCL o del linguaggio C#. I tipi all'interno della libreria dei primitivi fungono invece da blocchi predefiniti per alcuni dei pacchetti NuGet delle periferiche .NET, ad esempio:

Notifiche relative a modifiche

La propagazione delle notifiche quando si verifica una modifica è un concetto fondamentale nella programmazione. Lo stato osservato di un oggetto, il più delle volte, può cambiare. Quando si verifica una modifica, le implementazioni dell'interfaccia Microsoft.Extensions.Primitives.IChangeToken possono essere usate per notificare alle parti interessate tale modifica. Le implementazioni disponibili sono le seguenti:

Come sviluppatore, sei libero di implementare anche il tuo tipo. L'interfaccia IChangeToken definisce alcune proprietà:

Funzionalità basate su istanza

Considera l'esempio di utilizzo seguente di 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

Nell'esempio precedente viene creata un'istanza di CancellationTokenSource e il suo Token è passato al costruttore CancellationChangeToken. Lo stato iniziale di HasChanged viene scritto nella console. Viene creato un Action<object?> callback che scrive quando il callback viene richiamato nella console. Il metodo RegisterChangeCallback(Action<Object>, Object) del token viene chiamato, dato il callback. All'interno dell'istruzione using, il cancellationTokenSource viene annullato. In questo modo viene attivato il callback e lo stato di HasChanged viene nuovamente scritto nella console.

Quando devi intervenire da più origini di modifica, usa il CompositeChangeToken. Questa implementazione aggrega uno o più token di modifica e attiva ogni callback registrato esattamente una volta indipendentemente dal numero di volte in cui viene attivata una modifica. Si consideri l'esempio seguente:

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.

Nel codice C# precedente vengono create e associate tre istanze di oggetti CancellationTokenSource alle istanze corrispondenti CancellationChangeToken. Viene creata un'istanza del token composito passando una matrice dei token al costruttore CompositeChangeToken. Viene creato Action<object?> callback, ma questa volta l'oggetto state viene usato e scritto nella console come messaggio formattato. Il callback viene registrato quattro volte, ognuna con un argomento oggetto di stato leggermente diverso. Il codice usa un generatore di numeri pseudo-casuali per selezionare una delle origini del token di modifica (non importa quale origine) e chiamare il relativo metodo Cancel(). In questo modo viene attivata la modifica, richiamando ogni callback registrato una sola volta.

Approccio alternativo static

Oltre a chiamare RegisterChangeCallback, è possibile usare la classe statica Microsoft.Extensions.Primitives.ChangeToken. Considera il modello di consumo seguente:

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.

Analogamente agli esempi precedenti, è necessaria un'implementazione di IChangeToken prodotta da changeTokenProducer. Il producer viene definito come Func<IChangeToken> e si prevede che restituisca un nuovo token a ogni chiamata. consumer è un Action quando non si usa state o un Action<TState> quando il tipo generico TState passa attraverso la notifica delle modifiche.

Tokenizer di stringa, segmenti e valori

L'interazione con le stringhe è comune nello sviluppo di applicazioni. Diverse rappresentazioni di stringhe vengono analizzate, suddivise o iterate. La libreria di primitivi offre alcuni tipi di scelta che consentono di interagire con stringhe più ottimizzate ed efficienti. Considera i seguenti tipi:

  • StringSegment: rappresentazione ottimizzata di una sottostringa.
  • StringTokenizer: tokenizza string in istanze di StringSegment.
  • StringValues: rappresenta null, zero, una o più stringhe in modo efficiente.

Il tipo StringSegment

In questa sezione imparerai a conoscere una rappresentazione ottimizzata di una substring nota come tipo StringSegmentstruct. Considera l'esempio di codice C# seguente che mostra alcune proprietà StringSegment e il metodo AsSpan:

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 "

Il codice precedente crea un'istanza di StringSegment, dato un valore string, un offset e un length. StringSegment.Buffer è l'argomento stringa originale e StringSegment.Value è la sottostringa in base ai valori StringSegment.Offset e StringSegment.Length.

Lo struct StringSegment fornisce molti metodi per interagire con il segmento.

Il tipo StringTokenizer

L'oggetto StringTokenizer è un tipo di struct che tokenzza un string in istanze di StringSegment. La tokenizzazione di stringhe di grandi dimensioni comporta in genere la divisione della stringa e l'iterazione su di essa. Detto questo, probabilmente viene in mente String.Split. Queste API sono simili, ma in generale StringTokenizer offre prestazioni migliori. Prima di tutto, considera l'esempio seguente:

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

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

Nel codice precedente viene creata un'istanza del tipo StringTokenizer con 900 paragrafi di testo Lorem Ipsum generati automaticamente e una matrice con un singolo valore di uno spazio vuoto ' '. Ogni valore all'interno del tokenizer è rappresentato come StringSegment. Il codice esegue l'iterazione dei segmenti, consentendo al consumer di interagire con ogni segment.

Confronto tra benchmark StringTokenizer e string.Split

Con le varie modalità di sezionamento e suddivisione delle stringhe, è opportuno confrontare due metodi con un benchmark. Usando il pacchetto NuGet BenchmarkDotNet, considera i due metodi di benchmark seguenti:

  1. usando StringTokenizer:

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

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

I due metodi hanno un aspetto simile nell'area di attacco dell'API e sono entrambi in grado di suddividere una stringa di grandi dimensioni in blocchi. I risultati del benchmark riportati di seguito mostrano che l'approccio StringTokenizer è quasi tre volte più veloce, ma i risultati possono variare. Come per tutte le considerazioni sulle prestazioni, è consigliabile valutare il caso d'uso specifico.

metodo Media Error StdDev Ratio
Tokenizer 3.315 ms 0.0659 ms 0.0705 ms 0,32
Divisa 10.257 ms 0.2018 ms 0.2552 ms 1,00

Legenda

  • Media: media aritmetica di tutte le misurazioni
  • Errore: metà dell'intervallo di confidenza del 99,9%
  • Deviazione standard: deviazione standard di tutte le misurazioni
  • Valore mediano: valore che separa la metà superiore di tutte le misurazioni (50° percentile)
  • Rapporto: media della distribuzione del rapporto (corrente/baseline)
  • Deviazione standard del rapporto: deviazione standard della distribuzione del rapporto (corrente/baseline)
  • 1 ms: 1 millisecondo (0,001 sec)

Per altre informazioni sul benchmarking con .NET, vedi BenchmarkDotNet.

Il tipo StringValues

L'oggetto StringValues è un tipo struct che rappresenta null, zero, una o più stringhe in modo efficiente. Il tipo StringValues può essere costruito con una delle sintassi seguenti: string? o string?[]?. Usando il testo dell'esempio precedente, prendi in considerazione il codice C# seguente:

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

Il codice precedente crea un'istanza di un oggetto StringValues in base a una matrice di valori stringa. L'oggetto StringValues.Count viene scritto nella console.

Il tipo StringValues è un'implementazione dei tipi di raccolta seguenti:

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

Di conseguenza, può essere iterato e si può interagire con ogni value con in base alle esigenze.

Vedi anche