Partilhar via


O que há de novo nas bibliotecas .NET para .NET 9

Este artigo descreve novos recursos nas bibliotecas .NET para .NET 9.

Base64Url

Base64 é um esquema de codificação que traduz bytes arbitrários em texto composto por um conjunto específico de 64 caracteres. É uma abordagem comum para a transferência de dados e há muito tempo é suportada 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 usa tornam-no menos ideal para uso em algumas circunstâncias em que se pretendia utilizá-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 Base64Url classe, que fornece muitos métodos úteis e otimizados para codificação e decodificação com Base64Url de e para 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 é removido do tempo de BinaryFormatter execução do .NET. As APIs ainda estão presentes, mas suas implementações sempre lançam 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 BinaryFormatter migration guide.

Coleções

Os tipos de coleção no .NET ganham as seguintes atualizações para o .NET 9:

Pesquisas de coleções com intervalos

Em código de alto desempenho, spans são frequentemente usados para evitar a alocação de strings desnecessariamente, e tabelas de pesquisa com tipos como Dictionary<TKey,TValue> e HashSet<T> são frequentemente usadas como caches. No entanto, não houve nenhum mecanismo seguro e integrado para realizar consultas nesses tipos de coleção com spans. Com a nova allows ref struct funcionalidade no C# 13 e as novas funcionalidades nos tipos de coleção no .NET 9, agora é possível realizar esses tipos de pesquisas.

O exemplo a seguir demonstra a utilização 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 forma em que a ordem possa ser mantida (uma lista de pares chave-valor), mas onde a pesquisa rápida por chave também seja suportada (um dicionário de pares chave-valor). Desde os primórdios do .NET, o OrderedDictionary tipo oferece suporte a esse cenário, mas apenas de maneira não genérica, com chaves e valores digitados como object. O .NET 9 apresenta a coleção há muito solicitada OrderedDictionary<TKey,TValue> , que fornece um tipo eficiente e genérico 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 o PriorityQueue<TElement,TPriority> conjunto, que fornece uma implementação simples e rápida de pilha de arrays. Um problema com pilhas de matriz em geral é que elas não suportam atualizações prioritárias, tornando-as proibitivas para uso em algoritmos como variações do algoritmo de Dijkstra.

Embora não seja possível implementar atualizações de prioridade eficientes de $O(\log n)$ na coleção existente, o novo método PriorityQueue<TElement,TPriority>.Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) torna possível emular atualizações de prioridade (embora em $O(n)$ no tempo):

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 gráficos em contextos onde o desempenho assintótico não é um bloqueador. (Tais contextos incluem educação e prototipagem.) Por exemplo, aqui está uma implementação de brinquedo do algoritmo de Dijkstra que usa a nova API.

ReadOnlySet<T>

Muitas vezes, é desejável fornecer visões só de leitura de coleções. ReadOnlyCollection<T> permite criar um wrapper somente leitura em torno de um mutável IList<T>arbitrário e ReadOnlyDictionary<TKey,TValue> permite criar um wrapper somente leitura em torno de um mutável IDictionary<TKey,TValue>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 - TypeDescriptor suporte de corte

System.ComponentModel inclui novas APIs compatíveis com o trimmer opt-in para descrever componentes. Qualquer aplicação, especialmente aplicações independentes e reduzidas, pode usar as novas APIs para suportar cenários de redução.

O método TypeDescriptor.RegisterType na classe TypeDescriptor é a API primária. Este método tem o atributo DynamicallyAccessedMembersAttribute para que o trimmer preserve membros desse tipo. Você deve chamar esse método uma vez por tipo e, normalmente, logo no início.

As APIs secundárias têm um FromRegisteredType sufixo, como TypeDescriptor.GetPropertiesFromRegisteredType(Type). Ao contrário das suas contrapartes que não têm o sufixo FromRegisteredType, estas APIs não têm atributos de aparagem [RequiresUnreferencedCode] ou [DynamicallyAccessedMembers]. A falta de atributos de aparador beneficia os consumidores ao eliminar a necessidade de:

  • Suprimir avisos de recorte, que podem ser arriscados.
  • Propagar um parâmetro fortemente tipado Type para outros métodos, que podem ser complicados ou inviáveis.
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 de API .

