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:
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
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:
- IChangeToken.HasChanged: Obtém um valor que indica se ocorreu uma alteração.
- IChangeToken.ActiveChangeCallbacks: Indica se o token aumentará proativamente os retornos de chamada. Se
false
, o consumidor de token deve pesquisarHasChanged
para detetar alterações.
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
emStringSegment
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 offset
e 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 StringSegment
arquivo . O código itera os segmentos, permitindo que o consumidor interaja com cada segment
um.
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:
Utilização de StringTokenizer:
StringBuilder buffer = new(); var tokenizer = new StringTokenizer( s_nineHundredAutoGeneratedParagraphsOfLoremIpsum, new[] { ' ', '.' }); foreach (StringSegment segment in tokenizer) { buffer.Append(segment.Value); }
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.