Novidades do .NET 9

Saiba mais sobre os novos recursos do .NET 9 e encontre links para documentação adicional.

O .NET 9, o sucessor do .NET 8, tem um foco especial em desempenho e aplicativos nativos da nuvem. Ele terá suporte por 18 meses como uma versão STS (prazo padrão de suporte). Você pode baixar o .NET 9 aqui.

Uma novidade no .NET 9, a equipe de engenharia publica atualizações de versão prévia do .NET 9 nas Discussões do GitHub. Esse é um ótimo lugar para fazer perguntas e fornecer comentários sobre o lançamento.

Este artigo foi atualizado para o .NET 9 versão prévia 2. As seções a seguir descrevem as atualizações para as bibliotecas principais do .NET no .NET 9.

Runtime do .NET

Serialização

No System.Text.Json, o .NET 9 tem novas opções para serializar JSON e um novo singleton que facilita a serialização usando padrões da Web.

Opções de recuo

O 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

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}

LINQ

Os novos métodos CountBy e AggregateBy foram introduzidos. Esses métodos tornam possível agregar 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 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 uma enumeração. Agora você pode escrever código como o trecho a seguir para indexar itens automaticamente 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}");
}

Coleções

O tipo de coleção PriorityQueue<TElement,TPriority> no System.Collections.Generic namespace inclui um novo método Remove(TElement, TElement, TPriority, IEqualityComparer<TElement>) que você pode usar para atualizar a prioridade de um item na fila.

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 de prioridade $O(\log n)$ eficientes 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 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 onde o desempenho assintótico não é um bloqueador. (Esses contextos incluem educação e prototipagem.) Por exemplo, aqui está um exemplo de implementação do algoritmo de Dijkstra que usa a nova API.

Criptografia

Para criptografia, o .NET 9 adiciona um novo método de hash de uso único no tipo CryptographicOperations. Ele também adiciona novas classes que usam o algoritmo KMAC.

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 de usar porque podem fornecer o melhor desempenho possível 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 argumento HashAlgorithmName. 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 API CryptographicOperations.HashData. Essa API permite que você produza um hash ou HMAC sobre uma entrada como uso único, em que o algoritmo usado é determinado por um arquivo 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 Mensagens) KECCAK é uma função pseudoaleatória e função 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.
}

Reflexão

Nas versões do .NET Core e do .NET 5-8, o suporte para criar um assembly e emitir metadados de reflexão para tipos criados dinamicamente era limitado a um executável AssemblyBuilder. A falta de suporte para salvar um assembly geralmente era um bloqueador para os clientes que migravam do .NET Framework para o .NET. O .NET 9 adiciona APIs públicas a AssemblyBuilder para salvar um assembly emitido.

A nova implementação AssemblyBuilder persistente é independente de runtime e plataforma. Para criar uma instância de AssemblyBuilder persistente, use a nova API AssemblyBuilder.DefinePersistedAssembly. A API AssemblyBuilder.DefineDynamicAssembly existente aceita o nome do assembly e atributos personalizados opcionais. Para usar a nova API, passe o assembly principal, System.Private.CoreLib, que é usado para fazer referência a tipos base de runtime. Não há opção para AssemblyBuilderAccess. E, por enquanto, a implementação persistente AssemblyBuilder só dá suporte a salvar, não a executar. Depois de criar uma instância do AssemblyBuilder persistente, as etapas subsequentes para definir um módulo, tipo, método ou enum, escrita de IL e todos os outros usos permanecem inalterados. Isso significa que você pode usar o código existente System.Reflection.Emit no estado em que se encontra para salvar o assembly. O código a seguir mostra um exemplo.

public void CreateAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(
        new AssemblyName("MyAssembly"),
        typeof(object).Assembly
        );
    TypeBuilder tb = ab.DefineDynamicModule("MyModule")
        .DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);

    MethodBuilder mb = tb.DefineMethod(
        "SumMethod",
        MethodAttributes.Public | MethodAttributes.Static,
        typeof(int), [typeof(int), typeof(int)]
        );
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    tb.CreateType();
    ab.Save(assemblyPath); // or could save to a Stream
}

public 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]));
}

Desempenho

O .NET 9 inclui aprimoramentos para o compilador JIT de 64 bits que visam melhorar o desempenho do aplicativo. Esses aprimoramentos do compilador incluem:

Vetorização Arm64 é outro novo recurso do runtime.

Otimizações de loop

Melhorar a geração de código para loops é uma prioridade para .NET 9, e o compilador de 64 bits apresenta uma nova otimização chamada Ampliação de váriável de indução (IV).

Uma IV é uma variável cujo valor é alterado conforme a iteração do loop contido nela. No loop for a seguir, i é uma IV: for (int i = 0; i < 10; i++). Se o compilador puder analisar como o valor de um IV evolui em relação às iterações do loop, ele poderá produzir um código com melhor desempenho para expressões relacionadas.

Considere o exemplo a seguir que itera por meio de uma matriz:

static int Sum(int[] arr)
{
    int sum = 0;
    for (int i = 0; i < arr.Length; i++)
    {
        sum += arr[i];
    }

    return sum;
}

