.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.
char escapeChar1 = '\u001B'; // Usando a sequencia escape Unicode char escapeChar2 = '\x1B'; // Usando a sequencia escape hexadecimal
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: