C# Novidades do C# 8 ao 14

O C# ganha uma versão nova todo ano, em novembro, junto com o .NET. Muito do que este guia ensina como "o jeito normal" (dotnet run ola.cs sem class Program, [1, 2, 3], record, is not null) não existia há poucos anos, e você ainda vai encontrar muito código no estilo antigo. Esta aula é um mapa: o que entrou em cada versão, do C# 8 (2019) ao C# 14 (2025), onde cada coisa é ensinada no guia e, com mais detalhe, as novidades do C# 14, conferidas uma a uma no .NET 10 da máquina da Ana.

Você vai aprender
  • A relação entre a versão do C# e a do .NET, e o que é o LangVersion (erro CS9260).
  • A tabela das novidades do C# 8 ao 13, com um programa que usa várias delas.
  • O C# 14 na prática: a palavra field, os blocos extension, a atribuição condicional nula ?.=, nameof(List<>), lambdas com out e ref sem tipos, construtores partial, operadores += e spans de primeira classe.
  • As armadilhas: um campo chamado field (CS9258) e o ?. com ++ (CS1059).

Esta aula passa por muitos assuntos do guia e aponta a aula de cada um. Vale a pena voltar a ela depois de terminar o Nível 3. Os exemplos rodaram com dotnet run no .NET 10 (dotnet --version = 10.0.112) da máquina da Ana, na pasta ~/cs.


A ideia antes do código: linguagem × plataforma

Lembre a diferença entre o C# (a linguagem: as palavras e regras que você escreve) e o .NET (a plataforma: o compilador, as bibliotecas e o runtime que roda o programa), da aula .NET: SDK, Runtime e CLR. Cada .NET vem com um compilador que entende uma versão do C#, e cada projeto escolhe a versão da linguagem pelo .NET de destino:

C#.NETLançamentoSuporte
C# 8.NET Core 3.0 / 3.12019encerrado
C# 9.NET 5nov. 2020encerrado
C# 10.NET 6 (LTS)nov. 2021encerrado
C# 11.NET 7nov. 2022encerrado
C# 12.NET 8 (LTS)nov. 2023até nov. 2026
C# 13.NET 9nov. 2024até nov. 2026
C# 14.NET 10 (LTS)nov. 2025até nov. 2028

LTS (Long Term Support, "suporte longo") são as versões de 3 anos de suporte, as preferidas das empresas. A aula Versões do C# e do .NET conta essa história com mais calma. O que importa aqui: no .NET 10, o padrão é o C# 14, e um projeto pode pedir uma versão mais antiga da linguagem com <LangVersion> no .csproj. Se o código usar algo mais novo que a versão pedida, o compilador avisa exatamente o quê:

$ dotnet run
/home/ana/projetos/Velho/Program.cs(8,19): error CS9260: O recurso "campo palavra-chave" não está disponível no C# 13.0. Use a versão da linguagem 14.0 ou superior. [/home/ana/projetos/Velho/Velho.csproj]
/home/ana/projetos/Velho/Program.cs(14,20): error CS9260: O recurso "campo palavra-chave" não está disponível no C# 13.0. Use a versão da linguagem 14.0 ou superior. [/home/ana/projetos/Velho/Velho.csproj]

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Esse projeto tinha <LangVersion>13</LangVersion> e usava a palavra field (seção 3). "Campo palavra-chave" é a tradução, meio torta, de field keyword. O mesmo CS9260 aparece para cada recurso: "atribuição condicional nula", "listas de padrões"… É o jeito mais rápido de descobrir de que versão um recurso é.


1. A tabela: do C# 8 ao 13

As novidades mais importantes de cada versão, e a aula do guia que ensina cada uma:

VersãoRecursoExemploAula
C# 8Referências anuláveisstring? nomeReferências Anuláveis
Expressões switchx switch { 1 => "um", _ => "?" }Expressões switch
Índices e intervalosa[^1], a[1..3]Índices e Intervalos
using sem chavesusing var arquivo = …;using e IDisposable
??=nome ??= "Ana";?? e ??=
Padrões de propriedade e posicionaisp is { Idade: 18 }Pattern Matching
C# 9Instruções de nível superiorsem class Program nem MainProgram.cs e o Main
Recordsrecord Ponto(int X, int Y);Records
initpublic string Nome { get; init; }readonly e init
new sem o tipoProduto p = new("Caderno");Construtores
Padrões relacionais e lógicosn is >= 0 and <= 10, is not nullPattern Matching
C# 10global using e using implícitosglobal using System.Text;Namespaces e using
Namespace de arquivonamespace Loja;Namespaces e using
record structrecord struct Cor(byte R, byte G, byte B);Records
[CallerArgumentExpression]o texto do argumentoAtributos
C# 11Strings brutas (raw strings)"""texto com "aspas" """Escapes e Strings Literais
Padrões de listal is [1, .., var ultimo]Pattern Matching
requiredpublic required string Nome { get; init; }Inicializadores e required
Matemática genéricaT Somar<T>(T a, T b) where T : INumber<T>Restrições (where)
Atributos genéricos[Validar<int>]Atributos
C# 12Expressões de coleçãoint[] a = [1, 2, 3];, [..a, 4]Arrays, List
Construtores primários em classesclass Caixa(string operadora) { … }Construtores
Valores padrão em lambdas(int x = 1) => x * 2Lambdas
C# 13params com coleçõesparams ReadOnlySpan<int> valoresparams
O tipo Lockprivate readonly Lock _trava = new();Sincronização (lock)
Propriedades partial[GeneratedRegex(…)] static partial Regex R { get; }Regex, partial
Escape \eo caractere ESC, para cores no terminalEscapes

Um programa, várias versões

Este programa da lojinha usa um recurso de quase cada versão. Os comentários dizem de qual:

// C# 9: instruções de nível superior (sem class Program nem Main)
List<Produto> estoque = [new("Caderno", 15.90m), new("Caneta", 2.50m)];   // C# 12: [ ]

string? aviso = null;
aviso ??= "sem avisos";                                // C# 8: ??=
Console.WriteLine(aviso);

foreach (var p in estoque)
{
    string faixa = p.Preco switch                       // C# 8: expressão switch
    {
        < 5 => "barato",                                // C# 9: padrões relacionais
        _ => "normal",
    };
    Console.WriteLine($"{p.Nome}: {faixa}");
}

Console.WriteLine(estoque is [var primeiro, ..] ? $"Primeiro: {primeiro.Nome}" : "vazio");   // C# 11

string json = """
    { "loja": "Ana", "aberta": true }
    """;                                                // C# 11: raw string
Console.WriteLine(json);

Caixa caixa = new("Ana");                               // C# 12: construtor primário
Console.WriteLine(caixa.Somar(10m, 5.5m, 2m));          // C# 13: params com coleções

record Produto(string Nome, decimal Preco);             // C# 9: record

class Caixa(string operadora)
{
    public string Somar(params ReadOnlySpan<decimal> valores)
    {
        decimal total = 0;
        foreach (var v in valores) total += v;
        return $"{operadora} somou {total}";
    }
}
$ dotnet run tour.cs
sem avisos
Caderno: normal
Caneta: barato
Primeiro: Caderno
{ "loja": "Ana", "aberta": true }
Ana somou 17,5

No C# 7, esse mesmo programa precisaria de class Program e static void Main, de new List<Produto> { new Produto("Caderno", 15.90m), … }, de uma classe Produto inteira com construtor e propriedades, de um if no lugar do ??=, de um switch com case e break, de \" no JSON… A linguagem ficou bem mais enxuta.


2. O C# 14 em uma tabela C# 14

RecursoEm uma linhaOnde
A palavra fieldAcessar o campo escondido de uma propriedade automática, para validar no setSeção 3
Blocos extensionPropriedades de extensão e membros estáticos de extensão, além de métodosSeção 4
Atribuição condicional nulacliente?.Pontos = 10; só grava se cliente não for nuloSeção 5
nameof com genérico abertonameof(List<>) dá "List"Seção 6
Lambdas com modificadores, sem tipos(texto, out numero) => …Seção 6
Construtores e eventos partialDeclarar num arquivo, implementar em outroSeção 6
+=, ++ definidos pelo usuáriopublic void operator +=(int n)Sobrecarga de Operadores
Spans de primeira classeArray vira ReadOnlySpan sozinho, até em métodos de extensãoSpan<T>

