Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Este artigo descreve novos recursos nas bibliotecas do .NET para .NET 9.
Base64Url
Base64 é um esquema de codificação que converte bytes arbitrários em texto composto por um conjunto específico de 64 caracteres. É uma abordagem comum para transferir dados e há muito tempo tem suporte por meio de uma variedade de métodos, como com Convert.ToBase64String ou Base64.DecodeFromUtf8(ReadOnlySpan<Byte>, Span<Byte>, Int32, Int32, Boolean). No entanto, alguns dos caracteres que ele utiliza tornam seu uso menos ideal em determinadas circunstâncias em que você gostaria de empregá-lo, como em strings de consulta. Em particular, os 64 caracteres que compõem a tabela Base64 incluem '+' e '/', ambos com seu próprio significado em URLs. Isso levou à criação do esquema Base64Url, que é semelhante ao Base64, mas usa um conjunto ligeiramente diferente de caracteres que o torna apropriado para uso em contextos de URLs. O .NET 9 inclui a nova classe Base64Url, que fornece muitos métodos úteis e otimizados para codificação e decodificação com Base64Url para e de uma variedade de tipos de dados.
O exemplo a seguir demonstra o uso da nova classe.
ReadOnlySpan<byte> bytes = ...;
string encoded = Base64Url.EncodeToString(bytes);
BinaryFormatter
O .NET 9 remove BinaryFormatter do runtime do .NET. As APIs ainda estão presentes, mas suas implementações sempre geram uma exceção, independentemente do tipo de projeto. Para obter mais informações sobre a remoção e suas opções se você for afetado, consulte o guia de migração do BinaryFormatter.
Coleções
Os tipos de coleção no .NET obtêm as seguintes atualizações para o .NET 9:
- Pesquisas de coleção com spans
OrderedDictionary<TKey, TValue>- O método PriorityQueue.Remove() permite atualizar a prioridade de um item na fila.
ReadOnlySet<T>
Pesquisas de coleção com spans
No código de alto desempenho, os intervalos geralmente são usados para evitar a alocação de cadeias de caracteres desnecessariamente e tabelas de pesquisa com tipos como Dictionary<TKey,TValue> e HashSet<T> são frequentemente usados como caches. No entanto, não houve nenhum mecanismo interno seguro para realizar pesquisas nesses tipos de coleção com spans. Com o recurso allows ref struct no C# 13 e as novas funcionalidades nesses tipos de coleção no .NET 9, agora é possível realizar esses tipos de consultas.
O exemplo a seguir demonstra o uso de Dictionary<TKey,TValue>.GetAlternateLookup.
static Dictionary<string, int> CountWords(ReadOnlySpan<char> input)
{
Dictionary<string, int> wordCounts = new(StringComparer.OrdinalIgnoreCase);
Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>> spanLookup =
wordCounts.GetAlternateLookup<ReadOnlySpan<char>>();
foreach (Range wordRange in Regex.EnumerateSplits(input, @"\b\W+"))
{
if (wordRange.Start.Value == wordRange.End.Value)
{
continue; // Skip empty ranges.
}
ReadOnlySpan<char> word = input[wordRange];
spanLookup[word] = spanLookup.TryGetValue(word, out int count) ? count + 1 : 1;
}
return wordCounts;
}
OrderedDictionary<TKey, TValue>
Em muitos cenários, talvez você queira armazenar pares chave-valor de uma maneira em que a ordem possa ser mantida (uma lista de pares chave-valor), mas onde também há suporte para pesquisa rápida por chave (um dicionário de pares chave-valor). Desde os primeiros dias do .NET, o OrderedDictionary tipo tem suportado esse cenário, mas apenas de maneira não genérica, com chaves e valores tipados como object. O .NET 9 apresenta a coleção solicitada OrderedDictionary<TKey,TValue> há muito tempo, que fornece um tipo genérico eficiente para dar suporte a esses cenários.
O código a seguir usa a nova classe.
OrderedDictionary<string, int> d = new()
{
["a"] = 1,
["b"] = 2,
["c"] = 3,
};
d.Add("d", 4);
d.RemoveAt(0);
d.RemoveAt(2);
d.Insert(0, "e", 5);
foreach (KeyValuePair<string, int> entry in d)
{
Console.WriteLine(entry);
}
// Output:
// [e, 5]
// [b, 2]
// [c, 3]
Método PriorityQueue.Remove()
O .NET 6 introduziu a coleção PriorityQueue<TElement,TPriority>, que fornece uma implementação simples e rápida de heap de matriz. Um problema com heaps de matriz em geral é que eles não dão suporte a atualizações prioritárias, o que os torna proibitivos para uso em algoritmos, como variações do algoritmo de Dijkstra.
Embora não seja possível implementar atualizações eficientes de prioridade $O(\log n)$ na coleção existente, o método PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) novo possibilita emular atualizações de prioridade (embora em tempo $O(n)$):
public static void UpdatePriority<TElement, TPriority>(
this PriorityQueue<TElement, TPriority> queue,
TElement element,
TPriority priority
)
{
// Scan the heap for entries matching the current element.
queue.Remove(element, out _, out _);
// Re-insert the entry with the new priority.
queue.Enqueue(element, priority);
}
Esse método desbloqueia usuários que desejam implementar algoritmos de grafo em contextos em que o desempenho assíncrono não é um bloqueador. (Esses contextos incluem educação e prototipagem.) Por exemplo, aqui está uma implementação de toy do algoritmo de Dijkstra que usa a nova API.
ReadOnlySet<T>
Muitas vezes é desejável fornecer visualizações somente leitura de coleções. ReadOnlyCollection<T> permite criar um "wrapper" de somente leitura em torno de um IList<T> mutável arbitrário, e ReadOnlyDictionary<TKey,TValue> permite criar um "wrapper" de somente leitura em torno de um IDictionary<TKey,TValue> mutável arbitrário. No entanto, versões anteriores do .NET não tinham suporte interno para fazer o mesmo com ISet<T>. O .NET 9 apresenta ReadOnlySet<T> para resolver isso.
A nova classe habilita o seguinte padrão de uso.
private readonly HashSet<int> _set = [];
private ReadOnlySet<int>? _setWrapper;
public ReadOnlySet<int> Set => _setWrapper ??= new(_set);
Modelo de componente – Suporte à redução de TypeDescriptor
System.ComponentModel inclui novas APIs compatíveis com a ferramenta de redução, que podem ser ativadas, para descrever componentes. Qualquer aplicativo, especialmente aplicativos autossuficientes com redução, pode usar essas novas APIs para ajudar a dar suporte a cenários de redução.
A API primária é o TypeDescriptor.RegisterType método na TypeDescriptor classe. Esse método possui o atributo DynamicallyAccessedMembersAttribute para que a ferramenta de redução preserve os membros desse tipo. Você deve chamar esse método uma vez por tipo e, geralmente, logo no início.
As APIs secundárias têm um FromRegisteredType sufixo, como TypeDescriptor.GetPropertiesFromRegisteredType(Type). Ao contrário de seus equivalentes que não possuem o sufixo FromRegisteredType, essas APIs não possuem os atributos trimmer [RequiresUnreferencedCode] ou [DynamicallyAccessedMembers]. A falta de atributos da ferramenta de redução ajuda os consumidores, pois eles não precisam mais:
- Suprimir avisos da ferramenta de redução, o que pode ser arriscado.
- Propagar um parâmetro de
Typefortemente tipado para outros métodos, o que pode ser trabalhoso ou inviável.
public static void RunIt()
{
// The Type from typeof() is passed to a different method.
// The trimmer doesn't know about ExampleClass anymore
// and thus there will be warnings when trimming.
Test(typeof(ExampleClass));
Console.ReadLine();
}
private static void Test(Type type)
{
// When publishing self-contained + trimmed,
// this line produces warnings IL2026 and IL2067.
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(type);
// When publishing self-contained + trimmed,
// the property count is 0 here instead of 2.
Console.WriteLine($"Property count: {properties.Count}");
// To avoid the warning and ensure reflection
// can see the properties, register the type:
TypeDescriptor.RegisterType<ExampleClass>();
// Get properties from the registered type.
properties = TypeDescriptor.GetPropertiesFromRegisteredType(type);
Console.WriteLine($"Property count: {properties.Count}");
}
public class ExampleClass
{
public string? Property1 { get; set; }
public int Property2 { get; set; }
}
Para obter mais informações, consulte a proposta da API .
Criptografia
- Método CryptographicOperations.HashData()
- Algoritmo KMAC
- algoritmosAES-GCM e ChaChaPoly1305 habilitados para iOS/tvOS/MacCatalyst
- Carregamento de certificado X.509
- Suporte a provedores OpenSSL
- Segurança baseada em virtualização do Windows CNG
Método CryptographicOperations.HashData()
O .NET inclui várias implementações estáticas "de uso único" de funções de hash e funções relacionadas. Essas APIs incluem SHA256.HashData e HMACSHA256.HashData. As APIs de uso único são preferíveis para utilização, pois podem fornecer o melhor desempenho possível e reduzir ou eliminar alocações.
Se um desenvolvedor quiser fornecer uma API que dê suporte ao hash em que o chamador define qual algoritmo de hash usar, normalmente é feito aceitando um HashAlgorithmName argumento. No entanto, usar esse padrão com APIs de uso único exigiria alternar todos os possíveis HashAlgorithmName e, em seguida, usar o método apropriado. Para resolver esse problema, o .NET 9 apresenta a CryptographicOperations.HashData API. Essa API permite que você produza um hash ou HMAC de uma entrada de uma só vez, em que o algoritmo utilizado é determinado por um HashAlgorithmName.
static void HashAndProcessData(HashAlgorithmName hashAlgorithmName, byte[] data)
{
byte[] hash = CryptographicOperations.HashData(hashAlgorithmName, data);
ProcessHash(hash);
}
Algoritmo KMAC
O .NET 9 fornece o algoritmo KMAC, conforme especificado pelo NIST SP-800-185. O KMAC (Código de Autenticação de Mensagem KECCAK) é uma função de pseudorandom e uma função de hash com chave baseada em KECCAK.
As novas classes a seguir usam o algoritmo KMAC. Use instâncias para acumular dados a fim de produzir um MAC ou use o método estático HashData para uso único em apenas uma entrada.
O KMAC está disponível no Linux com OpenSSL 3.0 ou posterior e no Windows 11 Build 26016 ou posterior. Você pode usar a propriedade estática IsSupported para determinar se a plataforma dá suporte ao algoritmo desejado.
if (Kmac128.IsSupported)
{
byte[] key = GetKmacKey();
byte[] input = GetInputToMac();
byte[] mac = Kmac128.HashData(key, input, outputLength: 32);
}
else
{
// Handle scenario where KMAC isn't available.
}
algoritmos AES-GCM e ChaChaPoly1305 habilitados para iOS/tvOS/MacCatalyst
IsSupported e ChaChaPoly1305.IsSupported agora retornam "true" ao executar no iOS 13+, tvOS 13+ e Mac Catalyst.
AesGcm só dá suporte a valores de marca de 16 bytes (128 bits) em sistemas operacionais Apple.
Carregamento de certificado X.509
Desde o .NET Framework 2.0, a maneira de carregar um certificado tem sido new X509Certificate2(bytes). Também houve outros padrões, como new X509Certificate2(bytes, password, flags), new X509Certificate2(path), new X509Certificate2(path, password, flags) e X509Certificate2Collection.Import(bytes, password, flags) (e suas sobrecargas).
Esses métodos usavam análise de conteúdo para identificar se a entrada era algo que podiam processar e, em caso afirmativo, faziam o carregamento. Para quem ligou, essa estratégia foi muito conveniente. Mas também tem alguns problemas:
- Nem todo formato de arquivo funciona em todos os sistemas operacionais.
- É um desvio de protocolo.
- É uma fonte de problemas de segurança.
O .NET 9 apresenta uma nova X509CertificateLoader classe, que tem um design "um método, uma finalidade". Em sua versão inicial, ele suporta apenas dois dos cinco formatos que o construtor X509Certificate2 suportava. Esses são os dois formatos que funcionaram em todos os sistemas de operação.
Suporte a provedores OpenSSL
O .NET 8 introduziu as APIs específicas do OpenSSL OpenPrivateKeyFromEngine(String, String) e OpenPublicKeyFromEngine(String, String). Eles habilitam a interação com componentes OpenSSL ENGINE e usam HSM (módulos de segurança de hardware), por exemplo.
O .NET 9 apresenta SafeEvpPKeyHandle.OpenKeyFromProvider(String, String), que permite usar provedores OpenSSL e interagir com provedores como tpm2 ou pkcs11.
Algumas distribuições removeram o suporte a ENGINE, pois ele agora está obsoleto.
O snippet a seguir mostra o uso básico:
byte[] data = [ /* example data */ ];
// Refer to your provider documentation, for example, https://github.com/tpm2-software/tpm2-openssl/tree/master.
using (SafeEvpPKeyHandle priKeyHandle = SafeEvpPKeyHandle.OpenKeyFromProvider("tpm2", "handle:0x81000007"))
using (ECDsa ecdsaPri = new ECDsaOpenSsl(priKeyHandle))
{
byte[] signature = ecdsaPri.SignData(data, HashAlgorithmName.SHA256);
// Do stuff with signature created by TPM.
}
Existem algumas melhorias de desempenho durante o handshake do TLS, bem como melhorias nas interações com chaves privadas RSA que utilizam componentes ENGINE.
Segurança baseada em virtualização do Windows CNG
O Windows 11 adicionou novas APIs para ajudar a proteger as chaves do Windows com VBS (segurança baseada em virtualização). Com essa nova funcionalidade, as chaves podem ser protegidas contra ataques de roubo de chave no nível do administrador com efeito insignificante sobre desempenho, confiabilidade ou escala.
O .NET 9 adicionou sinalizadores correspondentes CngKeyCreationOptions . Os três sinalizadores a seguir foram adicionados:
-
CngKeyCreationOptions.PreferVbscorrespondente aNCRYPT_PREFER_VBS_FLAG -
CngKeyCreationOptions.RequireVbscorrespondente aNCRYPT_REQUIRE_VBS_FLAG -
CngKeyCreationOptions.UsePerBootKeycorrespondente aNCRYPT_USE_PER_BOOT_KEY_FLAG
O snippet a seguir demonstra como usar um dos sinalizadores:
using System.Security.Cryptography;
CngKeyCreationParameters cngCreationParams = new()
{
Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider,
KeyCreationOptions = CngKeyCreationOptions.RequireVbs | CngKeyCreationOptions.OverwriteExistingKey,
};
using (CngKey key = CngKey.Create(CngAlgorithm.ECDsaP256, "myKey", cngCreationParams))
using (ECDsaCng ecdsa = new ECDsaCng(key))
{
// Do stuff with the key.
}
Data e hora – novas sobrecargas de TimeSpan.From*
A TimeSpan classe oferece vários From* métodos que permitem criar um TimeSpan objeto usando um double. No entanto, como double é um formato de ponto flutuante baseado em binário, a imprecisão inerente pode levar a erros. Por exemplo, TimeSpan.FromSeconds(101.832) talvez não represente com precisão 101 seconds, 832 milliseconds, mas sim aproximadamente 101 seconds, 831.9999999999936335370875895023345947265625 milliseconds. Essa discrepância causou confusão frequente e também não é a maneira mais eficiente de representar esses dados. Para resolver isso, o .NET 9 adiciona novas sobrecargas que permitem criar TimeSpan objetos de inteiros. Há novas sobrecargas de FromDays, , FromHours, FromMinutes, FromSeconds, FromMillisecondse FromMicroseconds.
O código a seguir mostra um exemplo de chamada de double e de uma das novas sobrecargas com inteiros.
TimeSpan timeSpan1 = TimeSpan.FromSeconds(value: 101.832);
Console.WriteLine($"timeSpan1 = {timeSpan1}");
// timeSpan1 = 00:01:41.8319999
TimeSpan timeSpan2 = TimeSpan.FromSeconds(seconds: 101, milliseconds: 832);
Console.WriteLine($"timeSpan2 = {timeSpan2}");
// timeSpan2 = 00:01:41.8320000
Injeção de dependência – construtor ActivatorUtilities.CreateInstance
A resolução do construtor para ActivatorUtilities.CreateInstance foi alterada no .NET 9. Anteriormente, um construtor que foi explicitamente marcado usando o ActivatorUtilitiesConstructorAttribute atributo pode não ser chamado, dependendo da ordenação de construtores e do número de parâmetros de construtor. A lógica foi alterada no .NET 9 de modo que um construtor que tem o atributo seja sempre chamado.
Diagnósticos
- Debug.Assert relata condição de asserção por padrão
- Novo método Activity.AddLink
- Instrumento Metrics.Gauge
- Monitoramento de curinga do medidor fora do processo
Debug.Assert relata condição de asserção por padrão
Debug.Assert geralmente é usado para ajudar a validar condições que devem ser sempre verdadeiras. A falha normalmente indica um bug no código. Há muitas sobrecargas de Debug.Assert, sendo que a mais simples aceita apenas uma condição.
Debug.Assert(a > 0 && b > 0);
A asserção falha se a condição for falsa. Historicamente, no entanto, tais afirmações careciam de qualquer informação sobre qual condição falhou. A partir do .NET 9, se nenhuma mensagem for fornecida explicitamente pelo usuário, a declaração conterá a representação textual da condição. Por exemplo, no caso da asserção anterior, em vez de receber uma mensagem como:
Process terminated. Assertion failed.
at Program.SomeMethod(Int32 a, Int32 b)
A mensagem agora seria:
Process terminated. Assertion failed.
a > 0 && b > 0
at Program.SomeMethod(Int32 a, Int32 b)
Novo método Activity.AddLink
Anteriormente, você só podia vincular uma Activity de rastreamento a outros contextos de rastreamento ao criar a Activity. Nova no .NET 9, a AddLink(ActivityLink) API permite vincular um Activity objeto a outros contextos de rastreamento depois que ele é criado. Essa alteração também se alinha às especificações do OpenTelemetry .
ActivityContext activityContext = new(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None);
ActivityLink activityLink = new(activityContext);
Activity activity = new("LinkTest");
activity.AddLink(activityLink);
Instrumento Metrics.Gauge
System.Diagnostics.Metrics agora fornece o instrumento Gauge<T> de acordo com a especificação do OpenTelemetry. O Gauge instrumento foi projetado para registrar valores não aditivos quando ocorrem alterações. Por exemplo, ele pode medir o nível de ruído de plano de fundo, em que resumir os valores de várias salas seria sem sentido. O Gauge instrumento é um tipo genérico que pode registrar qualquer tipo de valor, como int, doubleou decimal.
O exemplo a seguir demonstra o uso do Gauge instrumento.
Meter soundMeter = new("MeasurementLibrary.Sound");
Gauge<int> gauge = soundMeter.CreateGauge<int>(
name: "NoiseLevel",
unit: "dB", // Decibels.
description: "Background Noise Level"
);
gauge.Record(10, new TagList() { { "Room1", "dB" } });
Monitoramento de curinga do medidor fora do processo
Já é possível monitorar medidores fora do processo usando o provedor de eventos System.Diagnostics.Metrics, mas, antes do .NET 9, era necessário especificar o nome completo do medidor. No .NET 9, você pode monitorar todos os contadores usando o caractere curinga *, que permite capturar métricas de cada contador em um processo. Além disso, ele adiciona suporte para escuta por prefixo de medidor, para que você possa ouvir todos os medidores cujos nomes começam com um prefixo especificado. Por exemplo, especificar MyMeter* permite ouvir todos os medidores com nomes que começam com MyMeter.
// The complete meter name is "MyCompany.MyMeter".
var meter = new Meter("MyCompany.MyMeter");
// Create a counter and allow publishing values.
meter.CreateObservableCounter("MyCounter", () => 1);
// Create the listener to use the wildcard character
// to listen to all meters using prefix names.
MyEventListener listener = new MyEventListener();
A MyEventListener classe é definida da seguinte maneira.
internal class MyEventListener : EventListener
{
protected override void OnEventSourceCreated(EventSource eventSource)
{
Console.WriteLine(eventSource.Name);
if (eventSource.Name == "System.Diagnostics.Metrics")
{
// Listen to all meters with names starting with "MyCompany".
// If using "*", allow listening to all meters.
EnableEvents(
eventSource,
EventLevel.Informational,
(EventKeywords)0x3,
new Dictionary<string, string?>() { { "Metrics", "MyCompany*" } }
);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
// Ignore other events.
if (eventData.EventSource.Name != "System.Diagnostics.Metrics" ||
eventData.EventName == "CollectionStart" ||
eventData.EventName == "CollectionStop" ||
eventData.EventName == "InstrumentPublished"
)
return;
Console.WriteLine(eventData.EventName);
if (eventData.Payload is not null)
{
for (int i = 0; i < eventData.Payload.Count; i++)
Console.WriteLine($"\t{eventData.PayloadNames![i]}: {eventData.Payload[i]}");
}
}
}
Quando você executa o código, a saída é a seguinte:
CounterRateValuePublished
sessionId: 7cd94a65-0d0d-460e-9141-016bf390d522
meterName: MyCompany.MyMeter
meterVersion:
instrumentName: MyCounter
unit:
tags:
rate: 0
value: 1
instrumentId: 1
CounterRateValuePublished
sessionId: 7cd94a65-0d0d-460e-9141-016bf390d522
meterName: MyCompany.MyMeter
meterVersion:
instrumentName: MyCounter
unit:
tags:
rate: 0
value: 1
instrumentId: 1
Você também pode usar o caractere curinga para monitorar métricas com ferramentas de monitoramento, como dotnet-counters.
LINQ
Novos métodos CountBy e AggregateBy foram introduzidos. Esses métodos possibilitam agregar o estado por chave sem a necessidade de alocar agrupamentos intermediários via GroupBy.
CountBy permite calcular rapidamente a frequência de cada chave. O exemplo a seguir localiza a palavra que ocorre com mais frequência em uma cadeia de caracteres de texto.
string sourceText = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Sed non risus. Suspendisse lectus tortor, dignissim sit amet,
adipiscing nec, ultricies sed, dolor. Cras elementum ultrices amet diam.
""";
// Find the most frequent word in the text.
KeyValuePair<string, int> mostFrequentWord = sourceText
.Split(new char[] { ' ', '.', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(word => word.ToLowerInvariant())
.CountBy(word => word)
.MaxBy(pair => pair.Value);
Console.WriteLine(mostFrequentWord.Key); // amet
AggregateBy permite implementar mais fluxos de trabalho de uso geral. O exemplo a seguir mostra como você pode calcular as pontuações associadas a uma determinada chave.
(string id, int score)[] data =
[
("0", 42),
("1", 5),
("2", 4),
("1", 10),
("0", 25),
];
var aggregatedData =
data.AggregateBy(
keySelector: entry => entry.id,
seed: 0,
(totalScore, curr) => totalScore + curr.score
);
foreach (var item in aggregatedData)
{
Console.WriteLine(item);
}
//(0, 67)
//(1, 15)
//(2, 4)
Index<TSource>(IEnumerable<TSource>) torna possível extrair rapidamente o índice implícito de um enumerável. Agora você pode escrever código, como o snippet a seguir, para indexar automaticamente itens em uma coleção.
IEnumerable<string> lines2 = File.ReadAllLines("output.txt");
foreach ((int index, string line) in lines2.Index())
{
Console.WriteLine($"Line number: {index + 1}, Line: {line}");
}
Gerador de fonte para registro em log
O C# 12 introduziu construtores primários, que permitem definir um construtor diretamente na declaração de classe. O gerador de fonte para registro em log agora oferece suporte ao registro em log usando classes que possuem um construtor primário.
public partial class ClassWithPrimaryConstructor(ILogger logger)
{
[LoggerMessage(0, LogLevel.Debug, "Test.")]
public partial void Test();
}
Miscelânea
Nesta seção, encontre informações sobre:
allows ref struct usado em bibliotecas
O C# 13 apresenta a capacidade de restringir um parâmetro genérico com allows ref struct, o que informa ao compilador e ao runtime que um ref struct pode ser usado para esse parâmetro genérico. Muitas APIs compatíveis com isso já foram anotadas. Por exemplo, o método String.Create tem uma sobrecarga que permite criar uma string ao escrever diretamente em sua memória, representada como um span. Esse método tem um argumento TState que é passado pelo chamador para o delegado que realiza a escrita propriamente dita.
Esse parâmetro do tipo TState em String.Create agora está anotado com allows ref struct:
public static string Create<TState>(int length, TState state, SpanAction<char, TState> action)
where TState : allows ref struct;
Esta anotação permite que você passe um span (ou qualquer outro ref struct) como entrada para esse método.
O exemplo a seguir mostra uma nova String.ToLowerInvariant() sobrecarga que usa essa funcionalidade.
public static string ToLowerInvariant(ReadOnlySpan<char> input) =>
string.Create(span.Length, input, static (stringBuffer, input) => span.ToLowerInvariant(stringBuffer));
Expansão de SearchValues
O .NET 8 introduziu o SearchValues<T> tipo, que fornece uma solução otimizada para pesquisar conjuntos específicos de caracteres ou bytes em intervalos. No .NET 9, SearchValues foi estendido para dar suporte à pesquisa de subcadeias de caracteres dentro de uma cadeia de caracteres maior.
O exemplo a seguir pesquisa vários nomes de animais dentro de um valor de cadeia de caracteres e retorna um índice para o primeiro encontrado.
private static readonly SearchValues<string> s_animals =
SearchValues.Create(["cat", "mouse", "dog", "dolphin"], StringComparison.OrdinalIgnoreCase);
public static int IndexOfAnimal(string text) =>
text.AsSpan().IndexOfAny(s_animals);
Essa nova funcionalidade tem uma implementação otimizada que aproveita o suporte ao SIMD na plataforma subjacente. Ele também permite que tipos de nível superior sejam otimizados. Por exemplo, Regex agora utiliza essa funcionalidade como parte de sua implementação.
Rede
- SocketsHttpHandler é padrão em HttpClientFactory
- System.Net.ServerSentEvents
- Retomada do TLS com certificados de cliente no Linux
- Ping keep alive e tempo limite de conexão ativa do WebSocket
- HttpClientFactory não registra mais valores de cabeçalho por padrão
SocketsHttpHandler é padrão em HttpClientFactory
HttpClientFactory cria HttpClient objetos apoiados por HttpClientHandler, por padrão.
HttpClientHandler é apoiado por SocketsHttpHandler, que é muito mais configurável, incluindo o gerenciamento do tempo de vida da conexão.
HttpClientFactory agora usa SocketsHttpHandler por padrão e o configura para definir limites em seus tempos de vida de conexão para corresponder ao tempo de vida de rotação especificado na fábrica.
System.Net.ServerSentEvents
O SSE (eventos enviados pelo servidor) é um protocolo simples e popular para transmitir dados de um servidor para um cliente. Ele é usado, por exemplo, pelo OpenAI como parte do streaming de texto gerado de seus serviços de IA. Para simplificar o consumo de SSE, a nova System.Net.ServerSentEvents biblioteca fornece um analisador para ingerir facilmente eventos enviados pelo servidor.
O código a seguir demonstra o uso da nova classe.
Stream responseStream = new MemoryStream();
await foreach (SseItem<string> e in SseParser.Create(responseStream).EnumerateAsync())
{
Console.WriteLine(e.Data);
}
Retomada do TLS com certificados de cliente no Linux
Retomada de TLS é um recurso do protocolo TLS que permite retomar sessões previamente estabelecidas com um servidor. Isso evita algumas viagens de ida e volta e salva recursos computacionais durante o handshake do TLS.
A Retomada de TLS já é compatível no Linux para conexões SslStream sem certificados de cliente. O .NET 9 adiciona suporte para retomar conexões TLS mutuamente autenticadas, que são comuns em cenários de servidor para servidor. O recurso é habilitado automaticamente.
Ping keep alive e tempo limite de conexão ativa do WebSocket
Novas APIs em ClientWebSocketOptions e WebSocketCreationOptions permitem que você opte por enviar pings WebSocket e cancele a conexão se o par não responder a tempo.
Até agora, você poderia especificar um KeepAliveInterval para evitar que a conexão ficasse ociosa, mas não havia nenhum mecanismo interno para garantir que o par estivesse respondendo.
O exemplo a seguir executa ping no servidor a cada 5 segundos e anula a conexão se ela não responder dentro de um segundo.
using var cws = new ClientWebSocket();
cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
cws.Options.KeepAliveInterval = TimeSpan.FromSeconds(5);
cws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(1);
await cws.ConnectAsync(uri, httpClient, cancellationToken);
HttpClientFactory não registra mais valores de cabeçalho por padrão
LogLevel.Trace os eventos registrados por HttpClientFactory não incluem mais valores de cabeçalho por padrão. Você pode escolher registrar os valores de determinados cabeçalhos por meio do RedactLoggedHeaders método auxiliar.
O exemplo a seguir redige todos os cabeçalhos, exceto o do agente de usuário.
services.AddHttpClient("myClient")
.RedactLoggedHeaders(name => name != "User-Agent");
Para obter mais informações, consulte O HttpClientFactory oculta os valores dos cabeçalhos por padrão nos logs.
Reflexão
Assemblies persistentes
Nas versões do .NET Core, assim como no .NET 5 a 8, o suporte para compilar um assembly e gerar metadados de reflexão para tipos criados dinamicamente se restringia a um componente executável AssemblyBuilder. A falta de suporte para salvar um assembly frequentemente foi um obstáculo para clientes que estavam migrando do .NET Framework para o .NET. O .NET 9 adiciona um novo tipo, PersistedAssemblyBuilder, que você pode usar para salvar um assembly emitido.
Para criar uma instância PersistedAssemblyBuilder, chame seu construtor e passe o nome do assembly, o assembly principal, System.Private.CoreLib, para fazer referência a tipos de runtime básicos e atributos personalizados opcionais. Depois de emitir todos os membros para o assembly, chame o método PersistedAssemblyBuilder.Save(String) para criar um assembly com configurações padrão. Se você quiser definir o ponto de entrada ou outras opções, poderá chamar PersistedAssemblyBuilder.GenerateMetadata e usar os metadados retornados para salvar o assembly. O código a seguir mostra um exemplo de como criar um assembly persistente e definir o ponto de entrada.
public void CreateAndSaveAssembly(string assemblyPath)
{
PersistedAssemblyBuilder ab = new PersistedAssemblyBuilder(
new AssemblyName("MyAssembly"),
typeof(object).Assembly
);
TypeBuilder tb = ab.DefineDynamicModule("MyModule")
.DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);
MethodBuilder entryPoint = tb.DefineMethod(
"Main",
MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static
);
ILGenerator il = entryPoint.GetILGenerator();
// ...
il.Emit(OpCodes.Ret);
tb.CreateType();
MetadataBuilder metadataBuilder = ab.GenerateMetadata(
out BlobBuilder ilStream,
out BlobBuilder fieldData
);
PEHeaderBuilder peHeaderBuilder = new PEHeaderBuilder(
imageCharacteristics: Characteristics.ExecutableImage);
ManagedPEBuilder peBuilder = new ManagedPEBuilder(
header: peHeaderBuilder,
metadataRootBuilder: new MetadataRootBuilder(metadataBuilder),
ilStream: ilStream,
mappedFieldData: fieldData,
entryPoint: MetadataTokens.MethodDefinitionHandle(entryPoint.MetadataToken)
);
BlobBuilder peBlob = new BlobBuilder();
peBuilder.Serialize(peBlob);
using var fileStream = new FileStream("MyAssembly.exe", FileMode.Create, FileAccess.Write);
peBlob.WriteContentTo(fileStream);
}
public static void UseAssembly(string assemblyPath)
{
Assembly assembly = Assembly.LoadFrom(assemblyPath);
Type? type = assembly.GetType("MyType");
MethodInfo? method = type?.GetMethod("SumMethod");
Console.WriteLine(method?.Invoke(null, [5, 10]));
}
A nova PersistedAssemblyBuilder classe inclui suporte a PDB. É possível emitir informações de símbolo e usá-las para depurar um assembly gerado. A API tem uma forma semelhante à implementação do .NET Framework. Para obter mais informações, consulte Emitir símbolos e gerar PDB.
Análise de nomes de tipo
TypeName é um analisador de nomes de tipo ECMA-335 que fornece funcionalidade semelhante à de System.Type, mas é dissociado do ambiente de tempo de execução. Componentes como serializadores e compiladores precisam analisar e processar nomes de tipo. Por exemplo, o compilador AOT nativo mudou para usar TypeName.
A nova TypeName classe fornece:
Métodos estáticos
ParseeTryParsepara analisar a entrada representada comoReadOnlySpan<char>. Ambos os métodos aceitam uma instância deTypeNameParseOptionsclasse (um recipiente de opções) que permite personalizar a análise.Name,FullNameeAssemblyQualifiedNamepropriedades que funcionam exatamente como seus equivalentes em System.Type.Várias propriedades e métodos que fornecem informações adicionais sobre o próprio nome:
-
IsArray,IsSZArray(SZsignifica dimensão única, matriz indexada zero)IsVariableBoundArrayTypeeGetArrayRankpara trabalhar com matrizes. -
IsConstructedGenericType,GetGenericTypeDefinitioneGetGenericArgumentspara trabalhar com nomes de tipo genéricos. -
IsByRefeIsPointerpara trabalhar com ponteiros e referências gerenciadas. -
GetElementType()para trabalhar com ponteiros, referências e matrizes. -
IsNestedeDeclaringTypepara trabalhar com tipos aninhados. -
AssemblyName, que expõe as informações de nome do assembly por meio da nova classe AssemblyNameInfo. Ao contrário deAssemblyName, o novo tipo é imutável, e a análise dos nomes de cultura não cria instâncias deCultureInfo.
-
Ambos os tipos TypeName e AssemblyNameInfo são imutáveis e não fornecem uma maneira de verificar a igualdade (eles não implementam IEquatable). Comparar nomes de assembly é simples, mas cenários diferentes precisam comparar apenas um subconjunto de informações expostas (Name, Version, CultureName e PublicKeyOrToken).
O snippet de código a seguir mostra alguns exemplos de uso.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
internal class RestrictedSerializationBinder
{
Dictionary<string, Type> AllowList { get; set; }
RestrictedSerializationBinder(Type[] allowedTypes)
=> AllowList = allowedTypes.ToDictionary(type => type.FullName!);
Type? GetType(ReadOnlySpan<char> untrustedInput)
{
if (!TypeName.TryParse(untrustedInput, out TypeName? parsed))
{
throw new InvalidOperationException($"Invalid type name: '{untrustedInput.ToString()}'");
}
if (AllowList.TryGetValue(parsed.FullName, out Type? type))
{
return type;
}
else if (parsed.IsSimple // It's not generic, pointer, reference, or an array.
&& parsed.AssemblyName is not null
&& parsed.AssemblyName.Name == "MyTrustedAssembly"
)
{
return Type.GetType(parsed.AssemblyQualifiedName, throwOnError: true);
}
throw new InvalidOperationException($"Not allowed: '{untrustedInput.ToString()}'");
}
}
As novas APIs estão disponíveis no System.Reflection.Metadata pacote NuGet, que pode ser usado com versões do .NET de nível inferior.
Expressões regulares
[GeneratedRegex] em propriedades
O .NET 7 introduziu o gerador de origem e o Regex atributo correspondente GeneratedRegexAttribute .
O seguinte método parcial será gerado automaticamente com todo o código necessário para implementar este Regex.
[GeneratedRegex(@"\b\w{5}\b")]
private static partial Regex FiveCharWord();
O C# 13 dá suporte a propriedades parciais além de métodos parciais, portanto, a partir do .NET 9, você também pode usar [GeneratedRegex(...)] em uma propriedade.
A propriedade parcial a seguir é a propriedade equivalente ao exemplo anterior.
[GeneratedRegex(@"\b\w{5}\b")]
private static partial Regex FiveCharWordProperty { get; }
Regex.EnumerateSplits
A Regex classe fornece um Split método, semelhante no conceito ao String.Split método. Com String.Split, você fornece um ou mais char ou string separadores, e a implementação divide o texto de entrada nesses separadores. Com Regex.Split, em vez de especificar o separador como um char ou string, ele é especificado como um padrão de expressão regular.
O exemplo a seguir demonstra Regex.Split.
foreach (string s in Regex.Split("Hello, world! How are you?", "[aeiou]"))
{
Console.WriteLine($"Split: \"{s}\"");
}
// Output, split by all English vowels:
// Split: "H"
// Split: "ll"
// Split: ", w"
// Split: "rld! H"
// Split: "w "
// Split: "r"
// Split: " y"
// Split: ""
// Split: "?"
No entanto, Regex.Split aceita apenas string como entrada e não dá suporte à entrada sendo fornecida como ReadOnlySpan<char>. Além disso, ele gera o conjunto completo de divisões como um string[], o que exige a alocação tanto do array de string para armazenar os resultados e um string para cada divisão. No .NET 9, o novo método EnumerateSplits permite executar a mesma operação, mas com uma entrada baseada em span e sem incorrer em alocações para os resultados. Ele aceita um ReadOnlySpan<char> e retorna um enumerável de Range objetos que representam os resultados.
O exemplo a seguir demonstra o uso de Regex.EnumerateSplits, recebendo um ReadOnlySpan<char> como entrada.
ReadOnlySpan<char> input = "Hello, world! How are you?";
foreach (Range r in Regex.EnumerateSplits(input, "[aeiou]"))
{
Console.WriteLine($"Split: \"{input[r]}\"");
}
Serialização (System.Text.Json)
- Opções de recuo
- Singleton de opções padrão da Web
- JsonSchemaExporter
- Respeitar anotações anuláveis
- Exigir parâmetros de construtor não opcionais
- Ordenar as propriedades do JsonObject
- Personalizar nomes de membros de enumeração
- Transmitir vários documentos JSON
Opções de recuo
JsonSerializerOptions inclui novas propriedades que permitem personalizar o caractere de recuo e o tamanho de recuo do JSON escrito.
var options = new JsonSerializerOptions
{
WriteIndented = true,
IndentCharacter = '\t',
IndentSize = 2,
};
string json = JsonSerializer.Serialize(
new { Value = 1 },
options
);
Console.WriteLine(json);
//{
// "Value": 1
//}
Singleton de opções padrão da Web
Se você quiser serializar com as opções padrão que o ASP.NET Core usa para aplicativos Web, use o novo singleton JsonSerializerOptions.Web.
string webJson = JsonSerializer.Serialize(
new { SomeValue = 42 },
JsonSerializerOptions.Web // Defaults to camelCase naming policy.
);
Console.WriteLine(webJson);
// {"someValue":42}
JsonSchemaExporter
O JSON é frequentemente usado para representar tipos em assinaturas de método como parte de esquemas de chamada de procedimento remoto. Ele é usado, por exemplo, como parte das especificações do OpenAPI ou como parte da chamada de ferramenta com serviços de IA como os do OpenAI. Os desenvolvedores podem serializar e desserializar tipos do .NET como JSON usando System.Text.Json. Mas eles também precisam ser capazes de obter um esquema JSON que descreva a forma do tipo .NET (ou seja, descreve a forma do que seria serializado e o que pode ser desserializado). System.Text.Json agora fornece o JsonSchemaExporter tipo, que dá suporte à geração de um esquema JSON que representa um tipo .NET.
Para obter mais informações, consulte o exportador de esquema JSON.
Respeitar anotações anuláveis
O System.Text.Json agora reconhece anotações de nulidade em propriedades e pode ser configurado para aplicá-las durante a serialização e desserialização usando o sinalizador RespectNullableAnnotations.
O código a seguir mostra como definir a opção:
public static void RunIt()
{
JsonSerializerOptions options = new() { RespectNullableAnnotations = true };
// Throws exception: System.Text.Json.JsonException: The property or field
// 'Title' on type 'Serialization+Book' doesn't allow getting null values.
// Consider updating its nullability annotation.
JsonSerializer.Serialize(new Book { Title = null! }, options);
// Throws exception: System.Text.Json.JsonException: The property or field
// 'Title' on type 'Serialization+Book' doesn't allow setting null values.
// Consider updating its nullability annotation.
JsonSerializer.Deserialize<Book>("""{ "Title" : null }""", options);
}
public class Book
{
public required string Title { get; set; }
public string? Author { get; set; }
public int PublishYear { get; set; }
}
Para obter mais informações, consulte Respeitar anotações anuláveis.
Exigir parâmetros de construtor não opcionais
Historicamente, o System.Text.Json tratava parâmetros de construtor não opcionais como opcionais ao usar a desserialização baseada em construtor. Você pode alterar esse comportamento usando o novo RespectRequiredConstructorParameters sinalizador.
O código a seguir mostra como definir a opção:
JsonSerializerOptions options = new() { RespectRequiredConstructorParameters = true };
// Throws exception: System.Text.Json.JsonException: JSON deserialization
// for type 'Serialization+MyPoco' was missing required properties including: 'Value'.
JsonSerializer.Deserialize<MyPoco>("""{}""", options);
Este tipo de MyPoco é definido da seguinte forma:
record MyPoco(string Value);
Para obter mais informações, consulte parâmetros de construtor não opcionais.
Ordenar propriedades do JsonObject
O JsonObject tipo agora expõe APIs semelhantes a dicionários ordenados que permitem a manipulação explícita da ordem das propriedades.
JsonObject jObj = new()
{
["key1"] = true,
["key3"] = 3
};
Console.WriteLine(jObj is IList<KeyValuePair<string, JsonNode?>>); // True.
// Insert a new key-value pair at the correct position.
int key3Pos = jObj.IndexOf("key3") is int i and >= 0 ? i : 0;
jObj.Insert(key3Pos, "key2", "two");
foreach (KeyValuePair<string, JsonNode?> item in jObj)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}
// Output:
// key1: true
// key2: two
// key3: 3
Para obter mais informações, consulte Manipular ordem de propriedade.
Personalizar nomes de membros de enumeração
O novo atributo System.Text.Json.Serialization.JsonStringEnumMemberNameAttribute pode ser usado para personalizar os nomes de membros de enumeração individuais para tipos serializados como cadeias de caracteres:
JsonSerializer.Serialize(MyEnum.Value1 | MyEnum.Value2); // "Value1, Custom enum value"
[Flags, JsonConverter(typeof(JsonStringEnumConverter))]
enum MyEnum
{
Value1 = 1,
[JsonStringEnumMemberName("Custom enum value")]
Value2 = 2,
}
Para obter mais informações, consulte Nomes de membros de enumeração personalizados.
Transmitir vários documentos JSON
System.Text.Json.Utf8JsonReader agora dá suporte à leitura de vários documentos JSON separados por espaço em branco de um único buffer ou fluxo. Por padrão, o leitor gerará uma exceção se detectar caracteres que não sejam de espaço em branco que estejam à direita do primeiro documento de nível superior. Você pode alterar esse comportamento usando o AllowMultipleValues sinalizador.
Para obter mais informações, consulte Ler vários documentos JSON.
Intervalos
No código de alto desempenho, os intervalos geralmente são usados para evitar alocar cadeias de caracteres desnecessariamente. Span<T> e ReadOnlySpan<T> continuam a revolucionar a maneira como o código é escrito no .NET, e a cada versão, mais e mais métodos são adicionados que operam em spans. O .NET 9 inclui as seguintes atualizações relacionadas ao span:
- Auxiliares de arquivo
- Sobrecargas de
params ReadOnlySpan<T> - Enumerar sobre segmentos de ReadOnlySpan<char>.Split()
Auxiliares de arquivo
A classe File agora tem novos auxiliares para gravar ReadOnlySpan<char>/ReadOnlySpan<byte> e ReadOnlyMemory<char>/ReadOnlyMemory<byte> diretamente em arquivos com facilidade.
O código a seguir grava com eficiência um ReadOnlySpan<char> em um arquivo.
ReadOnlySpan<char> text = ...;
File.WriteAllText(filePath, text);
Novos métodos de extensão StartsWith<T>(ReadOnlySpan<T>, T) e EndsWith<T>(ReadOnlySpan<T>, T) também foram adicionados para spans, facilitando testar se um ReadOnlySpan<T> começa ou termina com um valor específico T.
O código a seguir usa essas novas APIs de conveniência.
ReadOnlySpan<char> text = "some arbitrary text";
return text.StartsWith('"') && text.EndsWith('"'); // false
Sobrecargas de params ReadOnlySpan<T>
O C# sempre deu suporte à marcação de parâmetros de matriz como params. Essa palavra-chave permite uma sintaxe de chamada simplificada. Por exemplo, o String.Join(String, String[]) segundo parâmetro do método é marcado com params. Você pode chamar essa sobrecarga com uma matriz ou passando os valores individualmente:
string result = string.Join(", ", new string[3] { "a", "b", "c" });
string result = string.Join(", ", "a", "b", "c");
Antes do .NET 9, quando você passa os valores individualmente, o compilador C# emite um código idêntico à primeira chamada, produzindo uma matriz implícita em torno dos três argumentos.
A partir do C# 13, você pode usar params com qualquer argumento que possa ser construído por meio de uma expressão de coleção, incluindo intervalos de memória (Span<T> e ReadOnlySpan<T>). Isso é benéfico para usabilidade e desempenho. O compilador C# pode armazenar os argumentos na pilha, encapsular um span ao redor deles e passá-lo para o método, o que evita a alocação de matriz implícita que teria ocorrido de outra forma.
O .NET 9 inclui mais de 60 métodos com um params ReadOnlySpan<T> parâmetro. Algumas são sobrecargas completamente novas, enquanto outras são métodos existentes que já aceitavam um ReadOnlySpan<T>, mas agora têm o parâmetro marcado com params. O efeito líquido é que, se você atualizar para o .NET 9 e recompilar seu código, verá melhorias de desempenho sem precisar alterar o código. Isso ocorre porque o compilador prefere vincular a sobrecargas baseadas em span do que às sobrecargas baseadas em matriz.
Por exemplo, String.Join agora inclui a seguinte sobrecarga, que implementa o novo padrão: String.Join(String, ReadOnlySpan<String>)
Agora, uma chamada como string.Join(", ", "a", "b", "c") é feita sem alocar um array para passar nos argumentos "a", "b" e "c".
Enumerar sobre segmentos de ReadOnlySpan<char>.Split()
string.Split é um método conveniente para particionar rapidamente uma cadeia de caracteres com um ou mais separadores fornecidos. Para código focado em desempenho, no entanto, o perfil de alocação de string.Split pode ser proibitivo, pois aloca uma cadeia de caracteres para cada componente analisado e um string[] para armazenar todos. Ele também não funciona com spans, portanto, se você tiver um ReadOnlySpan<char>, será forçado a alocar mais uma cadeia de caracteres ao convertê-lo para poder chamar string.Split nela.
No .NET 8, foi introduzido um conjunto de métodos Split e SplitAny para ReadOnlySpan<char>. Em vez de retornar um novo string[], esses métodos aceitam um destino Span<Range> no qual os índices de delimitação para cada componente são gravados. Isso torna a operação totalmente livre de alocação. Esses métodos são apropriados para uso quando o número de intervalos é conhecido e pequeno.
No .NET 9, novas sobrecargas de Split e SplitAny foram adicionadas para permitir a análise incremental de um ReadOnlySpan<T> com um número de segmentos a priori desconhecido. Os novos métodos permitem a enumeração por meio de cada segmento, que é representado da mesma forma como um Range que pode ser usado para dividir o intervalo original.
public static bool ListContainsItem(ReadOnlySpan<char> span, string item)
{
foreach (Range segment in span.Split(','))
{
if (span[segment].SequenceEquals(item))
{
return true;
}
}
return false;
}
System.Formats
A posição ou deslocamento dos dados no fluxo delimitador de um objeto TarEntry agora é uma propriedade pública. TarEntry.DataOffset retorna a posição no fluxo de arquivos da entrada em que o primeiro byte de dados da entrada está localizado. Os dados da entrada são encapsulados em um substream que você pode acessar por meio TarEntry.DataStream, o que oculta a posição real dos dados em relação ao fluxo de arquivos. Isso é suficiente para a maioria dos usuários, mas se você precisar de mais flexibilidade e quiser saber a posição inicial real dos dados no fluxo de arquivos, a nova TarEntry.DataOffset API facilitará o suporte a recursos como acesso simultâneo com arquivos TAR muito grandes.
// Create stream for tar ball data in Azure Blob Storage.
BlobClient blobClient = new(connectionString, blobContainerName, blobName);
Stream blobClientStream = await blobClient.OpenReadAsync(options, cancellationToken);
// Create TarReader for the stream and get a TarEntry.
TarReader tarReader = new(blobClientStream);
System.Formats.Tar.TarEntry? tarEntry = await tarReader.GetNextEntryAsync();
if (tarEntry is null)
return;
// Get position of TarEntry data in blob stream.
long entryOffsetInBlobStream = tarEntry.DataOffset;
long entryLength = tarEntry.Length;
// Create a separate stream.
Stream newBlobClientStream = await blobClient.OpenReadAsync(options, cancellationToken);
newBlobClientStream.Seek(entryOffsetInBlobStream, SeekOrigin.Begin);
// Read tar ball content from separate BlobClient stream.
byte[] bytes = new byte[entryLength];
await newBlobClientStream.ReadExactlyAsync(bytes, 0, (int)entryLength);
System.Guid
NewGuid() cria um Guid preenchido principalmente com dados aleatórios criptograficamente seguros, seguindo a especificação da Versão 4 do UUID conforme o RFC 9562. Esse mesmo RFC também define outras versões, incluindo a Versão 7, que "apresenta um campo de valor ordenado por tempo derivado da fonte de timestamp Unix Epoch amplamente implementada e conhecida". Em outras palavras, grande parte dos dados ainda é aleatória, mas alguns deles são reservados para dados com base em um carimbo de data/hora, o que permite que esses valores tenham uma ordem de classificação natural. No .NET 9, você pode criar um Guid de acordo com a versão 7 usando os novos métodos Guid.CreateVersion7() e Guid.CreateVersion7(DateTimeOffset). Você também pode usar a nova Version propriedade para recuperar o campo de versão de um Guid objeto.
System.IO
- Compactação com zlib-ng
- Opções de compactação ZLib e Brotli
- Documentos XPS da impressora virtual XPS
Compactação com zlib-ng
System.IO.Compression recursos como ZipArchive, DeflateStream, GZipStreame ZLibStream são todos baseados principalmente na biblioteca zlib. A partir do .NET 9, esses recursos usam o zlib-ng, uma biblioteca que produz processamento mais consistente e eficiente em uma matriz mais ampla de sistemas operacionais e hardware.
Opções de compactação ZLib e Brotli
ZLibCompressionOptionse BrotliCompressionOptions são novos tipos para definir o nível e a estratégia de compactação específicos do algoritmo (Default, Filtered, , HuffmanOnlyou RunLengthEncodingFixed). Esses tipos são destinados a usuários que desejam configurações mais ajustadas do que a única opção existente, <System.IO.Compression.CompressionLevel>.
Os novos tipos de opção de compactação podem ser expandidos no futuro.
O snippet de código a seguir mostra alguns exemplos de uso:
private MemoryStream CompressStream(Stream uncompressedStream)
{
MemoryStream compressorOutput = new();
using ZLibStream compressionStream = new(
compressorOutput,
new ZLibCompressionOptions()
{
CompressionLevel = 6,
CompressionStrategy = ZLibCompressionStrategy.HuffmanOnly
}
);
uncompressedStream.CopyTo(compressionStream);
compressionStream.Flush();
return compressorOutput;
}
Documentos XPS da impressora virtual XPS
Documentos XPS provenientes de uma impressora virtual XPS V4 anteriormente não podiam ser abertos usando a System.IO.Packaging biblioteca, devido à falta de suporte para manipulação de arquivos .piece . Essa lacuna foi resolvida no .NET 9.
System.Numerics
- Limite superior do BigInteger
-
BigMulApis - APIs de conversão de vetor
- APIs de criação de vetores
- Aceleração adicional
Limite superior do BigInteger
BigInteger dá suporte à representação de valores inteiros de comprimento essencialmente arbitrário. No entanto, na prática, o comprimento é restringido por limites do computador subjacente, como memória disponível ou quanto tempo levaria para calcular uma determinada expressão. Além disso, existem algumas APIs que falham em determinadas entradas que resultam em um valor muito grande. Devido a esses limites, o .NET 9 impõe um comprimento máximo de BigInteger, que é que ele pode conter não mais do que (2^31) - 1 (aproximadamente 2,14 bilhões) bits. Esse número representa uma alocação de quase 256 MB e contém aproximadamente 646,5 milhões de dígitos. Esse novo limite garante que todas as APIs expostas sejam bem comportadas e consistentes, permitindo ainda números muito além da maioria dos cenários de uso.
APIs BigMul
BigMul é uma operação que produz o produto completo de dois números. O .NET 9 adiciona APIs dedicadas em BigMul, int, long, uint e ulong, cujo tipo de retorno é o próximo tipo inteiro maior do que os tipos de parâmetro.
As novas APIs são:
-
BigMul(Int32, Int32) (retorna
long) -
BigMul(Int64, Int64) (retorna
Int128) -
BigMul(UInt32, UInt32) (retorna
ulong) -
BigMul(UInt64, UInt64) (retorna
UInt128)
APIs de conversão de vetor
O .NET 9 adiciona APIs de extensão dedicadas para converter entre Vector2, Vector3, , Vector4e QuaternionPlane.
As novas APIs são as seguintes:
- AsPlane(Vector4)
- AsQuaternion(Vector4)
- AsVector2(Vector4)
- AsVector3(Vector4)
- AsVector4(Plane)
- AsVector4(Quaternion)
- AsVector4(Vector2)
- AsVector4(Vector3)
- AsVector4Unsafe(Vector2)
- AsVector4Unsafe(Vector3)
Para conversões do mesmo tamanho, como entre Vector4, Quaternione Plane, essas conversões são custo zero. O mesmo pode ser dito para restringir conversões, como de Vector4 para Vector2 ou Vector3. Para conversões de ampliação, como de Vector2 ou Vector3 para Vector4, há a API normal, que inicializa novos elementos para zero, e uma API com o sufixo Unsafe que deixa esses novos elementos indefinidos e, portanto, pode ser custo zero.
APIs para criação de vetores
Há novas APIs Create expostas para Vector, Vector2, Vector3 e Vector4 que trazem paridade com as APIs equivalentes expostas para os tipos de vetor de hardware no namespace System.Runtime.Intrinsics.
Para obter mais informações sobre as novas APIs, consulte:
Essas APIs são principalmente para conveniência e consistência geral entre os tipos acelerados por SIMD do .NET.
Aceleração adicional
Melhorias de desempenho adicionais foram feitas em muitos tipos no System.Numerics namespace, incluindo BigInteger, Vector2, Vector3, Vector4, Quaternion e Plane.
Em alguns casos, isso resultou em uma aceleração de 2 a 5 vezes em APIs principais, incluindo a multiplicação de Matrix4x4, a criação de Plane a partir de uma série de vértices, a concatenação de Quaternion e o cálculo do produto vetorial de um Vector3.
Há também suporte a dobragem constante para a API SinCos, que calcula tanto Sin(x) quanto Cos(x) em uma única chamada, tornando a operação mais eficiente.
Tensors para IA
Tensors são a estrutura de dados fundamental da IA (inteligência artificial). Muitas vezes, elas podem ser consideradas como matrizes multidimensionais.
Tensores são usados para:
- Representar e codificar dados como sequências de texto (tokens), imagens, vídeo e áudio.
- Manipule dados de dimensão superior com eficiência.
- Aplique cálculos com eficiência em dados de dimensão superior.
- Armazene informações de peso e cálculos intermediários (em redes neurais).
Para usar as APIs de tensor do .NET, instale o pacote NuGet System.Numerics.Tensors .
Novo tipo de Tensor<T>
O novo Tensor<T> tipo expande os recursos de IA das bibliotecas do .NET e do runtime. Este tipo:
- Fornece interoperabilidade eficiente com bibliotecas de IA como ML.NET, TorchSharp e ONNX Runtime usando zero cópias sempre que possível.
- Baseia-se em TensorPrimitives para operações matemáticas eficientes.
- Habilita a manipulação de dados fácil e eficiente fornecendo operações de indexação e fatiamento.
- Não é uma substituição para bibliotecas de IA e machine learning existentes. Em vez disso, destina-se a fornecer um conjunto comum de APIs para reduzir a duplicação e as dependências de código e obter um melhor desempenho usando os recursos de runtime mais recentes.
Os códigos a seguir mostram algumas das APIs incluídas com o novo Tensor<T> tipo.
// Create a tensor (1 x 3).
Tensor<int> t0 = Tensor.Create([1, 2, 3], [1, 3]); // [[1, 2, 3]]
// Reshape tensor (3 x 1).
Tensor<int> t1 = t0.Reshape(3, 1); // [[1], [2], [3]]
// Slice tensor (2 x 1).
Tensor<int> t2 = t1.Slice(1.., ..); // [[2], [3]]
// Broadcast tensor (3 x 1) -> (3 x 3).
// [
// [ 1, 1, 1],
// [ 2, 2, 2],
// [ 3, 3, 3]
// ]
var t3 = Tensor.Broadcast<int>(t1, [3, 3]);
// Math operations.
var t4 = Tensor.Add(t0, 1); // [[2, 3, 4]]
var t5 = Tensor.Add(t0.AsReadOnlyTensorSpan(), t0); // [[2, 4, 6]]
var t6 = Tensor.Subtract(t0, 1); // [[0, 1, 2]]
var t7 = Tensor.Subtract(t0.AsReadOnlyTensorSpan(), t0); // [[0, 0, 0]]
var t8 = Tensor.Multiply(t0, 2); // [[2, 4, 6]]
var t9 = Tensor.Multiply(t0.AsReadOnlyTensorSpan(), t0); // [[1, 4, 9]]
var t10 = Tensor.Divide(t0, 2); // [[0.5, 1, 1.5]]
var t11 = Tensor.Divide(t0.AsReadOnlyTensorSpan(), t0); // [[1, 1, 1]]
Observação
Essa API é marcada como experimental para o .NET 9.
TensorPrimitives
A System.Numerics.Tensors biblioteca inclui a TensorPrimitives classe, que fornece métodos estáticos para executar operações numéricas em intervalos de valores. No .NET 9, o escopo dos métodos expostos por TensorPrimitives foi significativamente expandido, crescendo de 40 (no .NET 8) para quase 200 sobrecargas. A área de superfície abrange operações numéricas familiares de tipos como Math e MathF. Ele também inclui as interfaces matemáticas genéricas como INumber<TSelf>, exceto em vez de processar um valor individual, que processam um intervalo de valores. Muitas operações também foram aceleradas por meio de implementações otimizadas para SIMD no .NET 9.
TensorPrimitives agora expõe sobrecargas genéricas para qualquer tipo T que implemente uma determinada interface. (A versão do .NET 8 incluía apenas sobrecargas para manipulação de spans de valores do tipo float.) Por exemplo, a nova sobrecarga CosineSimilarity<T>(ReadOnlySpan<T>, ReadOnlySpan<T>) executa a similaridade de cosseno em dois vetores de valores float, double ou Half ou valores de qualquer outro tipo que implemente IRootFunctions<TSelf>.
Compare a precisão da operação de similaridade de cosseno em dois vetores do tipo float versus double:
ReadOnlySpan<float> vector1 = [1, 2, 3];
ReadOnlySpan<float> vector2 = [4, 5, 6];
Console.WriteLine(TensorPrimitives.CosineSimilarity(vector1, vector2));
// Prints 0.9746318
ReadOnlySpan<double> vector3 = [1, 2, 3];
ReadOnlySpan<double> vector4 = [4, 5, 6];
Console.WriteLine(TensorPrimitives.CosineSimilarity(vector3, vector4));
// Prints 0.9746318461970762
Encadeamento
As APIs de threading incluem melhorias para iteração de tarefas, para canais priorizados, que podem ordenar seus elementos em vez de serem PEPS (primeiro a entrar, primeiro a sair) e Interlocked.CompareExchange para mais tipos.
Task.WhenEach
Uma variedade de novas APIs úteis foram adicionadas para trabalhar com Task<TResult> objetos. O novo método Task.WhenEach permite iterar através de tarefas à medida que são concluídas, utilizando uma instrução await foreach. Você não precisa mais fazer coisas como chamar Task.WaitAny repetidamente em um conjunto de tarefas para selecionar a próxima tarefa concluída.
O código a seguir realiza várias chamadas HttpClient e processa seus resultados conforme elas são concluídas.
using HttpClient http = new();
Task<string> dotnet = http.GetStringAsync("http://dot.net");
Task<string> bing = http.GetStringAsync("http://www.bing.com");
Task<string> ms = http.GetStringAsync("http://microsoft.com");
await foreach (Task<string> t in Task.WhenEach(bing, dotnet, ms))
{
Console.WriteLine(t.Result);
}
Canal ilimitado priorizado
O namespace System.Threading.Channels permite criar canais first-in-first-out (FIFO) usando os métodos CreateBounded e CreateUnbounded. Com os canais FIFO, os elementos são lidos do canal na ordem em que foram gravados nele. No .NET 9, foi adicionado o novo método CreateUnboundedPrioritized, que ordena os elementos de forma que o próximo elemento lido do canal seja o considerado mais importante, de acordo com um Comparer<T>.Default ou um IComparer<T> personalizado.
O exemplo a seguir usa o novo método para criar um canal que gera os números de 1 a 5 em ordem, mesmo que sejam gravados no canal em uma ordem diferente.
Channel<int> c = Channel.CreateUnboundedPrioritized<int>();
await c.Writer.WriteAsync(1);
await c.Writer.WriteAsync(5);
await c.Writer.WriteAsync(2);
await c.Writer.WriteAsync(4);
await c.Writer.WriteAsync(3);
c.Writer.Complete();
while (await c.Reader.WaitToReadAsync())
{
while (c.Reader.TryRead(out int item))
{
Console.Write($"{item} ");
}
}
// Output: 1 2 3 4 5
Interlocked.CompareExchange para mais tipos
Nas versões anteriores do .NET, Interlocked.Exchange e Interlocked.CompareExchange tinha sobrecargas para trabalhar com int, , uint, long, ulong, nint, nuint, float, doublee object, bem como uma sobrecarga genérica para trabalhar com qualquer tipo Tde referência. No .NET 9, há novas sobrecargas para trabalhar atomicamente com byte, sbyte, short e ushort. Além disso, a restrição nas sobrecargas genéricas Interlocked.Exchange<T> e Interlocked.CompareExchange<T> foi removida, portanto, esses métodos não estão mais limitados a trabalhar apenas com tipos de referência. Agora eles podem trabalhar com qualquer tipo primitivo, que inclui todos os tipos mencionados acima, bem como boolchar, bem como qualquer enum tipo.