.NET 9.0  -  Novidades do C# 13


   Neste artigo vou apresentar as principais novidades do C# 13 que já estão consolidadas com o lançamento da versão do .NET 9.0 SDK.

Para experimentar esses recursos em primeira mão, você precisará da versão mais recente do Visual Studio 2022 ou do .NET 9 SDK. Ambas as opções fornecem acesso às funcionalidades de ponta do C# 13.

1- Coleções de parâmetros aprimoradas

A palavra-chave params era anteriormente restrita a arrays; agora, ela abrange uma gama mais ampla de tipos de coleção.

Você pode usá-la com System.Span<T>, System.ReadOnlySpan<T> e coleções que implementam System.Collections.Generic.IEnumerable<T> e possuem um método Add.

Além disso, interfaces como System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T> e mais podem ser utilizadas com params.

Essa flexibilidade simplifica a passagem de parâmetros para vários cenários :

// Antes
static void ContadorNumeros(params int[] numeros) => Console.WriteLine(numeros.Length);

// .NET 9
static void ContadorNumeros(params ReadOnlySpan<int> numeros) => Console.WriteLine(numeros.Length);
static void ContadorNumeros(params IEnumerable<int> numeros) =>Console.WriteLine(numeros.Count());
static void ContadorNumeros(params HashSet<int> numeros) =>Console.WriteLine(numeros.Count);

Suporte aprimorado para ReadOnlySpan<T>

No C# 13, o suporte para ReadOnlySpan<T> foi aprimorado para permitir que expressões de coleção funcionem diretamente com essa estrutura de alto desempenho.

ReadOnlySpan<T> é uma representação somente leitura segura para tipo e memória de uma região contígua de memória arbitrária. Ela é alocada na pilha e nunca pode escapar para o heap gerenciado, o que ajuda a evitar alocações e melhora o desempenho.

Expressões de coleção agora podem funcionar diretamente com ReadOnlySpan<T>, uma estrutura de alto desempenho que evita alocações. Esse aprimoramento é particularmente benéfico para aplicativos que exigem desempenho ideal.

Benefícios do ReadOnlySpan<T>:

Evita alocações: Como o ReadOnlySpan<T> é uma estrutura alocada na pilha, ele evita alocações de heap, o que pode beneficiar aplicativos de desempenho crítico.

Segurança da memória: Ele fornece uma maneira segura de manipular a memória, garantindo que você não modifique acidentalmente os dados subjacentes.

Versatilidade: O ReadOnlySpan<T> pode apontar para memória gerenciada, memória nativa ou memória gerenciada na pilha, tornando-o versátil para vários cenários.

Considere um cenário em que você precisa inicializar uma coleção de forma eficiente.

public void AddScores(params int[] scores)
{
    var scoresCollection = new int[] { 75, 88, 92 };
    AddScores(scoresCollection);
}

No C# 13, você pode conseguir isso com melhor desempenho usando ReadOnlySpan<T>.

public void AddScores(ReadOnlySpan<int> scores)
{
    foreach (var score in scores)
    {
        // Processa o placar sem alocações
    }
}

Isso é útil para aplicativos que precisam de desempenho ideal, como sistemas em tempo real, desenvolvimento de jogos e aplicativos de negociação de alta frequência.

Suporte aprimorado para IEnumerable<T>

A palavra-chave params foi aprimorada para funcionar com IEnumerable<T>, permitindo que você passe coleções diretamente para métodos que aceitam um número variável de argumentos. Esse aprimoramento melhora a flexibilidade e a usabilidade da palavra-chave params.

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        // usando params com IEnumerable<T>.
        AddItems(new List<int> { 1, 2, 3, 4, 5 });
    }

    // Método aceitando params com IEnumerable<T>.
    public static void AddItems(params IEnumerable<int>[] collections)
    {
        foreach (var collection in collections)
        {
            foreach (var item in collection)
            {
                Console.WriteLine(item);
            }
        }
    }
}

Essa prática melhora o desempenho, especialmente em ambientes que exigem alta taxa de requisições, como ASP.NET Core.

2- Sincronização de thread modernizada com lock

O C# 13 apresenta o tipo System.Threading.Lock, projetado para melhorar as práticas de sincronização de thread. Ele ostenta uma API superior em comparação à abordagem tradicional System.Threading.Monitor.

Principais recursos

Escopo de execução exclusivo: o método Lock.EnterScope() estabelece um escopo de execução exclusivo. Isso garante que apenas uma thread execute o código dentro do escopo por vez.

Padrão Dispose of: a estrutura ref retornada de Lock.EnterScope() oferece suporte ao padrão Dispose(), permitindo uma saída elegante do escopo. Isso garante que o bloqueio seja liberado mesmo se ocorrer uma exceção.

Integração com a instrução lock: a instrução lock do C# agora reconhece quando o alvo é um objeto de bloqueio e usa a API atualizada. Essa integração simplifica o código e melhora a segurança da thread.