Criptografia

Método CryptographicOperations.HashData()

O .NET inclui várias implementações estáticas "one-shot" de funções hash e funções relacionadas. Essas APIs incluem SHA256.HashData e HMACSHA256.HashData. As APIs one-shot são preferíveis pois podem fornecer o desempenho máximo e reduzir ou eliminar alocações.

Se um desenvolvedor quiser fornecer uma API que ofereça suporte a hash em que o chamador define qual algoritmo de hash usar, isso geralmente é feito aceitando um HashAlgorithmName argumento. No entanto, usar esse padrão com APIs one-shot 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 gerar um hash ou HMAC de uma entrada em uma única operação, onde 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. KECCAK Message Authentication Code (KMAC) é uma função pseudoaleatória e função hash chaveada baseada em KECCAK.

As novas classes a seguir usam o algoritmo KMAC. Use instâncias para acumular dados para produzir um MAC ou use o método estático HashData para uma captura única em uma única 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 static IsSupported para determinar se a plataforma suporta o 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.
}

Os algoritmos AES-GCM e ChaChaPoly1305 ativados para iOS/tvOS/MacCatalyst

IsSupported e ChaChaPoly1305.IsSupported agora retorne true quando executado no iOS 13+, tvOS 13+ e Mac Catalyst.

AesGcm suporta apenas valores de etiquetas de 16 bytes (128 bits) nos sistemas operativos Apple.

Carregamento do certificado X.509

Desde o .NET Framework 2.0, a maneira de carregar um certificado é new X509Certificate2(bytes). Houve também 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).

Todos esses métodos utilizavam a análise de conteúdo para determinar se a entrada era algo que conseguiam processar e, em seguida, carregavam-na caso conseguissem. Para alguns interlocutores, essa estratégia foi muito conveniente. Mas também tem alguns problemas:

  • Nem todos os formatos de arquivo funcionam em todos os sistemas operacionais.
  • É um desvio de protocolo.
  • É uma fonte de problemas de segurança.

O .NET 9 introduz 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 X509Certificate2 construtor suportava. Esses são os dois formatos que funcionaram em todos os sistemas operacionais.

Suporte a provedores OpenSSL

O .NET 8 introduziu as APIs OpenPrivateKeyFromEngine(String, String) específicas do OpenSSL e OpenPublicKeyFromEngine(String, String). Permitem a interação com componentes OpenSSL ENGINE e utilizam, por exemplo, módulos de segurança de hardware (HSM).

O .NET 9 apresenta SafeEvpPKeyHandle.OpenKeyFromProvider(String, String), que permite usar provedores OpenSSL e interagir com provedores como tpm2 ou pkcs11.

Algumas distros removeram o ENGINE suporte uma vez que agora está obsoleto.

O trecho 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.
}

Há algumas melhorias de desempenho durante o handshake TLS, bem como melhorias nas interações com chaves privadas RSA que usam ENGINE componentes.

Segurança baseada em virtualização do Windows CNG

O Windows 11 adicionou novas APIs para ajudar a proteger chaves do Windows com segurança baseada em virtualização (VBS). Com esse novo recurso, as chaves podem ser protegidas contra ataques de roubo de chaves em nível de administrador com efeito insignificante no desempenho, confiabilidade ou escala.

O .NET 9 adicionou sinalizadores correspondentes CngKeyCreationOptions . Foram acrescentadas as seguintes três bandeiras:

  • CngKeyCreationOptions.PreferVbs correspondência NCRYPT_PREFER_VBS_FLAG
  • CngKeyCreationOptions.RequireVbs correspondência NCRYPT_REQUIRE_VBS_FLAG
  • CngKeyCreationOptions.UsePerBootKey correspondência NCRYPT_USE_PER_BOOT_KEY_FLAG

