O que vem por aí no C# 11?

O C# 11 é a versão da linguagem que acompanha o .NET 7, lançado em novembro de 2022, e suas primeiras novidades apareceram ainda no começo daquele ano em um preview público. Neste artigo, você vai conferir esses recursos iniciais, aprender a habilitá-los no seu projeto e entender o que mudou entre o anúncio e a versão final.

O que vem por aí no C# 11

Em fevereiro de 2022, Kathleen Dollard, Program Manager do C#, publicou um post com as primeiras novidades do C# 11. O texto apresentava três recursos que já podiam ser testados em modo preview.

São eles: quebras de linha dentro das expressões de interpolação de strings, os list patterns e a checagem de nulos com !! nos parâmetros. Ao longo deste artigo, você vai ver cada um deles com exemplos práticos.

Além disso, vale lembrar como o versionamento funciona. Como o C# é uma linguagem gerenciada pelo .NET, cada versão da linguagem acompanha uma versão da plataforma, e o C# 11 chega junto com o .NET 7.

Se você está começando agora com a linguagem, o artigo sobre o que é C# ajuda a contextualizar o restante do conteúdo.

Como testar o preview do C# 11

Para testar essas funcionalidades, você precisa editar o arquivo csproj e adicionar a chave <LangVersion>preview</LangVersion>. Com isso, o compilador libera os recursos que ainda estão em avaliação.

Segundo o anúncio oficial, o preview exige o Visual Studio 17.1, ou seja, o Visual Studio 2022 Update 1, ou então o .NET SDK 6.0.200 ou superior. Portanto, mesmo um projeto que ainda aponta para o .NET 6 consegue experimentar os recursos.

O exemplo abaixo mostra um projeto de console configurado dessa forma:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <!-- Habilita os recursos em preview do C# 11 -->
        <LangVersion>preview</LangVersion>
    </PropertyGroup>
</Project>

Note que o TargetFramework continua como net6.0. Afinal, o que muda é apenas a versão da linguagem usada pelo compilador.

No entanto, recursos em preview podem mudar ou até sair da versão final. Por isso, evite levar esse tipo de configuração para código de produção.

Vale lembrar também que o design do C# acontece de forma aberta. As propostas e discussões ficam públicas no repositório dotnet/csharplang no GitHub.

Dessa forma, qualquer pessoa pode ler as propostas do C# 11, comentar e acompanhar as decisões do time. Como você verá mais adiante, esse feedback mudou o rumo de um dos recursos anunciados.

Quebra de linha na interpolação de strings no C# 11

No C#, o caractere $ antes de uma string permite a sua interpolação. Já o caractere @ cria uma string literal textual, que aceita quebras de linha.

Veja como essas duas formas aparecem no código:

var interpolated = $"I am a string {value}";
var verbatim = @"I can
                break lines";
var both = $@"I interpolate {values} and
              still break lines";

Note que precisamos usar $@ juntos para obter os dois comportamentos. Até o C# 10, inclusive, as expressões entre chaves de uma string interpolada comum não podiam conter quebras de linha.

No C# 11, essa restrição deixa de existir. Assim, você pode quebrar a expressão dentro das chaves usando apenas o $, como no exemplo do anúncio:

// A expressão entre chaves agora pode ocupar várias linhas
var v = $"Count is: { this.Is.Really.Something()
                            .That.I.Should(
                                be + able)[
                                    to.Wrap()] }.";

Repare que a quebra de linha acontece dentro do trecho entre chaves, e não no texto literal. Em outras palavras, o recurso melhora a legibilidade de expressões longas, sem mudar o resultado da string.

Essa distinção é importante para não confundir os recursos. Se você precisa que o próprio texto tenha várias linhas, continua precisando do @ ou, a partir do C# 11, de um raw string literal.

Por outro lado, se a necessidade é apenas organizar uma chamada longa dentro da interpolação, o $ sozinho resolve. Assim, o código fica mais limpo e você evita o prefixo duplo $@ só para ganhar espaço.

Quando usar expressões em várias linhas

A documentação de interpolação de strings mostra um caso típico: uma expressão switch dentro da interpolação. Nesse cenário, a quebra de linha deixa cada condição visível.

// Cada faixa de pontuação fica em uma linha própria
string message = $"The usage policy for {safetyScore} is {
    safetyScore switch
    {
        > 90 => "Unlimited usage",
        > 80 => "General usage, with daily safety check",
        > 70 => "Issues must be addressed within 1 week",
        _ => "Issues must be addressed before continued use",
    }
    }";

Ainda assim, use o recurso com moderação. Quando a expressão fica grande demais, extrair a lógica para uma variável ou método costuma deixar o código mais claro.

Para se aprofundar no tema, o guia de manipulação de strings em C# e .NET cobre interpolação, concatenação e formatação.

List patterns no C# 11

Outra mudança bem-vinda são os list patterns, ou padrões de listas. Eles lembram a desestruturação do JavaScript, mas usam dois pontos .. em vez de três.

Com eles, você compara um array ou uma lista com uma sequência de padrões. Dessa forma, fica simples verificar o formato de uma coleção sem escrever laços ou acessar índices manualmente.

Como os list patterns funcionam

Cada posição entre colchetes corresponde a um elemento da coleção. Além disso, você pode usar três peças principais:

  • Um valor constante, que exige aquele valor naquela posição.
  • O descarte _, que aceita qualquer elemento único.
  • O slice pattern .., que aceita zero ou mais elementos.

Em seguida, veja um exemplo adaptado do anúncio do C# 11, que combina essas peças em uma expressão switch:

int[] numbers = { 1, 2, 5, 6, 7, 8, 9, 10 };

public static int CheckSwitch(int[] values)
    => values switch
    {
        [1, 2, .., 10] => 1, // Começa com 1 e 2 e termina com 10
        [1, 2] => 2,         // Exatamente dois elementos: 1 e 2
        [1, _] => 3,         // Dois elementos, o primeiro é 1
        [1, ..] => 4,        // Começa com 1, com qualquer tamanho
        [..] => 50           // Qualquer lista, inclusive vazia
    };

Nesse caso, o array numbers cai no primeiro braço, porque começa com 1 e 2 e termina com 10. Portanto, o método retorna 1.

Vale reforçar que o slice pattern pode aparecer no máximo uma vez em cada list pattern. Por isso, um padrão como [.., 5, ..] não compila.

Os list patterns também funcionam com o operador is, e cada posição aceita qualquer outro padrão. Por exemplo, a documentação oficial mostra a combinação com padrões relacionais e lógicos:

int[] numbers = { 1, 2, 3 };

Console.WriteLine(numbers is [1, 2, 3]);             // True
Console.WriteLine(numbers is [1, 2, 4]);             // False
Console.WriteLine(numbers is [1, 2, 3, 4]);          // False
Console.WriteLine(numbers is [0 or 1, <= 2, >= 3]);  // True

O padrão vazio [] também é válido e corresponde a uma coleção sem elementos. Com isso, você consegue tratar o caso de lista vazia no mesmo switch dos demais formatos.

Capturando uma fatia da lista

Também é possível capturar parte da lista. No exemplo abaixo, a variável middle recebe a fatia que fica entre o primeiro e o último elemento.

public static string CaptureSlice(int[] values)
    => values switch
    {
        // Captura os elementos entre o primeiro (1) e o último
        [1, .. var middle, _] => $"Middle {string.Join(", ", middle)}",
        // Captura a lista inteira
        [.. var all] => $"All {string.Join(", ", all)}"
    };

Para a entrada { 1, 2, 3, 4 }, por exemplo, o resultado seria Middle 2, 3. Já uma lista que não começa com 1 cai no segundo braço e devolve todos os valores.

Um uso prático aparece na leitura de argumentos de linha de comando. No exemplo abaixo, baseado na documentação, o método identifica uma opção no início e captura o nome do arquivo no fim:

static string GetInputFile(string[] arguments) =>
    arguments switch
    {
        // Opção no início, arquivo no fim e qualquer coisa no meio
        ["--verbose", .., var fileName] => $"Verbose processing: {fileName}",
        // Qualquer lista não vazia: o último item é o arquivo
        [.., var fileName] => $"Processing: {fileName}",
        // Nenhum argumento informado
        [] => "No input file was provided"
    };

Repare que a ordem dos braços importa. Afinal, o switch escolhe o primeiro padrão compatível, então os formatos mais específicos devem vir antes dos mais genéricos.

Quais tipos aceitam list patterns

Segundo a referência de padrões do C#, o tipo precisa ser contável e indexável. Ou seja, ele deve ter uma propriedade Length ou Count e um indexador.

Na prática, arrays, List<T>, strings e spans funcionam sem nenhum ajuste. Por outro lado, uma variável declarada apenas como IEnumerable<T> não aceita o padrão, mesmo que o objeto real seja uma lista.

Para capturar uma fatia com .. var, o tipo também precisa suportar Range, seja por indexador ou por um método Slice. Felizmente, arrays e strings já atendem a esse requisito.

O que o compilador faz com um list pattern

Entender a tradução ajuda a explicar essas exigências. Segundo a especificação do C#, um padrão como expr is [1, 2, 3] equivale a checar o tamanho e depois cada índice:

// Equivalente conceitual de: expr is [1, 2, 3]
expr.Length is 3
    && expr[new Index(0, fromEnd: false)] is 1
    && expr[new Index(1, fromEnd: false)] is 2
    && expr[new Index(2, fromEnd: false)] is 3

Ou seja, o list pattern não percorre a coleção inteira. Ele consulta o tamanho e acessa apenas as posições citadas no padrão, o que explica a necessidade de Length ou Count e de um indexador.

Por isso, quando a decisão depende de examinar todos os elementos, um laço ou o LINQ continua sendo a escolha adequada.

Se pattern matching ainda é novidade para você, o artigo sobre os operadores as e is no C# apresenta a base desse conceito.

Checagem de nulos no C# 11

A terceira novidade do preview tratava da checagem de nulos em parâmetros. Esse tipo de validação já havia melhorado em versões anteriores com o uso do is null, como mostra o código abaixo:

public static void M(string s)
{
    if (s is null)
    {
        throw new ArgumentNullException(nameof(s));
    }
    // Corpo do método
}

O preview do C# 11 propunha o operador !!, conhecido como double bang. Ele seria colocado após o nome do parâmetro e geraria a checagem automaticamente, dispensando o bloco if.

// Sintaxe proposta no preview, removida antes da versão final
public static void M(string s!!)
{
    // Corpo do método
}

Segundo o anúncio, o time escolheu uma sintaxe em vez de um atributo para manter a assinatura legível. Além disso, a sintaxe permitia gerar o código da checagem em tempo de compilação.

É importante não confundir essa proposta com os tipos de referência anuláveis do C# 8. Eles produzem apenas avisos do compilador, enquanto a checagem com !! lançaria uma exceção em tempo de execução.

Na prática, os dois mecanismos se complementam. O primeiro avisa durante o desenvolvimento, e o segundo protege a API pública contra chamadores que ignoram esses avisos.

O que aconteceu com o operador !!

O operador !! gerou um grande volume de feedback da comunidade, com muitas críticas à sintaxe. Por isso, em abril de 2022, o time anunciou a remoção do recurso do C# 11.

O argumento foi direto: o time não tinha confiança suficiente de que aquele era o design certo para a linguagem. Portanto, quem testou o !! no preview precisou voltar para as alternativas tradicionais.

Esse episódio mostra, na prática, por que recursos em preview não devem ir para produção. Afinal, a fase de preview existe justamente para coletar opiniões antes de fechar o design.

A alternativa recomendada

Para quem usa o .NET 6 ou superior, a Microsoft recomenda o método ArgumentNullException.ThrowIfNull. Ele faz a checagem em uma única linha e inclui o nome do parâmetro na mensagem de erro automaticamente.

