Primitivos: a biblioteca de extensões do .NET

Neste artigo, você aprenderá sobre a biblioteca Microsoft.Extensions.Primitives. Os primitivos neste artigo não devem ser confundidos com os tipos primitivos do .NET da BCL ou com os da linguagem C#. Em vez disso, os tipos da biblioteca de primitivos servem como blocos de construção para alguns dos pacotes NuGet periféricos do .NET, como:

Notificações de alteração

Propagar notificações quando ocorre uma alteração é um conceito fundamental na programação. O estado observado de um objeto frequentemente pode ser alterado. Quando a alteração ocorre, as implementações da interface Microsoft.Extensions.Primitives.IChangeToken podem ser usadas para notificar as partes interessadas sobre essa alteração. As implementações disponíveis são as seguintes:

Como desenvolvedor, você também é livre para implementar seu próprio tipo. A interface IChangeToken define algumas propriedades:

Funcionalidade baseada em instância

Considere o seguinte exemplo de uso do 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

No exemplo anterior, um CancellationTokenSource é instanciado e seu Token é passado para o construtor CancellationChangeToken. O estado inicial de HasChanged é gravado no console. Um Action<object?> callback é criado e grava quando o retorno de chamada é invocado para o console. O método RegisterChangeCallback(Action<Object>, Object) do token é chamado, considerando o callback. Na instrução using, o cancellationTokenSource é cancelado. Isso dispara o retorno de chamada e o estado de HasChanged gravado novamente no console.

Quando você precisar tomar medidas de várias fontes de alteração, use o CompositeChangeToken. Essa implementação agrega um ou mais tokens de alteração e aciona cada retorno de chamada registrado exatamente uma vez, independentemente do número de vezes que uma alteração é disparada. Considere o seguinte exemplo:

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.

No código C# anterior, três instâncias do objeto CancellationTokenSource são criadas e emparelhadas com as instâncias CancellationChangeToken correspondentes. O token composto é instanciado passando uma matriz dos tokens para o construtor CompositeChangeToken. O Action<object?> callback é criado; mas, desta vez, o objeto state é usado e gravado no console como uma mensagem formatada. O retorno de chamada é registrado quatro vezes, cada uma delas com um argumento de objeto de estado ligeiramente diferente. O código usa um gerador de números pseudoaleatórios para escolher uma das fontes de token de alteração (não importa qual) e chamar seu método Cancel(). Isso dispara a alteração, invocando cada retorno de chamada registrado exatamente uma vez.

Abordagem static alternativa

Como alternativa para a chamada de RegisterChangeCallback, você pode usar a classe estática Microsoft.Extensions.Primitives.ChangeToken. Considere o seguinte padrão 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.

Assim como nos exemplos anteriores, você precisará de uma implementação de IChangeToken que seja produzida pelo changeTokenProducer. O produtor é definido como um Func<IChangeToken> e espera-se que retorne um novo token a cada invocação. O consumer é um Action, quando não está usando state, ou um Action<TState>, em que o tipo genérico TState flui por meio da notificação de alteração.

Tokenizadores de cadeia de caracteres, segmentos e valores

Interagir com cadeias de caracteres é comum no desenvolvimento de aplicativos. Várias representações de cadeias de caracteres são analisadas, divididas ou iteradas. A biblioteca de primitivos oferece alguns tipos de escolha que ajudam a tornar a interação com cadeias de caracteres mais otimizada e eficiente. Considere os seguintes tipos:

  • StringSegment: uma representação otimizada de uma substring.
  • StringTokenizer: tokeniza string em instâncias de StringSegment.
  • StringValues: representa null, zero, uma ou várias cadeias de caracteres de uma maneira eficiente.

O tipo StringSegment

Nesta seção, você aprenderá sobre uma representação otimizada de uma substring conhecida como tipo StringSegmentstruct. Considere o exemplo de código C# a seguir mostrando algumas das propriedades StringSegment e o 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 "

O código anterior cria uma instância do StringSegment determinado um valor string, um offset e um length. O StringSegment.Buffer é o argumento de cadeia de caracteres original e StringSegment.Value é a substring com base nos valores StringSegment.Offset e StringSegment.Length.

O struct StringSegment fornece muitos métodos para interagir com o segmento.

O tipo StringTokenizer.

O objeto StringTokenizer é um tipo de struct que tokeniza um string em instâncias de StringSegment. A tokenização de cadeias de caracteres grandes geralmente envolve dividir a cadeia de caracteres e iterar sobre ela. Dito isso, provavelmente String.Split vem à mente. Essas APIs são semelhantes. Mas, em geral, StringTokenizer fornece melhor desempenho. Primeiramente, considere o exemplo a seguir:

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

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

No código anterior, uma instância do tipo StringTokenizer é criada com 900 parágrafos do texto Lorem Ipsum gerados automaticamente e uma matriz com um único valor de um caractere ' ' de espaço em branco. Cada valor do tokenizador é representado como um StringSegment. O código itera os segmentos, permitindo que o consumidor interaja com cada segment.

Parâmetro de comparação de StringTokenizer com string.Split

Com as várias maneiras de dividir e definir cadeias de caracteres, é apropriado comparar dois métodos com um parâmetro de comparação. Usando o pacote NuGet BenchmarkDotNet, considere os dois métodos de parâmetro de comparação a seguir:

  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);
    }
    

Ambos os métodos são semelhantes na área de superfície da API e ambos são capazes de dividir uma cadeia de caracteres grande em partes. Os resultados do parâmetro de comparação abaixo mostram que a abordagem StringTokenizer é quase três vezes mais rápida. Mas, os resultados podem variar. Assim como acontece com todas as considerações de desempenho, você deve avaliar seu caso de uso específico.

Método Média Erro StdDev Proporção
Gerador de token 3,315 ms 0,0659 ms 0,0705 ms 0,32
Divisão 10,257 ms 0,2018 ms 0,2552 ms 1.00

Legenda

  • Média: média aritmética de todas as medidas
  • Erro: metade do intervalo de confiança de 99,9%
  • Desvio padrão: desvio padrão de todas as medidas
  • Mediana: valor que separa a metade superior de todas as medidas (50º percentil)
  • Proporção: média da distribuição da proporção (atual/linha de base)
  • Desvio padrão da proporção: desvio padrão da distribuição da proporção (atual/linha de base)
  • 1 ms: 1 milissegundo (0,001 s)

Para obter mais informações sobre parâmetro de comparação com o .NET, confira BenchmarkDotNet.

O tipo StringValues.

O objeto StringValues é um tipo struct que representa null, zero, uma ou várias cadeias de caracteres de forma eficiente. O tipo StringValues pode ser construído com qualquer uma das seguintes sintaxes: string? ou string?[]?. Usando o texto do exemplo anterior, considere o seguinte código 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

O código anterior cria uma instância de um objeto StringValues com uma matriz de valores de cadeia de caracteres. O StringValues.Count é gravado no console.

O tipo StringValues é uma implementação dos seguintes tipos de coleção:

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

Dessa forma, ele pode ser iterado e cada value pode interagir conforme necessário.

Confira também