O trecho 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) pode não representar 101 seconds, 832 millisecondscom precisão , mas sim aproximadamente 101 seconds, 831.9999999999936335370875895023345947265625 milliseconds. Essa discrepância tem causado 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 a partir de inteiros. Há novas sobrecargas de FromDays, FromHours, FromMinutes, FromSeconds, FromMillisecondse FromMicroseconds.

O código a seguir mostra um exemplo de uma chamada para double e uma das novas sobrecargas de 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 - ActivatorUtilities.CreateInstance construtor

A resolução do construtor para ActivatorUtilities.CreateInstance foi modificada no .NET 9. Anteriormente, um construtor que foi explicitamente marcado usando o ActivatorUtilitiesConstructorAttribute atributo pode não ser chamado, dependendo da ordem dos construtores e do número de parâmetros do construtor. A lógica foi alterada no .NET 9 de tal forma que um construtor que tem o atributo é sempre chamado.

Diagnóstico

Debug.Assert relata condição de asserção por padrão

Debug.Assert é comumente usado para ajudar a validar condições que se espera que sejam sempre verdadeiras. Falha normalmente indica um bug no código. Existem muitas sobrecargas de Debug.Assert, a mais simples das quais apenas aceita uma condição:

Debug.Assert(a > 0 && b > 0);

A asserção falhará se a condição for falsa. Historicamente, no entanto, tais afirmações eram desprovidas de qualquer informação sobre qual condição falhou. A partir do .NET 9, se nenhuma mensagem for explicitamente fornecida pelo utilizador, a afirmação conterá a representação textual da condição. Por exemplo, para o exemplo de assertiva anterior, ao invés 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)

Anteriormente, você só podia vincular um rastreamento Activity a outros contextos de rastreamento quando criava o Activity. Novidade no .NET 9, a AddLink(ActivityLink) API permite vincular um Activity objeto a outros contextos de rastreamento após sua criação. Essa alteração também está alinhada com as especificações do OpenTelemetry .

ActivityContext activityContext = new(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None);
ActivityLink activityLink = new(activityContext);

Activity activity = new("LinkTest");
activity.AddLink(activityLink);

Medidor do Metrics.Gauge

System.Diagnostics.Metrics agora fornece a ferramenta Gauge<T> de acordo com a especificação OpenTelemetry. O Gauge instrumento foi concebido para registar valores não aditivos quando ocorrem alterações. Por exemplo, ele pode medir o nível de ruído de fundo, onde somar os valores de várias salas seria absurdo. 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" } });

Escuta curinga do medidor fora do proc

Já é possível escutar medidores fora do processo usando o System.Diagnostics.Metrics provedor de fonte de eventos, mas antes do .NET 9, tinhas que especificar o nome completo do medidor. No .NET 9, você pode ouvir 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 ouvir 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 forma.

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 monitorizar métricas com ferramentas de monitorização, como dotnet-counters.

LINQ

Novos métodos CountBy e AggregateBy foram introduzidos. Esses métodos permitem agregar estado por chave sem a necessidade de alocar agrupamentos intermediários via GroupBy.

CountBy permite-lhe calcular rapidamente a frequência de cada chave. O exemplo a seguir localiza a palavra que ocorre com mais freqüê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 fluxos de trabalho de uso mais geral. O exemplo a seguir mostra como você pode calcular 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 trecho 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 origem de registro

O C# 12 introduziu construtores primários, que permitem definir um construtor diretamente na declaração de classe. O gerador de origem de log agora oferece suporte ao registro em log usando classes que têm um construtor primário.

public partial class ClassWithPrimaryConstructor(ILogger logger)
{
    [LoggerMessage(0, LogLevel.Debug, "Test.")]
    public partial void Test();
}

Diversos

Nesta secção, encontre informações sobre:

allows ref struct utilizados em bibliotecas

