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:
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 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:
- IChangeToken.HasChanged: obtém um valor que indica se uma alteração ocorreu.
- IChangeToken.ActiveChangeCallbacks: indica se o token gerará proativamente retornos de chamada. Se
false
, o consumidor do token deverá sondarHasChanged
para detectar 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, 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 deStringSegment
. - 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:
Usando StringTokenizer:
StringBuilder buffer = new(); var tokenizer = new StringTokenizer( s_nineHundredAutoGeneratedParagraphsOfLoremIpsum, new[] { ' ', '.' }); foreach (StringSegment segment in tokenizer) { buffer.Append(segment.Value); }
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.