3. A palavra field C# 14

Uma propriedade automática ({ get; set; }) tem um campo escondido, que o compilador cria. Até o C# 13, se você quisesse validar o valor no set, precisava desistir da propriedade automática e escrever o campo à mão (private decimal _preco;). No C# 14, a palavra field ("campo") é esse campo escondido, e você pode usá-la dentro do get ou do set:

Produto p = new() { Nome = "  caderno  ", Preco = -5 };
Console.WriteLine($"[{p.Nome}] custa {p.Preco}");
p.Preco = 15.90m;
Console.WriteLine($"[{p.Nome}] custa {p.Preco}");

class Produto
{
    public string Nome
    {
        get;
        set => field = value.Trim();
    } = "";

    public decimal Preco
    {
        get;
        set => field = value < 0 ? 0 : value;
    }
}
$ dotnet run field.cs
[caderno] custa 0
[caderno] custa 15,90
get;
O get continua automático: devolve o campo escondido.
set => field = value.Trim();
O set tem corpo: tira os espaços das pontas e grava no campo escondido, que agora tem nome: field.
} = "";
O valor inicial vai para o mesmo campo.
value < 0 ? 0 : value
Preço negativo vira 0. Por isso o -5 do inicializador virou 0.

Antes, seriam 6 linhas a mais: private string _nome = "";, get => _nome;, set => _nome = value.Trim();, e o mesmo para o preço. E o campo _nome ficaria visível para o resto da classe, que poderia gravar nele sem passar pela validação. O field só existe dentro da propriedade.

Missão: uma nota precisa ficar entre 0 e 10. Troque o set; por um set que use field e Math.Clamp(value, 0, 10) ("prender" o valor na faixa). A saída deve ser Nota: 10, Nota: 0 e Nota: 7.

Aluno a = new();
a.Nota = 15;
Console.WriteLine($"Nota: {a.Nota}");
a.Nota = -3;
Console.WriteLine($"Nota: {a.Nota}");
a.Nota = 7;
Console.WriteLine($"Nota: {a.Nota}");

class Aluno
{
    public int Nota { get; set; }
}
Aluno a = new();
a.Nota = 15;
Console.WriteLine($"Nota: {a.Nota}");
a.Nota = -3;
Console.WriteLine($"Nota: {a.Nota}");
a.Nota = 7;
Console.WriteLine($"Nota: {a.Nota}");

class Aluno
{
    public int Nota { get; set => field = Math.Clamp(value, 0, 10); }
}

4. Blocos extension C# 14

Os métodos de extensão (com this no primeiro parâmetro) existem desde o C# 3, mas só serviam para métodos. O C# 14 trouxe uma forma nova: um bloco extension(tipo nome) { … } dentro de uma classe estática, onde cabem também propriedades de extensão e membros estáticos de extensão:

string frase = "a lojinha da Ana";
Console.WriteLine(frase.Palavras);
Console.WriteLine(frase.Iniciais());
Console.WriteLine(string.Repetir("-", 10));
int[] vazio = [];
Console.WriteLine(vazio.EstaVazia);

static class ExtensoesDeTexto
{
    extension(string s)
    {
        public int Palavras => s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;

        public string Iniciais() => string.Concat(s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(p => char.ToUpper(p[0])));

        public static string Repetir(string pedaco, int vezes) => string.Concat(Enumerable.Repeat(pedaco, vezes));
    }

    extension<T>(IEnumerable<T> itens)
    {
        public bool EstaVazia => !itens.Any();
    }
}
$ dotnet run extensao.cs
4
ALDA
----------
True
TrechoO que é
extension(string s) { … }"Tudo aqui dentro estende string; dentro dos membros, o texto se chama s." O s faz o papel do antigo this string s.
public int Palavras => …Uma propriedade de extensão: usa-se frase.Palavras, sem parênteses. Antes do C# 14, isso não existia.
public string Iniciais() => …Um método de extensão comum, no estilo novo. O jeito antigo (this string s) continua valendo, e os dois convivem.
public static string Repetir(…)Um membro estático de extensão: aparece como se fosse do próprio tipo, string.Repetir("-", 10).
extension<T>(IEnumerable<T> itens)Um bloco genérico: vale para qualquer coleção, como o int[].

