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:
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
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à:
- IChangeToken.HasChanged: ottiene un valore che indica se è stata apportata una modifica.
- IChangeToken.ActiveChangeCallbacks: indica se il token genera in modo proattivo i callback. Se
false
, il consumer del token deve eseguire il pollingHasChanged
per rilevare le modifiche.
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 diStringSegment
. - 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:
usando StringTokenizer:
StringBuilder buffer = new(); var tokenizer = new StringTokenizer( s_nineHundredAutoGeneratedParagraphsOfLoremIpsum, new[] { ' ', '.' }); foreach (StringSegment segment in tokenizer) { buffer.Append(segment.Value); }
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.