Partilhar via


Primitivos: A biblioteca de extensões para .NET

Neste artigo, você aprenderá sobre a biblioteca Microsoft.Extensions.Primitives . As primitivas neste artigo não devem ser confundidas com os tipos primitivos .NET da BCL ou da linguagem C#. Em vez disso, os tipos dentro da biblioteca do primitivo servem como blocos de construção para alguns dos pacotes NuGet do .NET periférico, 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 na maioria das vezes pode mudar. Quando a mudança ocorre, as Microsoft.Extensions.Primitives.IChangeToken implementações da interface podem ser usadas para notificar as partes interessadas da referida alteração. As implementações disponíveis são as seguintes:

Como desenvolvedor, você também é livre para implementar seu próprio tipo. A IChangeToken interface 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, a CancellationTokenSource é instanciado e seu Token é passado para o CancellationChangeToken construtor. O estado inicial de HasChanged é gravado no console. É criado um Action<object?> callback que grava quando o retorno de chamada é invocado para o console. O método do RegisterChangeCallback(Action<Object>, Object) token é chamado, dado o callback. Dentro do using comunicado, o cancellationTokenSource é cancelado. Isso aciona o retorno de chamada e o estado de HasChanged é gravado novamente no console.

Quando precisar agir a partir de várias fontes de alteração, use o CompositeChangeToken. Essa implementação agrega um ou mais tokens de alteração e dispara cada retorno de chamada registrado exatamente uma vez, independentemente do número de vezes que uma alteração é acionada. 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 CancellationTokenSource instâncias de objetos são criadas e emparelhadas com instâncias correspondentes CancellationChangeToken . O token composto é instanciado passando uma matriz dos tokens para o CompositeChangeToken construtor. O Action<object?> callback é criado, mas desta vez o state objeto é usado e gravado no console como uma mensagem formatada. O retorno de chamada é registrado quatro vezes, cada uma 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 Cancel() método. Isso aciona a alteração, invocando cada retorno de chamada registrado exatamente uma vez.

Abordagem alternativa static

Como alternativa à chamada RegisterChangeCallback, você pode usar a Microsoft.Extensions.Primitives.ChangeToken classe estática. 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 os exemplos anteriores, você precisará de IChangeToken uma implementação que seja produzida pelo changeTokenProducer. O produtor é definido como um Func<IChangeToken> e espera-se que isso retorne um novo token a cada invocação. O consumer é um Action quando não está usando state, ou um Action<TState> onde o tipo TState genérico flui através da notificação de alteração.

Tokenizadores de cadeia de caracteres, segmentos e valores

A interação 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 um string em StringSegment instâncias.
  • StringValues: Representa null, zero, uma ou muitas cadeias de caracteres de forma eficiente.

O StringSegment tipo

Nesta seção, você aprenderá sobre uma representação otimizada de uma substring conhecida como tipo StringSegment struct . Considere o seguinte exemplo de código C# mostrando algumas das StringSegment propriedades e o AsSpan método:

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 instancia o StringSegment dado um string valor, um offsete um length. O StringSegment.Buffer é o argumento de cadeia de caracteres original e o StringSegment.Value é a subcadeia de caracteres baseada nos StringSegment.Offset valores e StringSegment.Length .

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

O StringTokenizer tipo

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

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

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

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

Benchmark comparando StringTokenizer com string.Split

Com as várias maneiras de fatiar e cortar cordas, parece apropriado comparar dois métodos com um benchmark. Usando o pacote BenchmarkDotNet NuGet, considere os dois métodos de benchmark a seguir:

  1. Utilização de StringTokenizer:

    StringBuilder buffer = new();
    
    var tokenizer =
        new StringTokenizer(
            s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
            new[] { ' ', '.' });
    
    foreach (StringSegment segment in tokenizer)
    {
        buffer.Append(segment.Value);
    }
    
  2. Utilização de 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 de referência abaixo mostram que a abordagem é quase três vezes mais rápida, mas os StringTokenizer resultados podem variar. Como em todas as considerações de desempenho, você deve avaliar seu caso de uso específico.

Método Média Erro StdDev Proporção
Tokenizador 3,315 ms 0,0659 ms 0,0705 ms 0.32
Dividida 10.257 ms 0,2018 ms 0,2552 ms 1,00

Legenda

  • Média: Média aritmética de todas as medições
  • Erro: Metade do intervalo de confiança de 99,9%
  • Desvio-padrão: desvio-padrão de todas as medições
  • Mediana: Valor que separa a metade superior de todas as medições (percentil 50)
  • Rácio: Média da distribuição do rácio (Corrente/Base de referência)
  • Desvio-padrão da razão: desvio-padrão da distribuição da razão (corrente/linha de base)
  • 1 ms: 1 milissegundo (0,001 seg.)

Para obter mais informações sobre benchmarking com .NET, consulte BenchmarkDotNet.

O StringValues tipo

O StringValues objeto é um struct tipo que representa null, zero, uma ou muitas cadeias de caracteres de forma eficiente. O StringValues tipo pode ser construído com 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 instancia um StringValues objeto dado uma matriz de valores de cadeia de caracteres. O StringValues.Count é gravado no console.

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

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

Como tal, ele pode ser iterado e cada value um pode ser interagido conforme necessário.

Consulte também