Benefícios

Segurança de thread aprimorada: ao usar o tipo System.Threading.Lock, os desenvolvedores podem obter melhor sincronização e evitar armadilhas comuns associadas à contenção de thread.

Manutenção do código: a nova API torna o código mais legível e fácil de manter, reduzindo a complexidade da sincronização de threads.

Exemplo:

// Antes
public class LockExemplo
{
  private readonly object _lock = new();

  public void DoStuff() 
  {
    lock (_lock)
    {
        Console.WriteLine("Estamos usando o lock antigo);
    } 
  }
}
// .NET 9 com C# 13
public class LockExemplo 
{
  private readonly Lock _lock = new();

  public void DoStuff() 
  {
    lock (_lock)
    {
        Console.WriteLine("Estamos usando o lock do .NET 9");
    } 
  }
}

3- Propriedades automáticas com lógica personalizada

As propriedades automáticas no C# 13 fornecem uma maneira mais simplificada de declarar propriedades sem campos de apoio explícitos.

No entanto, se você quisesse adicionar lógica personalizada ao getter ou setter, teria que usar a sintaxe de propriedade completa, o que resultava em código boilerplate adicional.

O C# 13 introduziu melhorias que permitem que a lógica personalizada seja incluída diretamente nos getters e setters das propriedades automáticas, reduzindo assim a necessidade da sintaxe de propriedade completa e mantendo o código mais conciso.

Vamos explicar isso com o exemplo de código a seguir.

Considere um cenário em que você deseja garantir que uma propriedade de data seja sempre definida para a data atual se o valor fornecido estiver no passado.

public class Program
{
    public static void Main()
    {
        Evento meuEvento = new Evento();
        // Definindo a data passada
        meuEvento.EventoData = new DateTime(2020, 1, 1);
        Console.WriteLine(meuEvento.EventoData); // Exibe a data atual
        // Definindo a data futura
        meuEvento.EventoData = new DateTime(2025, 1, 1);
        Console.WriteLine(meuEvento.EventoData); // Exibe 2025-01-01
    }
}
public class Evento
{
    private DateTime eventoData;
    public DateTime EventoData
    {
        get => eventoData;
        set => eventoData = value < DateTime.Now ? DateTime.Now : value;
    }
}

Com o recurso das propriedades automática aprimoradas, agora você pode incorporar lógica personalizada diretamente na própria definição de propriedade, minimizando a necessidade de campos de apoio e garantindo que seu código permaneça conciso e inteligível.

Atualmente para testar este recurso usando o Visual Studio 2022 no ambiente do .NET 9.0 você tem que definir no arquivo de projeto (.csproj)  a propriedade <LangVersion>Preview<LangVersion>

4-  Nova sequência de escape para o caractere ESCAPE

O C# 13 introduz uma maneira mais conveniente de representar o caractere ESCAPE (Unicode U+001B) dentro de literais de caracteres.

Este novo recurso permite que os desenvolvedores usem a sequência de escape \e em vez dos métodos mais antigos, \u001B ou \x1B. Este aprimoramento simplifica a legibilidade do código e reduz erros potenciais associados a interpretações hexadecimais.

Antes do C# 13, representar o caractere ESCAPE exigia usar a sequência de escape Unicode \u001B ou a sequência de escape hexadecimal \x1B. Esses métodos poderiam ser menos legíveis e mais propensos a erros, especialmente se os caracteres a seguir fossem dígitos hexadecimais válidos.

//Antes
char escapeChar1 = '\u001B'; // Usando a sequencia escape Unicode
char escapeChar2 = '\x1B';   // Usando a sequencia escape hexadecimal
//.NET 9.0
char escapeChar = '\e'; //Usando a nova sequencia escape
 

Benefícios :

Melhor legibilidade: A sequência de escape \e é mais concisa e fácil de ler em comparação com \u001B ou \x1B.

Erros reduzidos: Usar \e minimiza o risco de erros que podem ocorrer com interpretações hexadecimais, onde caracteres subsequentes podem ser mal interpretados como parte da sequência de escape.

Consistência: A nova sequência de escape se alinha com outras sequências de escape comuns, tornando o código mais consistente e fácil de entender.

5- Operador de índice implícito em inicializadores de objetos

O C# 13 permite usar o operador de índice implícito "do fim" (^) dentro de expressões de inicializador de objetos. Isso permite que você inicialize matrizes diretamente do fim, ou seja, agora você pode usar ^ para especificar uma posição em uma coleção que seja relativa ao fim da coleção.

Podemos usar a sintaxe : ^2, ^3 e, de forma geral, ^n no operador de índice implícito do C# para acessar elementos a partir do final de uma coleção.

O que cada um significa:

^2: Acessa o segundo elemento a partir do final da coleção.
^3: Acessa o terceiro elemento a partir do final da coleção.
^n: Acessa o n-ésimo elemento a partir do final da coleção, onde n é um número inteiro positivo.

Por exemplo, considere o seguinte exemplo:

var arr = new InitializerDemo
{
    Inteiros =
    {
       [0] = 100,
       [^1] = 1000
    }
};
Console.WriteLine("O valor de arr.Inteiros[0] é {0} e arr.Inteiros[4] é {1}", arr.Inteiros[0], arr.Inteiros[4]);
class InitializerDemo
{
    public int[] Inteiros { get; set; } = new int[5];
}

Ao executar o programa acima, arr.Inteiros[0] terá o valor 100, enquanto arr.Inteiros[4] terá o valor 1000.

Limitações e Considerações

Índices negativos: O índice após o ^ deve ser um número inteiro não negativo. Índices negativos não são permitidos.

Índice maior que o tamanho da coleção: Se o índice especificado for maior que o tamanho da coleção, uma exceção ArgumentOutOfRangeException será lançada.

Desempenho: Embora o operador de índice implícito seja conveniente, em alguns casos, o cálculo do índice a partir do final pode ter um pequeno impacto no desempenho, especialmente em coleções muito grandes. No entanto, para a maioria dos casos, essa diferença é insignificante.

Assim, agora o C# 13 gora permite usar o operador de índice implícito “from the end” (^) dentro de expressões inicializadoras de objetos. Isso permite que você inicialize arrays diretamente do fim conforme o exemplo acima.

6- Propriedades parciais

Uma propriedade parcial é uma propriedade cuja implementação pode ser declarada em múltiplos locais dentro de uma classe parcial. Para que uma propriedade seja considerada parcial, ela precisa estar dentro de uma classe que também seja declarada como parcial usando o modificador partial.

public partial class Produto
{
    public int Id { get; set; }
    public string Nome { get; set; }
    public decimal Preco { get; set; }
    // Propriedade parcial e virtual
    public partial decimal ValorComImposto { get; }
}

A segunda parte desta declaração poderia se feita em um arquivo com nome distinto, por exemplo Produto.Imposto.cs:

// Produto.Imposto.cs (Parte 2)
public partial class Produto.Imposto
{
    private decimal _taxaImposto = 0.18m; // 18% de imposto
    public partial decimal ValorComImposto
    {
        get
        {
            return Preco * (1 + _taxaImposto);
        }
    }
}

O arquivo Produto.cs,  contém as propriedades principais e métodos gerais, e o arquivo Produto.Impostos.cs,  contém a lógica relacionada a impostos definida na propriedade parcial.

Note que estamos usando nomes distintos de arquivos pois isso ajudam a definir o proposito de cada parte e também ajuda na manutenção. A propriedade _taxaImposto é privada e, portanto, só é acessível dentro da classe Produto.

Isso encapsula a implementação do cálculo do imposto e evita que ela seja modificada acidentalmente de fora da classe.

am geradores de fonte e são usados ​​para separar a declaração de uma propriedade de seu código de implementação. Observe que métodos parciais foram introduzidos anteriormente e ganharam alguma força no C# 9.

Você pode declarar propriedades parciais e indexadores parciais no C# 13.

As propriedades parciais e indexadores geralmente seguem as mesmas regras que métodos parciais: você cria uma declaração de declaração e uma declaração de implementação. As assinaturas nas duas declarações das propriedades parciais devem corresponder.

Como restrição temos que as propriedades parciais não podem ser abstractas ou estaticas:

Uma propriedade abstrata não possui uma implementação concreta na classe base e deve ser implementada nas classes derivadas. Propriedades parciais, por sua natureza, geralmente exigem uma implementação concreta.

Uma propriedade estática pertence à classe e não a um objeto específico. Propriedades parciais, por outro lado, são geralmente associadas a instâncias de objetos.

restrição é que você não pode usar uma declaração de propriedade automática para uma propriedade parcial.

As propriedades que não declaram um corpo são consideradas a declaração de declaração.

partial class MinhaClasse
{
    string meuCampo;

    public partial string MinhaPropriedade { get; set; }

    public partial string MinhaPropriedade
    { 
        get => meuCampo; 
        set => meuCampo = value; 
    }
}

Deve-se notar que quando você declara uma propriedade parcial com acessadores tendo corpos de ponto e vírgula sem nenhum código de implementação dentro, presume-se que seja uma declaração de definição.

Por outro lado, quando uma propriedade parcial tem acessadores que contêm código de implementação, ela é considerada uma declaração de implementação.

As Propriedades parciais são usadas para fornecer suporte para geradores de fonte. Seu propósito é isolar a declaração de uma propriedade de seu código de implementação.

Para explorar os novos recursos do C# 13, você deve baixar a versão mais recente do Visual Studio 2022 com .NET 9.

E estamos conversados...

"Ensina-me, Senhor, o teu caminho, e andarei na tua verdade; une o meu coração ao temor do teu nome."
Salmos 86:11

Referências:


José Carlos Macoratti