O C# 13 introduz a capacidade de restringir um parâmetro genérico com allows ref struct, que informa ao compilador e ao tempo de execução que um ref struct pode ser usado para esse parâmetro genérico. Muitas APIs compatíveis com isso foram agora documentadas. Por exemplo, o String.Create método tem uma sobrecarga que permite criar um string escrevendo diretamente em sua memória, representada como uma extensão. Este método tem um TState argumento que é passado do chamador para o delegado que faz a escrita propriamente dita.

Esse TState parâmetro de tipo em String.Create agora é 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 passe um intervalo (ou qualquer outro ref struct) como entrada para este método.

O exemplo a seguir mostra uma nova String.ToLowerInvariant() sobrecarga que usa esse recurso.

public static string ToLowerInvariant(ReadOnlySpan<char> input) =>
    string.Create(span.Length, input, static (stringBuffer, input) => span.ToLowerInvariant(stringBuffer));

SearchValues expansão

O .NET 8 introduziu o tipo SearchValues<T>, que fornece uma solução otimizada para pesquisar conjuntos específicos de caracteres ou bytes em intervalos. No .NET 9, SearchValues foi estendido para oferecer suporte à pesquisa de substrings dentro de uma cadeia de caracteres maior.

O exemplo a seguir procura 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);

Esta nova capacidade tem uma implementação otimizada que tira partido do suporte 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

HttpClientFactory Cria HttpClient objetos apoiados pelo HttpClientHandler, por padrão. HttpClientHandler é apoiado pelo SocketsHttpHandler, que é muito mais configurável, inclusive em torno do gerenciamento do tempo de vida da conexão. HttpClientFactory agora usa SocketsHttpHandler por padrão e configura-o 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

Eventos enviados por servidor (SSE) é um protocolo simples e popular para streaming de dados de um servidor para um cliente. É usado, por exemplo, pela OpenAI como parte do streaming de texto gerado a partir de seus serviços de IA. Para simplificar o consumo de SSE, a nova System.Net.ServerSentEvents biblioteca fornece um analisador para a ingestão fácil de 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);
}

Reinício de TLS com certificados de cliente no Linux

Reativação 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 economiza recursos computacionais durante o handshake TLS.

Reinício de sessão TLS já é suportado no Linux para conexões SslStream sem certificados de cliente. O .NET 9 adiciona suporte para a retomada TLS de conexões TLS mutuamente autenticadas, que são comuns em cenários de servidor para servidor. O recurso é ativado automaticamente.

Ping e tempo limite de manutenção de vida do WebSocket

Novas APIs em ClientWebSocketOptions e WebSocketCreationOptions permitem que se opte por enviar pings de WebSocket e abortar a conexão se o par não responder a tempo.

Até agora, era possível especificar um KeepAliveInterval para evitar que a conexão ficasse ociosa, mas não havia nenhum mecanismo integrado para assegurar que o parceiro responda.

O exemplo a seguir executa ping no servidor a cada 5 segundos e anula a conexão se ele não responder em 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 optar por registrar valores para cabeçalhos específicos por meio do RedactLoggedHeaders método auxiliar.

O exemplo a seguir retira todos os cabeçalhos, exceto o agente do usuário.

services.AddHttpClient("myClient")
    .RedactLoggedHeaders(name => name != "User-Agent");

Para obter mais informações, consulte O registo do HttpClientFactory oculta os valores do cabeçalho por predefinição.

Reflexão

Montagens persistentes

Nas versões .NET Core e .NET 5-8, o suporte para criar um assembly e emitir metadados de reflexão para tipos criados dinamicamente estava limitado a um AssemblyBuilder executável. A falta de suporte para salvar um assembly era muitas vezes um bloqueador para clientes que migravam do .NET Framework para o .NET. O .NET 9 adiciona um novo tipo, PersistedAssemblyBuilder, que pode ser usado para guardar um assembly emitido.

Para criar uma PersistedAssemblyBuilder instância, chame seu construtor e passe o nome do assembly, o assembly principal, System.Private.CoreLib, para fazer referência a tipos de tempo de execução base e atributos personalizados opcionais. Depois de emitir todos os membros para o assembly, chame o PersistedAssemblyBuilder.Save(String) método para criar um assembly com as configurações padrão. Se quiser definir o ponto de entrada ou outras opções, você pode chamar PersistedAssemblyBuilder.GenerateMetadata e usar os metadados que ele retorna para salvar o assembly. O código a seguir mostra um exemplo de criação de um assembly persistente e definição do 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 ao PDB. Você pode 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 Emita símbolos e gere PDB.