A variável de índice, i, tem 4 bytes de tamanho. No nível do assembly, os registros de 64 bits normalmente são usados para armazenar índices de matriz no x64 e, em versões anteriores do .NET, o compilador gerou um código de zero estendido i a 8 bytes para o acesso à matriz, mas continuou a tratar i como um inteiro de 4 bytes em outros lugares. No entanto, estender i para 8 bytes requer uma instrução adicional sobre x64. Com a ampliação do IV, o compilador JIT de 64 bits agora amplia i para 8 bytes em todo o loop, omitindo a extensão zero. O loop sobre matrizes é muito comum, e os benefícios dessa remoção de instrução aumentam rapidamente.

Melhorias inline para AOT Nativo

Um dos objetivos do .NET para o inliner do compilador JIT de 64 bits é remover o máximo possível de restrições que impedem um método de ser inline. O .NET 9 permite inlining de acessos a estática local de thread no Windows x64, Linux x64 e Linux Arm64.

Para membros da classe static, existe exatamente uma instância do membro em todas as instâncias da classe, que “compartilham” o membro. Se o valor de um membro static for exclusivo para cada thread, fazer com que esse valor thread-local pode melhorar o desempenho, pois eliminará a necessidade de um primitivo de simultaneidade acessar com segurança o membro static do thread incluído.

Anteriormente, os acessos à estática local do thread em programas compilados pelo AOT nativo exigiam que o compilador JIT de 64 bits emitisse uma chamada no tempo de execução para obter o endereço base do armazenamento local do thread. Agora, o compilador pode incorporar essas chamadas, resultando em muito menos instruções para acessar esses dados.

Melhorias do PGO: verificações de tipo e conversões

O .NET 8 habilitou a PGO (otimização guiada por perfil dinâmico) por padrão. O NET 9 expande a implementação PGO do compilador JIT de 64 bits para criar mais padrões de código. Quando a compilação em camadas está habilitada, o compilador JIT de 64 bits já insere a instrumentação em seu programa para criar o perfil de seu comportamento. Quando ele recompila com otimizações, o compilador aproveita o perfil criado em tempo de execução para tomar decisões específicas para a execução atual do seu programa. No .NET 9, o compilador JIT de 64 bits usa dados PGO para melhorar o desempenho de verificações de tipo.

Determinar o tipo de um objeto requer uma chamada para o tempo de execução, que vem com uma penalidade de desempenho. Quando o tipo de um objeto precisa ser verificado, o compilador JIT de 64 bits emite essa chamada para fins de correção (os compiladores geralmente não podem descartar nenhuma possibilidade, mesmo que pareçam improváveis). No entanto, se os dados de PGO sugerirem que um objeto provavelmente será um tipo específico, o compilador JIT de 64 bits agora emitirá um caminho rápido que verifica barato esse tipo e volta ao caminho lento de chamar para o runtime somente se necessário.

Vetorização do Arm64 em bibliotecas do .NET

Uma nova implementação de EncodeToUtf8 aproveita a capacidade do compilador JIT de 64 bits de emitir instruções de carregamento/armazenamento de vários registros no Arm64. Esse comportamento permite que os programas processem partes maiores de dados com menos instruções. Os aplicativos .NET em vários domínios devem ver melhorias de taxa de transferência no hardware Arm64 que dá suporte a esses recursos. Alguns parâmetros de comparação reduzem o tempo de execução em mais da metade.

SDK .NET

Teste de unidade

Esta seção descreve as atualizações do teste de unidade no .NET 9: a execução de testes em paralelo e a saída de teste do agente de terminal.

Executar testes em paralelo

No .NET 9, dotnet test é mais totalmente integrado ao MSBuild. Como o MSBuild dá suporte à criação em paralelo, você pode executar testes para o mesmo projeto em diferentes estruturas de destino em paralelo. Por padrão, o MSBuild limita o número de processos paralelos ao número de processadores no computador. Você também pode definir seu próprio limite usando a opção -maxcpucount. Se você quiser recusar o paralelismo, defina a propriedade MSBuild TestTfmsInParallel como false.

Exibição de teste do agente de terminal

O relatório de resultados de teste para dotnet test agora tem suporte diretamente no agente de terminal do MSBuild. Você obtém relatórios de teste mais completos enquanto os testes de estão em execução (exibe o nome do teste em execução) e após a conclusão dos testes de (todos os erros de teste são renderizados de uma maneira melhor).

Para obter mais informações sobre o registrador de terminal, consulte opções de dotnet build.

Roll-forward de ferramenta do .NET

As ferramentas do .NET são aplicativos dependentes da estrutura que podem ser instalados global ou localmente, em seguida, executados usando o SDK do .NET e os tempos de execução do .NET instalados. Essas ferramentas, como todos os aplicativos .NET, têm como destino uma versão principal específica do .NET. Por padrão, os aplicativos não são executados em versões mais recentes do .NET. Os autores de ferramentas foram capazes de aceitar a execução de suas ferramentas em versões mais recentes do runtime do .NET definindo a propriedade RollForward MSBuild. No entanto, nem todas as ferramentas fazem isso.

Uma nova opção para dotnet tool install permite que usuários decidam como as ferramentas do .NET devem ser executadas. Ao instalar uma ferramenta por meio de dotnet tool install, ou ao executar a ferramenta por meio de dotnet tool run <toolname>, você pode especificar um novo sinalizador chamado --allow-roll-forward. Essa opção configura a ferramenta com o modo roll-forward Major. Esse modo permite que a ferramenta seja executada em uma versão principal mais recente do .NET se a versão do .NET correspondente não estiver disponível. Esse recurso ajuda os usuários pioneiros a usarem ferramentas do .NET sem que os autores de ferramentas precisem alterar os códigos.

Confira também