Compartir a través de


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:

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:

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 de StringSegment.
  • 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:

  1. Con StringTokenizer :

    StringBuilder buffer = new();
    
    var tokenizer =
        new StringTokenizer(
            s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
            new[] { ' ', '.' });
    
    foreach (StringSegment segment in tokenizer)
    {
        buffer.Append(segment.Value);
    }
    
  2. 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.

Vea también