Análise de nome de tipo

TypeName é um analisador para nomes de tipo ECMA-335 que fornece praticamente a mesma funcionalidade que 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 passou a usar TypeName.

A nova TypeName classe prevê:

  • Métodos estáticos Parse e TryParse para analisar entradas representadas como ReadOnlySpan<char>. Ambos os métodos aceitam uma instância da classe TypeNameParseOptions (um conjunto de opções) que permite a personalização da análise.

  • Name, FullNamee AssemblyQualifiedName propriedades que funcionam exatamente como suas contrapartes em System.Type.

  • Várias propriedades e métodos que fornecem informações adicionais sobre o próprio nome:

    • IsArray, IsSZArray (SZ significa uma matriz unidimensional indexada a zero), IsVariableBoundArrayType e GetArrayRank para trabalhar com arrays.
    • IsConstructedGenericType, GetGenericTypeDefinitione GetGenericArguments para trabalhar com nomes de tipo genéricos.
    • IsByRef e IsPointer para trabalhar com ponteiros e referências gerenciadas.
    • GetElementType() para trabalhar com ponteiros, referências e matrizes.
    • IsNested e DeclaringType para trabalhar com tipos aninhados.
    • AssemblyName, que expõe as informações de nome do assembly por meio da nova classe AssemblyNameInfo. Em contraste com AssemblyName, o novo tipo é imutável, e a análise de nomes culturais não cria instâncias de CultureInfo.

Os tipos TypeName e AssemblyNameInfo são imutáveis e não fornecem uma maneira de verificar a igualdade (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 trecho 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 .NET de nível inferior.

Expressões regulares

[GeneratedRegex] em propriedades

O .NET 7 introduziu o gerador de código-fonte e o Regex atributo correspondente GeneratedRegexAttribute .

O método parcial abaixo será gerado com todo o código necessário para implementar este elemento Regex.

[GeneratedRegex(@"\b\w{5}\b")]
private static partial Regex FiveCharWord();

O C# 13 oferece 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 do exemplo anterior.

[GeneratedRegex(@"\b\w{5}\b")]
private static partial Regex FiveCharWordProperty { get; }

Regex.EnumerateSplits

A Regex classe fornece um Split método, semelhante em conceito ao String.Split método. Com String.Split, você fornece um ou mais separadores char ou string, 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 abaixo 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 só aceita um string como entrada e não suporta que a entrada seja fornecida como um ReadOnlySpan<char>. Além disso, ele produz todo o conjunto de partições como um string[], o que requer a alocação da matriz string para armazenar os resultados e um string para cada partição. No .NET 9, o novo EnumerateSplits método permite executar a mesma operação, mas com uma entrada baseada em 'span' e sem incorrer em qualquer alocação 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 Regex.EnumerateSplits, tomando 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

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
//}

Opções padrão da Web singleton

Se quiser serializar com as opções padrão que o ASP.NET Core usa para aplicações web, utilize a nova instância única JsonSerializerOptions.Web.

string webJson = JsonSerializer.Serialize(
    new { SomeValue = 42 },
    JsonSerializerOptions.Web // Defaults to camelCase naming policy.
    );
Console.WriteLine(webJson);
// {"someValue":42}

ExportadorJsonSchema

JSON é freqüentemente usado para representar tipos em assinaturas de método como parte de esquemas de chamada de procedimento remoto. É usado, por exemplo, como parte das especificações OpenAPI, ou como parte de chamadas de ferramentas com serviços de IA como os da OpenAI. Os desenvolvedores podem serializar e desserializar tipos .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, descreva a forma do que seria serializado e o que pode ser desserializado). System.Text.Json agora fornece o tipo JsonSchemaExporter, que suporta a geração de um esquema JSON que representa um tipo .NET.

