Versões do C#, o Compilador e Recursos Preview
Objective
Um projeto C# tem dois números de versão que as pessoas confundem o tempo todo: a versão da linguagem (qual sintaxe o compilador aceita) e o target framework (em qual runtime e bibliotecas o código roda). O compilador vem com o SDK, então o SDK do .NET 10 consegue compilar código C# 14 que tem como alvo o .NET 8. Mas só alguns recursos do C# são trabalho puro de compilador. Outros precisam de um tipo ou de uma capacidade de runtime que frameworks antigos não têm. Este conceito explica como a versão padrão é escolhida, como sobrescrevê-la e inspecioná-la, quais recursos do C# 14 sobrevivem em um framework antigo (verificado compilando, sem chute) e como os recursos preview são habilitados.
Use Cases
- Atualizar para o SDK do .NET 10 enquanto a aplicação ainda mira o .NET 8 e
querer usar
fieldou atribuição null-conditional hoje. - Ler um erro como "Feature 'x' is not available in C# 12.0" e saber se o
caminho é mudar o
LangVersionou o target framework. - Descobrir qual versão da linguagem um build usa de verdade na máquina de CI.
- Experimentar um recurso preview do compilador em um projeto descartável sem deixá-lo vazar para o código de produção.
Deep Dive
O padrão segue o target framework, não o SDK
Quando você não define LangVersion, o compilador escolhe o padrão a partir do
target framework, para que um projeto compilado por um SDK mais novo continue
com a linguagem para a qual foi desenhado. Dá para observar isso com o mesmo
arquivo-fonte:
plaintext<!-- p.csproj, só o TargetFramework muda --> <TargetFramework>net8.0</TargetFramework> <!-- padrão C# 12 --> <TargetFramework>net9.0</TargetFramework> <!-- padrão C# 13 --> <TargetFramework>net10.0</TargetFramework> <!-- padrão C# 14 -->
Pedir a propriedade ao MSBuild em um projeto net8.0 com o SDK 10.0.100
devolve o valor efetivo:
plaintext$ dotnet msbuild -getProperty:LangVersion 12.0 (projeto net10.0: 14.0)
Outro truque é forçar um diagnóstico que imprime a versão do compilador e da
linguagem. Um arquivo que contém só #error version falha com:
plaintexterror CS8304: Compiler version: '5.0.0-2.25523.111 (b0f34d51)'. Language version: 12.0.
(projeto net8.0 com configuração padrão; o mesmo arquivo em um projeto
net10.0 informa 14.0.) A versão do compilador pertence ao SDK, a versão da
linguagem pertence ao projeto. A Microsoft recomenda não definir LangVersion e
subir o target framework, porque o padrão é a combinação que linguagem e
bibliotecas testaram juntas.
Sobrescrevendo o LangVersion
LangVersion aceita um número (13.0, 14), default/latestMajor (a maior
versão major que este compilador suporta, ignorando o target framework),
latest (a maior versão lançada, incluindo minors) e preview. Duas regras
importam na prática:
- O valor é limitado pelo compilador. Com o SDK 10.0.100, definir
LangVersioncomo15falha comCS1617: Invalid option '15', e um SDK do .NET 9 (9.0.307) rejeita14do mesmo jeito. Linguagem mais nova exige SDK mais novo. latestedefaultdependem da máquina. A documentação desaconselhalatestporque duas máquinas com SDKs diferentes compilam linguagens diferentes. Fixe um número explícito se precisar sobrescrever, ou melhor, fixe o SDK comglobal.json.
O que funciona em um target framework antigo
Definindo <LangVersion>14</LangVersion> em projetos net8.0 e net9.0 e
compilando um programa pequeno por recurso, com o SDK 10.0.100, o resultado foi
este (sem LangVersion, cada linha do C# 14 falha com CS9202 ou CS9260,
"not available in C# 12.0/13.0"):
| Recurso | net8.0 | net9.0 | net10.0 |
|---|---|---|---|
Palavra-chave field em accessors |
funciona | funciona | funciona |
Atribuição null-conditional a?.B = x |
funciona | funciona | funciona |
nameof(List<>) |
funciona | funciona | funciona |
Modificadores em parâmetros de lambda (out x) => ... |
funciona | funciona | funciona |
Extension blocks extension(string s) { ... } |
funciona | funciona | funciona |
| Partial constructors | funciona | funciona | funciona |
Operadores compostos do usuário (operator +=) |
funciona | funciona | funciona |
params ReadOnlySpan<int> (C# 13) |
funciona | funciona | funciona |
allows ref struct (C# 13) |
CS9240, runtime sem generics by-ref-like |
funciona | funciona |
System.Threading.Lock (tipo, .NET 9) |
CS0234, tipo ausente |
funciona | funciona |
O padrão é a regra de bolso: sintaxe que o compilador consegue reduzir (lowering)
para código que já roda no runtime antigo funciona em todo lugar, enquanto um
recurso que depende de uma capacidade nova do runtime (allows ref struct
precisa de suporte do runtime para tipos ref-like como argumento genérico) ou de
um tipo novo de biblioteca (Lock) falha no framework antigo mesmo com o
LangVersion mais novo. A documentação diz isso diretamente: cada recurso tem
seu próprio requisito mínimo de API ou de runtime.
Um recurso merece nota. As conversões implícitas de span do C# 14 são uma
mudança de linguagem, então um extension method escrito para
ReadOnlySpan<int> só pode ser chamado em um int[] num projeto net9.0
depois que você sobe a versão da linguagem:
csharpint[] arr = [1, 2, 3];
Console.WriteLine(arr.Twice()); // LangVersion 14: 6
// C# 13 padrão: erro CS1929, 'int[]' does not contain a definition for 'Twice'
static class E { public static int Twice(this ReadOnlySpan<int> s) => s.Length * 2; }Recursos preview e EnablePreviewFeatures
Dois mecanismos diferentes se chamam "preview" e fazem coisas diferentes:
<LangVersion>preview</LangVersion>manda o compilador aceitar a sintaxe da próxima versão da linguagem, ainda não lançada. Compilou normalmente no SDK 10.0.100 para um programa que não usava nada novo. Sintaxe preview pode mudar ou sumir e não é suportada em produção.<EnablePreviewFeatures>true</EnablePreviewFeatures>faz o opt-in de APIs e recursos de runtime preview, os marcados com[RequiresPreviewFeatures]. Sem ele, usar esse tipo gera o warningCA2252("requires opting into preview features"); com ele, o código roda.
csharpusing System.Runtime.Versioning;
Foo.Bar(); // warning CA2252 a menos que EnablePreviewFeatures seja true
[RequiresPreviewFeatures] static class Foo { public static void Bar() => Console.WriteLine("preview api"); }Também dá para compilar com um SDK mais novo que o runtime em que você publica:
um projeto pode mirar net10.0 e usar um compilador mais novo via
LangVersion, desde que os recursos usados sejam só de compilador e o SDK esteja
fixado no global.json para o time inteiro concordar.
Padrão versus implementação
O C# é especificado separadamente do Roslyn, o compilador que o implementa. As
edições do ECMA-334 linkadas no Microsoft Learn param na era do C# 7, e as
versões seguintes são descritas pela especificação da linguagem mantida pela
Microsoft e pelas propostas de recurso no repositório csharplang. Na prática o
Roslyn é a referência: um recurso existe quando o Roslyn o entrega, e cada página
"What's new in C# N" é a prova para uma afirmação de versão.
Trade-offs
- Subir o
LangVersionem um framework antigo compra sintaxe, não bibliotecas. A maior parte da sintaxe do C# 14 funciona emnet8.0, mas um recurso que precisa de capacidade de runtime ou de biblioteca falha na compilação ou, pior, só em algumas máquinas. Teste com o menor framework que você suporta.csharpstatic T Id<T>(T t) where T : allows ref struct => t; // net8.0: CS9240 - Deixar o
LangVersionsem definir é o padrão seguro, mas parece lento. Você recebe C# 12 em um projeto .NET 8 mesmo com o SDK do .NET 10, e precisa subir o target framework para avançar. A vantagem é que todo contribuidor vê a mesma linguagem, seja qual for o SDK instalado. latestepreviewfazem o build depender da máquina. Duas pessoas com SDKs diferentes podem discordar sobre o código compilar ou não. Fixe o SDK noglobal.jsonse usar essas opções.plaintext{ "sdk": { "version": "10.0.100", "rollForward": "disable" } }- Sintaxe preview é uma aposta. Pode mudar antes do lançamento, e o código escrito para ela pode parar de compilar. Mantenha em projetos de exploração.
- Multi-targeting multiplica a matriz de testes. Uma biblioteca que mira
net8.0enet10.0comLangVersionalto precisa ser compilada e testada para cada alvo, porque um recurso pode compilar em um alvo e falhar em outro.