public static void M(string s)
{
    // Lança ArgumentNullException com o nome "s" se o valor for nulo
    ArgumentNullException.ThrowIfNull(s);

    // Corpo do método
}

O nome do parâmetro chega à exceção graças ao atributo CallerArgumentExpression. Assim, você ganha a mesma concisão proposta pelo !!, sem nenhuma sintaxe nova.

Para strings, o método ArgumentException.ThrowIfNullOrEmpty vai além e também rejeita valores vazios. Inclusive, a regra de análise CA1511 sugere esses métodos auxiliares no lugar de blocos if.

Segundo a documentação da regra, os métodos auxiliares são mais simples e mais eficientes. Isso acontece porque as checagens manuais aumentam o tamanho do código e dificultam o inlining de métodos pequenos.

Para acompanhar como o tratamento de nulos continuou evoluindo, veja o artigo sobre a atribuição condicional nula no C# 14.

O que entrou na versão final do C# 11

O C# 11 foi lançado em novembro de 2022 junto com o .NET 7. Das três novidades do preview, duas chegaram à versão final: a quebra de linha nas interpolações e os list patterns.

Além delas, o histórico de versões do C# lista outros recursos que entraram no C# 11:

  • Raw string literals, para strings com aspas e várias linhas sem escape.
  • Suporte a generic math, com membros estáticos abstratos em interfaces.
  • Atributos genéricos.
  • Strings literais UTF-8.
  • Tipos locais de arquivo, com o modificador file.
  • Membros obrigatórios, com o modificador required.
  • Structs com inicialização padrão automática.
  • Pattern matching de Span<char> com uma string constante.
  • Campos ref e scoped ref.

Entre eles, os raw string literals também resolvem a dor das strings com várias linhas. Por exemplo, um JSON com aspas pode ser escrito sem nenhuma barra de escape:

// Raw string literal: aspas e quebras de linha sem escape
var json = """
    {
        "name": "balta",
        "language": "C#"
    }
    """;

Outro recurso que ganhou espaço no dia a dia foi o modificador required. Com ele, o compilador obriga quem cria o objeto a preencher determinadas propriedades:

public class Customer
{
    // O compilador exige que Name seja informado na criação
    public required string Name { get; init; }
    public string? Email { get; init; }
}

var customer = new Customer { Name = "Balta" }; // Compila
// var invalid = new Customer();                 // Erro de compilação

Por fim, a grande aposta da versão foi o generic math. Com ele, é possível escrever algoritmos numéricos uma única vez para todos os tipos de número.

Conclusão

O C# vem se modernizando a cada versão, melhorando o desempenho e se aproximando de recursos populares em outras linguagens. O C# 11 seguiu esse caminho com interpolações mais flexíveis e list patterns que deixam a análise de coleções mais expressiva.

Ao mesmo tempo, a remoção do !! mostrou que o processo de preview funciona. Ou seja, a comunidade testa, opina e influencia o design final da linguagem.

Se você quer acompanhar a evolução do C# 11 em diante, habilite o preview em projetos de teste e participe das discussões. Desse modo, você entende as mudanças antes que elas cheguem ao seu código de produção.

FAQ

Qual versão do .NET acompanha o C# 11?

O C# 11 acompanha o .NET 7, lançado em novembro de 2022. Cada versão da linguagem segue uma versão da plataforma.

Como habilitar os recursos em preview do C# 11?

Adicione <LangVersion>preview</LangVersion> ao csproj. Segundo o anúncio, é preciso ter o Visual Studio 17.1 ou o .NET SDK 6.0.200 em diante.

O operador !! existe no C# 11 final?

Não. O time removeu o recurso em abril de 2022 e recomenda usar ArgumentNullException.ThrowIfNull.

Quais coleções aceitam list patterns?

Tipos contáveis e indexáveis, como arrays, List<T>, strings e spans. Uma variável declarada como IEnumerable<T> não aceita o padrão.

Quantas vezes posso usar o slice pattern em um list pattern?

No máximo uma vez. Além disso, ele só pode aparecer diretamente dentro de um list pattern.