Para obter mais informações, consulte Exportador de esquema JSON.

Respeitar anotações anuláveis

System.Text.Json agora reconhece anotações de anulabilidade nas 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 as anotações anuláveis.

Requer parâmetros de construtor não opcionais

Historicamente, System.Text.Json tratou parâmetros de construtor não obrigatórios como se fossem obrigatórios ao usar a desserialização por 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);

O MyPoco tipo é definido da seguinte forma:

record MyPoco(string Value);

Para obter mais informações, consulte Parâmetros do construtor não opcionais.

Ordenar propriedades JsonObject

O JsonObject tipo agora expõe APIs ordenadas semelhantes a dicionários que permitem a manipulação explícita da ordem de propriedade.

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 a ordem das propriedades.

Personalizar nomes de elementos enum

O novo System.Text.Json.Serialization.JsonStringEnumMemberNameAttribute atributo pode ser usado para personalizar os nomes de membros individuais do enum para tipos que são 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 enum personalizados.

Transmitir vários documentos JSON

System.Text.Json.Utf8JsonReader agora suporta a leitura de vários documentos JSON separados por espaços em branco a partir de um único buffer ou fluxo. Por padrão, o leitor lança uma exceção se detetar caracteres que não sejam de espaço em branco que estejam atrás 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.

Abrangências

Em código de alto desempenho, spans são frequentemente usados para evitar a alocação desnecessária de strings. Span<T> e ReadOnlySpan<T> continuam a revolucionar a forma como o código é escrito em .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 à extensão:

Auxiliares de ficheiros

A classe File agora tem novos auxiliares para escrever ReadOnlySpan<char>/ReadOnlySpan<byte> e ReadOnlyMemory<char>/ReadOnlyMemory<byte> de forma fácil e direta para arquivos.

O código a seguir grava eficientemente 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, tornando mais fácil verificar 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

params ReadOnlySpan<T> sobrecargas

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 código idêntico à primeira chamada, produzindo uma matriz implícita em torno dos três argumentos.

A partir de C# 13, você pode usar params com qualquer argumento que possa ser construído por meio de uma expressão de coleção, incluindo extensões (Span<T> e ReadOnlySpan<T>). Isso é benéfico para a usabilidade e o desempenho. O compilador C# pode armazenar os argumentos na pilha, encapsular um span em torno deles e passá-lo para o método, o que evita a alocação implícita de array que teria ocorrido de outra forma.

O .NET 9 inclui mais de 60 métodos com um params ReadOnlySpan<T> parâmetro. Alguns são novas sobrecargas, e outros são métodos existentes que já possuíam um ReadOnlySpan<T>, mas agora têm esse parâmetro marcado com params. O efeito líquido é que, se atualizares para o .NET 9 e recompilares o teu código, verás melhorias no desempenho sem realizares alterações no código. Isso ocorre porque o compilador prefere se vincular a sobrecargas baseadas em span do que às sobrecargas baseadas em array.

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 a necessidade de alocar uma matriz 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 no desempenho, no entanto, o perfil de alocação de string.Split pode ser proibitivo, porque aloca uma string para cada componente analisado e um string[] para armazenar todos eles. Ele também não funciona com spans, portanto, se tiveres um ReadOnlySpan<char>, serás forçado a alocar mais uma string quando a converteres em uma string para poder chamar string.Split.

No .NET 8, um conjunto de Split e SplitAny métodos foram introduzidos para ReadOnlySpan<char>. Em vez de retornar um novo string[], esses métodos aceitam um destino Span<Range> no qual os índices delimitadores para cada componente são gravados. Isso torna a operação totalmente livre de alocação. Estes métodos são apropriados para usar quando o número de intervalos é conhecido e pequeno.