Missão: crie, dentro de Extensoes, um bloco extension(string s) com a propriedade public int Palavras => s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length; e tire os //. A saída deve ser 4 palavras e 2 palavras.

string a = "a lojinha da Ana";
string b = "  caderno   azul ";
// Console.WriteLine($"{a.Palavras} palavras");
// Console.WriteLine($"{b.Palavras} palavras");
Console.WriteLine(a);

static class Extensoes
{
    // escreva o bloco extension aqui
}
string a = "a lojinha da Ana";
string b = "  caderno   azul ";
Console.WriteLine($"{a.Palavras} palavras");
Console.WriteLine($"{b.Palavras} palavras");
Console.WriteLine(a);

static class Extensoes
{
    extension(string s)
    {
        public int Palavras => s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

5. Atribuição condicional nula: ?.= C# 14

O operador ?. (veja ?. e ??) já servia para ler sem medo de null: cliente?.Nome dá null se cliente for nulo, em vez de lançar exceção. No C# 14, ele também serve para gravar: cliente?.Pontos = 10; grava se cliente existir e não faz nada se ele for nulo.

Cliente? ana = new() { Nome = "Ana" };
Cliente? ninguem = null;

ana?.Pontos = 10;
ninguem?.Pontos = 10;
ana?.Pontos += 5;
ninguem?.Pontos += 5;

Console.WriteLine(ana?.Pontos);
Console.WriteLine(ninguem?.Pontos is null ? "ninguém, nada aconteceu" : "?");

int[]? notas = [7, 8];
notas?[0] = 10;
Console.WriteLine(notas?[0]);

class Cliente
{
    public string Nome { get; set; } = "";
    public int Pontos { get; set; }
}
$ dotnet run nulo.cs
15
ninguém, nada aconteceu
10
ana?.Pontos = 10;
"Se ana não for nulo, grave 10 em Pontos." Antes: if (ana is not null) ana.Pontos = 10;.
ninguem?.Pontos = 10;
ninguem é null: a linha não faz nada, e não há NullReferenceException.
ana?.Pontos += 5;
Também vale com +=, -= e companhia: 10 + 5 = 15.
notas?[0] = 10;
E com colchetes: o ?[ ] grava no array só se ele existir.

Um detalhe importante: o lado direito só é calculado se o objeto não for nulo. Em ninguem?.Pontos = CalcularPontos();, o método CalcularPontos nem é chamado.


6. As outras novidades do C# 14

nameof com genéricos abertos

O nameof devolve o nome de algo como texto, conferido pelo compilador. Até o C# 13, para um tipo genérico era preciso inventar um tipo qualquer (nameof(List<int>)). Agora dá para deixar os <> vazios:

Console.WriteLine(nameof(List<>));
Console.WriteLine(nameof(Dictionary<,>));
Console.WriteLine(nameof(List<>.Count));
Console.WriteLine(nameof(List<int>));
$ dotnet run nomes.cs
List
Dictionary
Count
List

Repare que o resultado é só o nome, sem os <>, nas duas formas. A vírgula em Dictionary<,> diz "dois parâmetros de tipo".

Parâmetros de lambda com out e ref, sem tipos

Numa lambda, o C# descobre os tipos dos parâmetros pelo delegate. Mas, se um parâmetro tinha out ou ref, era obrigatório escrever todos os tipos: (string texto, out int numero) => …. No C# 14, o modificador basta:

Conversor conv = (texto, out numero) => int.TryParse(texto, out numero);

foreach (var t in new[] { "42", "abc" })
{
    bool ok = conv(t, out int n);
    Console.WriteLine($"{t}: {(ok ? n : "inválido")}");
}

Dobrar dobrar = (ref x) => x *= 2;
int v = 21;
dobrar(ref v);
Console.WriteLine(v);

delegate bool Conversor(string texto, out int numero);
delegate void Dobrar(ref int x);
$ dotnet run lambda.cs
42: 42
abc: inválido
42

O delegate Conversor já diz que texto é string e numero é out int; a lambda só repete o out. O ref x da segunda lambda dobra a variável original (veja ref, out e in): 21 virou 42.

Construtores e eventos partial

As classes parciais podem ter partes em arquivos diferentes, e desde o C# 13 propriedades também podem ser declaradas numa parte e implementadas em outra. O C# 14 estendeu isso a construtores e eventos. É um recurso pensado para geradores de código, como o do [GeneratedRegex]:

Conta c = new(100m);
Console.WriteLine(c.Saldo);

partial class Conta
{
    public decimal Saldo { get; }
    public partial Conta(decimal inicial);
}

partial class Conta
{
    public partial Conta(decimal inicial)
    {
        Saldo = inicial;
    }
}
$ dotnet run parcial.cs
100

Operadores += e spans de primeira classe

Os dois têm aula própria: o public void operator +=(…) de instância está em Sobrecarga de Operadores (seção 5), e a conversão automática de array para ReadOnlySpan em métodos de extensão está em Span<T> (seção 3), com o erro CS1929 que o mesmo código dá no C# 13.

No editor do guia

O editor desta página compila com o C# 14 de verdade: todos os exemplos desta aula foram conferidos no testador do guia e deram a mesma saída que o dotnet 10 do laboratório.


Qual usar?

SituaçãoRecomendação
Projeto novo no .NET 10Use o C# 14 à vontade: é o padrão e tem suporte até 2028.
Projeto que ainda roda no .NET 8Ele usa C# 12: nada de field, extension, ?.=, params ReadOnlySpan. Cada versão do C# só tem suporte oficial no .NET que a trouxe; para usar o C# 14, atualize o projeto para o .NET 10 (o .NET 8 sai de suporte em novembro de 2026).
Validar uma propriedadefield no set, em vez de um campo _nome à mão.
Uma "propriedade" a mais num tipo que não é seuBloco extension com uma propriedade.
Gravar num objeto que pode ser nuloobj?.Prop = valor; (mas pense se o null ali não é um erro que deveria aparecer).
Ler código antigoUse a tabela da seção 1 para reconhecer a forma nova de cada coisa.

Quando dá errado

error CS9260: recurso novo numa versão antiga

Mensagem: O recurso "…" não está disponível no C# 13.0. Use a versão da linguagem 14.0 ou superior. (veja "A ideia antes do código"). Como consertar: tire o <LangVersion> do .csproj (o padrão do .NET 10 é o C# 14) ou, se o projeto precisa mesmo da versão antiga, escreva do jeito antigo. Atenção: os blocos extension numa versão antiga nem chegam a dar CS9260; o compilador não reconhece a sintaxe e mostra erros confusos, como CS1513: } esperada.

Uma classe que já tinha um campo chamado field

Caixa c = new();
c.Valor = 7;
Console.WriteLine(c.Valor);
Console.WriteLine(c.CampoAntigo);

class Caixa
{
    private int field;
    public int CampoAntigo => this.field;
    public int Valor
    {
        get => field;
        set => field = value;
    }
}
$ dotnet run campo.cs
/home/ana/cs/campo.cs(12,16): warning CS9258: Na versão da linguagem 14.0, a palavra-chave ''field'' é associada a um campo de suporte sintetizado para a propriedade. Para evitar a geração de um campo de suporte sintetizado e para se referir ao membro existente, use 'this.field' ou '@field'.
/home/ana/cs/campo.cs(13,16): warning CS9258: Na versão da linguagem 14.0, a palavra-chave ''field'' é associada a um campo de suporte sintetizado para a propriedade. Para evitar a geração de um campo de suporte sintetizado e para se referir ao membro existente, use 'this.field' ou '@field'.
/home/ana/cs/campo.cs(8,17): warning CS0649: Campo "Caixa.field" nunca é atribuído e sempre terá seu valor padrão 0
7
0

O que quer dizer: num código escrito antes do C# 14, field era um nome comum. Agora, dentro de uma propriedade, ele é a palavra nova: o Valor passou a usar um campo escondido, e o campo field da linha 8 ficou esquecido (por isso CampoAntigo mostrou 0). O compilador avisa com CS9258. Como consertar: se você quer o campo antigo, escreva this.field ou @field dentro da propriedade, como a mensagem sugere; melhor ainda, renomeie o campo para _valor.

?. com ++

Este código tem um erro de propósito.
Cliente? ana = new();
ana?.Pontos++;
class Cliente { public int Pontos { get; set; } }
$ dotnet run mais.cs
/home/ana/cs/mais.cs(2,1): error CS1059: O operando de aumento ou diminuição deve ser uma variável, propriedade ou indexador

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: a atribuição condicional nula aceita =, +=, -= e afins, mas não ++ e --. Como consertar: ana?.Pontos += 1;.


Referência rápida: recurso → versão

RecursoVersão
string?, switch expressão, ^1 e .., ??=, using varC# 8
Sem Main, record, init, new(), is not null, and/orC# 9
global using, namespace X;, record struct, { A.B: … }C# 10
"""…""", [1, ..] em padrões, required, INumber<T>C# 11
[1, 2, 3], class X(int a), lambdas com valor padrãoC# 12
params ReadOnlySpan<T>, Lock, propriedades partial, \eC# 13
field, extension(…) { }, ?.=, nameof(List<>), (x, out y) =>, construtores partial, operator +=, spans de primeira classeC# 14

Exercícios

1. Complete o set para gravar no campo escondido da propriedade:

public string Nome { get; set => = value.Trim(); }

2. Qual operador grava só se o objeto não for nulo?

clientePontos = 10;

3. Qual versão do C# vem com o .NET 10?

C#

4. O que mostra Console.WriteLine(nameof(Dictionary<,>)); no C# 14?

5. Num bloco extension(string s) { public int Palavras => …; }, como se usa a propriedade?

6. Um projeto com <LangVersion>13</LangVersion> usa cliente?.Pontos = 10;. O que acontece?

Missão final: a promoção da loja dá 10 pontos a cada cliente da fila, mas algumas posições da fila estão vazias (null). Troque o if por uma atribuição condicional nula: c?.Pontos += 10;. A saída não muda (Ana: 15 pontos, Bruno: 20 pontos, ninguém mais), mas o código fica com uma linha só.

Cliente?[] fila = [new("Ana", 5), null, new("Bruno", 10), null];

foreach (var c in fila)
{
    if (c is not null)
    {
        c.Pontos = c.Pontos + 10;
    }
}

foreach (var c in fila)
{
    if (c is not null) Console.WriteLine($"{c.Nome}: {c.Pontos} pontos");
}
Console.WriteLine("ninguém mais");

class Cliente(string nome, int pontos)
{
    public string Nome { get; } = nome;
    public int Pontos { get; set; } = pontos;
}
Cliente?[] fila = [new("Ana", 5), null, new("Bruno", 10), null];

foreach (var c in fila)
{
    c?.Pontos += 10;
}

foreach (var c in fila)
{
    if (c is not null) Console.WriteLine($"{c.Nome}: {c.Pontos} pontos");
}
Console.WriteLine("ninguém mais");

class Cliente(string nome, int pontos)
{
    public string Nome { get; } = nome;
    public int Pontos { get; set; } = pontos;
}

Resumo

  • Cada .NET traz uma versão do C#: .NET 8 → C# 12, .NET 9 → C# 13, .NET 10 → C# 14 (LTS, até 2028). Um recurso mais novo que o <LangVersion> do projeto dá CS9260, dizendo o nome do recurso e a versão necessária.
  • Do C# 8 ao 13, a linguagem ganhou referências anuláveis, expressões switch, índices ^, records, instruções de nível superior, padrões cada vez mais ricos, strings brutas, required, expressões de coleção, construtores primários e params com spans.
  • C# 14: field nas propriedades (cuidado com campos chamados field: CS9258), blocos extension com propriedades e membros estáticos, obj?.Prop = valor (e +=, mas não ++: CS1059), nameof(List<>), lambdas (x, out y) =>, construtores e eventos partial, operator += e spans de primeira classe.
  • Todos os exemplos do C# 14 desta aula foram conferidos no .NET 10 do laboratório e no editor do guia.

Próxima aula: Arquivos (File), o começo do grupo Arquivos e Dados.