Primitivos: la biblioteca de extensiones para .NET
En este artículo, aprenderá sobre la biblioteca Microsoft.Extensions.Primitives. Los primitivos de este artículo no se deben confundir con los tipos primitivos de .NET de la BCL ni con los del lenguaje C#. En cambio, los tipos dentro de la biblioteca de primitivos sirven como bloques de creación para algunos de los paquetes periféricos de NuGet para .NET, como:
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
Notificaciones de cambios
La propagación de notificaciones cuando se produce un cambio es un concepto fundamental en programación. El estado observado de un objeto puede cambiar la mayoría de las veces. Cuando se produce un cambio, se pueden usar implementaciones de la interfaz Microsoft.Extensions.Primitives.IChangeToken para notificar a las partes interesadas dicho cambio. Las implementaciones disponibles son las siguientes:
Como desarrollador, también puede implementar su propio tipo. La interfaz IChangeToken define algunas propiedades:
- IChangeToken.HasChanged: obtiene un valor que indica si se ha producido un cambio.
- IChangeToken.ActiveChangeCallbacks: indica si el token generará devoluciones de llamada de forma proactiva. Si es
false
, el consumidor del token debe sondearHasChanged
para detectar los cambios.
Funcionalidad basada en instancias
Considere el siguiente uso de ejemplo de 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
En el ejemplo anterior, se crea una instancia de CancellationTokenSource y su Token se pasa al constructor CancellationChangeToken. El estado inicial de HasChanged
se escribe en la consola. Se crea un objeto Action<object?> callback
que escribe cuando se invoca la devolución de llamada a la consola. Se llama al método RegisterChangeCallback(Action<Object>, Object) del token, dado el objeto callback
. En la instrucción using
, el objeto cancellationTokenSource
se cancela. Como consecuencia se desencadena la devolución de llamada y el estado de HasChanged
se escribe de nuevo en la consola.
Cuando necesite realizar acciones desde varios orígenes de cambio, use CompositeChangeToken. Esta implementación agrega uno o varios tokens de cambio y activa cada devolución de llamada registrada exactamente una vez, con independencia del número de veces que se desencadene un cambio. Considere el ejemplo siguiente:
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.
En el código de C# anterior, se crean tres instancias de objetos CancellationTokenSource y se emparejan con las instancias de CancellationChangeToken correspondientes. Para crear una instancia del token compuesto, se pasa una matriz de los tokens al constructor CompositeChangeToken. Se crea Action<object?> callback
, pero esta vez el objeto state
se usa y se escribe en la consola como un mensaje con formato. La devolución de llamada se registra cuatro veces, cada una con un argumento de objeto de estado ligeramente diferente. El código usa un generador de números pseudoaleatorios para elegir uno de los orígenes de tokens de cambio (no importa cuál) y llamar a su método Cancel(). Como consecuencia, se desencadena el cambio, y se invoca cada devolución de llamada registrada exactamente una vez.
Enfoque static
alternativo
Como alternativa a la llamada a RegisterChangeCallback
, podría usar la clase estática Microsoft.Extensions.Primitives.ChangeToken. Tenga en cuenta el siguiente patrón de consumo:
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.
Al igual que en los ejemplos anteriores, necesitará una implementación de IChangeToken
producida por changeTokenProducer
. El productor se define como un objeto Func<IChangeToken>
y se espera que devuelva un nuevo token con cada invocación. El objeto consumer
es un elemento Action
cuando no se usa state
, o un elemento Action<TState>
cuando el tipo genérico TState
fluye a través de la notificación del cambio.
Tokenizadores de cadena, segmentos y valores
La interacción con cadenas es habitual en el desarrollo de aplicaciones. Se analizan, dividen o iteran varias representaciones de cadenas. La biblioteca de primitivo ofrece algunos tipos de elección que ayudan a que la interacción con las cadenas sea más optimizada y eficaz. Considere los tipos siguientes:
- StringSegment: una representación optimizada de una subcadena.
- StringTokenizer: tokeniza un elemento
string
en instancias deStringSegment
. - StringValues: representa
null
, cero, una o muchas cadenas de una forma eficaz.
El tipo StringSegment
En esta sección, aprenderá sobre una representación optimizada de una subcadena conocida como el tipo struct
de StringSegment. Considere el siguiente ejemplo de código de C# que muestra algunas de las propiedades de StringSegment
y el método 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 "
En el código anterior se crea una instancia de StringSegment
dado un valor string
, un valor offset
y un valor length
. StringSegment.Buffer es el argumento de cadena original, y StringSegment.Value es la subcadena basada en los valores StringSegment.Offset y StringSegment.Length.
La estructura StringSegment
proporciona muchos métodos para interactuar con el segmento.
El tipo StringTokenizer
El objeto StringTokenizer es un tipo de estructura que tokeniza un valor string
en instancias de StringSegment
. La tokenización de cadenas grandes normalmente implica dividir la cadena e iterar sobre ella. Dicho esto, probablemente venga a la mente String.Split. Estas API son similares, pero, en general, StringTokenizer un mejor rendimiento. En primer lugar, considere el ejemplo siguiente:
var tokenizer =
new StringTokenizer(
s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
new[] { ' ' });
foreach (StringSegment segment in tokenizer)
{
// Interact with segment
}
En el código anterior, se crea una instancia del tipo StringTokenizer
dados 900 párrafos de texto Lorem Ipsum generados automáticamente y una matriz con un valor único de un carácter de espacio en blanco ' '
. Cada valor del tokenizador se representa como StringSegment
. El código recorre en iteración los segmentos, lo que permite al consumidor interactuar con cada segment
.
Prueba comparativa entre StringTokenizer
y string.Split
Teniendo en cuenta las distintas formas de cortar y trocear las cadenas, se considera oportuno comparar dos métodos con un punto de referencia. Usando el paquete de NuGet BenchmarkDotNet, considere los dos métodos siguientes de comparación:
Con StringTokenizer :
StringBuilder buffer = new(); var tokenizer = new StringTokenizer( s_nineHundredAutoGeneratedParagraphsOfLoremIpsum, new[] { ' ', '.' }); foreach (StringSegment segment in tokenizer) { buffer.Append(segment.Value); }
Con String.Split :
StringBuilder buffer = new(); string[] tokenizer = s_nineHundredAutoGeneratedParagraphsOfLoremIpsum.Split( new[] { ' ', '.' }); foreach (string segment in tokenizer) { buffer.Append(segment); }
Ambos métodos tienen un aspecto similar en el área de superficie de la API y ambos son capaces de dividir una cadena grande en fragmentos. Los resultados siguientes de la prueba comparativa muestran que el enfoque StringTokenizer
es casi tres veces más rápido, pero los resultados pueden variar. Al igual que con todas las consideraciones de rendimiento, debe evaluar su caso de uso específico.
Método | Media | Error | StdDev | Relación |
---|---|---|---|---|
Tokenizador | 3,315 ms | 0,0659 ms | 0,0705 ms | 0,32 |
Dividir | 10,257 ms | 0,2018 ms | 0,2552 ms | 1.00 |
Leyenda
- Media: media aritmética de todas las medidas.
- Error: mitad del intervalo de confianza del 99,9 %.
- Desviación estándar: desviación estándar de todas las medidas.
- Mediana: valor que separa la mitad superior de todas las medidas (percentil 50).
- Proporción: media de la distribución de proporción (actual/línea de base).
- Desviación estándar dela proporción: desviación estándar de la distribución de la proporción (actual/línea de base).
- 1 ms: 1 milisegundo (0,001 segundos)
Para más información sobre las pruebas comparativas con .NET, consulte BenchmarkDotNet.
El tipo StringValues
El objeto StringValues es un tipo struct
que representa null
, cero, uno o muchas cadenas de una manera eficaz. El tipo StringValues
se puede construir con cualquiera de las sintaxis siguientes: string?
o string?[]?
. Con el texto del ejemplo anterior, tenga en cuenta el siguiente código de 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
El código anterior crea instancias de un objeto StringValues
dada una matriz de valores de cadena. StringValues.Count se escribe en la consola.
El tipo StringValues
es una implementación de los siguientes tipos de colección:
IList<string>
ICollection<string>
IEnumerable<string>
IEnumerable
IReadOnlyList<string>
IReadOnlyCollection<string>
Por lo tanto, se puede iterar y se puede interactuar con cada elemento value
según sea necesario.