No .NET 9, foram adicionadas novas sobrecargas de Split e SplitAny para permitir a análise incremental de um ReadOnlySpan<T> com um número a priori desconhecido de segmentos. Os novos métodos permitem enumerar por cada segmento, que é representado de forma semelhante como um Range que pode ser usado para dividir o segmento 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 que engloba um objeto TarEntry agora é uma propriedade pública. TarEntry.DataOffset Retorna a posição no fluxo de arquivo da entrada onde o primeiro byte de dados da entrada está localizado. Os dados da entrada são encapsulados em um subfluxo que você pode acessar via TarEntry.DataStream, que oculta a posição real dos dados em relação ao fluxo de arquivamento. 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 arquivamento, a nova TarEntry.DataOffset API facilita 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 UUID Versão 4 no RFC 9562. Essa mesma RFC também define outras versões, incluindo a Versão 7, que "apresenta um campo de valor organizado temporalmente, derivado da amplamente implementada e conhecida fonte de carimbo de data/hora do Unix Epoch". Em outras palavras, muitos dos dados ainda são aleatórios, mas alguns deles são reservados para dados baseados 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 através do novo Guid.CreateVersion7() e Guid.CreateVersion7(DateTimeOffset) métodos. Você também pode usar a nova Version propriedade para recuperar o campo de versão de um Guid objeto.

System.IO

Compressão com zlib-ng

System.IO.Compression recursos como ZipArchive, DeflateStream, GZipStream, e ZLibStream são todos baseados principalmente na biblioteca zlib. A partir do .NET 9, todos esses recursos usam zlib-ng, uma biblioteca que produz um processamento mais consistente e eficiente em uma ampla variedade de sistemas operacionais e hardware.

Opções de compressão ZLib e Brotli

ZLibCompressionOptions e BrotliCompressionOptions são novos tipos para definir o nível de compressão e a estratégia específicos do algoritmo (Default, Filtered, HuffmanOnly, RunLengthEncodingou Fixed). Esses tipos são destinados a usuários que desejam configurações mais refinadas do que a única opção existente, <System.IO.Compression.CompressionLevel>.

Os novos tipos de opção de compressão podem ser expandidos no futuro.

O trecho 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

Os 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 lidar com arquivos .piece . Essa lacuna foi resolvida no .NET 9.

System.Numerics

Limite superior do BigInteger

BigInteger suporta a representação de valores inteiros de comprimento essencialmente arbitrário. No entanto, na prática, o comprimento é limitado 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 determinadas entradas que resultam em um valor muito grande. Devido a esses limites, o .NET 9 impõe um comprimento máximo de BigInteger, o que significa que não pode conter mais do que (2^31) - 1 (aproximadamente 2,14 mil milhões) bits. Tal 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, ao mesmo tempo em que permite números muito além da maioria dos cenários de uso.

BigMul Interfaces de Programação de Aplicações (APIs)

BigMul é uma operação que produz o produto completo de dois números. O .NET 9 adiciona APIs dedicadas BigMul em 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:

APIs de conversão vetorial

O .NET 9 adiciona APIs de extensão dedicadas para conversão entre Vector2, Vector3, Vector4, Quaternione Plane.

As novas APIs são as seguintes:

Para conversões do mesmo tamanho, como entre Vector4, Quaternione Plane, essas conversões são de custo zero. O mesmo pode ser dito para estreitar conversões, como de Vector4 para Vector2 ou Vector3. Para ampliar conversões, como de Vector2 ou Vector3 para Vector4, há a API normal, que inicializa novos elementos para 0, e uma Unsafe API sufixada que deixa esses novos elementos indefinidos e, portanto, pode ser custo zero.

APIs de criação de vetores

Há novas Create APIs expostas para Vector, Vector2, Vector3 e Vector4 que garantem paridade com as APIs equivalentes expostas para os tipos de vetor de hardware expostos 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 nos tipos acelerados por SIMD do .NET.

Aceleração adicional

Melhorias adicionais de desempenho foram feitas em muitos tipos no System.Numerics espaço de nomes, incluindo BigInteger, Vector2, Vector3, Vector4, Quaternion e Plane.

Em alguns casos, isso resultou em uma aceleração de 2 a 5x para APIs principais, incluindo Matrix4x4 multiplicação, criação de Plane a partir de uma série de vértices, Quaternion concatenação e cálculo do produto cruzado de um Vector3.

Há também suporte de dobramento constante para a SinCos API, que calcula ambos Sin(x) e Cos(x) numa única chamada, tornando-a mais eficiente.

Tensores para IA

Os tensores são a estrutura de dados fundamental da inteligência artificial (IA). Muitas vezes podem ser pensadas como matrizes multidimensionais.

Os tensores são usados para:

  • Represente e codifique dados como sequências de texto (tokens), imagens, vídeo e áudio.
  • Manipule eficientemente dados de dimensões superiores.
  • Aplique eficientemente cálculos em dados de dimensões superiores.
  • Armazene informações de peso e cálculos intermediários (em redes neurais).

Para usar as APIs do tensor .NET, instale o pacote NuGet System.Numerics.Tensors .

Novo tipo Tensor<T>

O novo Tensor<T> tipo expande os recursos de IA das bibliotecas .NET e do tempo de execução. 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.
  • Permite uma manipulação de dados fácil e eficiente, fornecendo operações de indexação e fatiamento.
  • Não substitui as bibliotecas existentes de IA e aprendizado de máquina. Em vez disso, destina-se a fornecer um conjunto comum de APIs para reduzir a duplicação de código e as dependências e obter um melhor desempenho usando os recursos de tempo de execução 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

Esta API está marcada como experimental para o .NET 9.

TensorPrimitivas

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 engloba 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 que, em vez de processar um valor individual, elas processam uma extensão de valores. Muitas operações também foram aceleradas por meio de implementações SIMD otimizadas para o .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 manipular intervalos de float valores.) Por exemplo, a nova CosineSimilarity<T>(ReadOnlySpan<T>, ReadOnlySpan<T>) sobrecarga calcula a semelhança de cosseno em dois vetores de float, double, ou Half valores, ou valores de qualquer outro tipo que implementa 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 iterar tarefas, para canais priorizados, que podem ordenar seus elementos em vez de serem FIFO (first-in-first-out), e Interlocked.CompareExchange para mais tipos.

Task.WhenEach

Uma variedade de novas APIs úteis foram adicionadas para trabalhar com Task<TResult> objetos. O novo Task.WhenEach método permite iterar as tarefas à medida que elas são concluídas usando uma await foreach instrução. Você não precisa mais fazer coisas como chamar Task.WaitAny repetidamente um conjunto de tarefas para escolher a próxima que for concluída.

O código a seguir faz várias chamadas de HttpClient e trabalha com os seus resultados conforme são concluídos.

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 System.Threading.Channels namespace permite criar canais em regime FIFO (first-in-first-out) usando os métodos CreateBounded e CreateUnbounded. Com os canais FIFO, os elementos são lidos do canal na ordem em que foram escritos. No .NET 9, foi adicionado um novo método CreateUnboundedPrioritized, que ordena os elementos de tal forma que o próximo elemento lido do canal seja o considerado mais importante, de acordo com Comparer<T>.Default ou um IComparer<T> personalizado.

O exemplo a seguir usa o novo método para criar um canal que produz os números de 1 a 5 em ordem, mesmo que eles 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 suportar mais tipos

Em versões anteriores do .NET, Interlocked.Exchange e Interlocked.CompareExchange tinham sobrecargas para trabalhar com int, uint, long, ulong, nint, nuint, float, double, e object, bem como uma sobrecarga genérica para trabalhar com qualquer tipo de referência T. No .NET 9, há novas sobrecargas para trabalhar atomicamente com byte, sbyte, short, e ushort. Além disso, a restrição genérica sobre as sobrecargas genéricas Interlocked.Exchange<T> e Interlocked.CompareExchange<T> foi removida, de modo que esses métodos já não estão limitados a trabalhar apenas com tipos de referência. Eles agora podem trabalhar com qualquer tipo primitivo, que inclui todos os tipos acima mencionados, bem como bool e char, bem como qualquer enum tipo.