O C# 14 chegou junto com o .NET 10 e trouxe recursos que mudam a forma como você escreve código no dia a dia. Não são mudanças cosméticas — são features que reduzem boilerplate, eliminam bugs sutis e tornam o código mais expressivo. Este artigo cobre as 4 novidades mais impactantes com exemplos práticos para você começar a usar agora.
A keyword field: propriedades sem backing field manual
Até o C# 13, se você precisava de lógica customizada no set de uma propriedade, era obrigado a criar um backing field manualmente. O C# 14 introduz a keyword field — ela dá acesso ao backing field gerado automaticamente pelo compilador, direto no accessor.
// ❌ Antes (C# 13) — backing field manual obrigatório
private string _nome;
public string Nome
{
get => _nome;
set
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
_nome = value.Trim();
}
}
// ✅ C# 14 — field keyword elimina o backing field explícito
public string Nome
{
get;
set
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
field = value.Trim();
}
}
O field é uma keyword contextual — só tem significado especial dentro de property accessors. Se você já tem uma variável chamada field no seu código, ela continua funcionando normalmente fora de propriedades.
Onde field brilha na prática
Validação inline sem boilerplate:
public class Produto
{
public string Nome
{
get;
set
{
ArgumentException.ThrowIfNullOrWhiteSpace(value);
field = value.Trim();
}
}
public decimal Preco
{
get;
set => field = value >= 0
? value
: throw new ArgumentOutOfRangeException(nameof(value), "Preço não pode ser negativo");
}
public int EstoqueMinimo
{
get;
set => field = Math.Max(value, 0);
}
}
Lazy initialization sem campo extra:
public class ConfigService
{
public AppSettings Settings
{
get => field ??= CarregarConfiguracoes();
}
private AppSettings CarregarConfiguracoes()
=> JsonSerializer.Deserialize<AppSettings>(File.ReadAllText("appsettings.json"))!;
}
Notificação de mudança (INotifyPropertyChanged):
public class PedidoViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string Cliente
{
get;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cliente)));
}
}
public decimal Total
{
get;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Total)));
}
}
}
Cada propriedade que antes exigia 3 linhas extras (campo + get + set) agora é auto-contida. Em ViewModels com 10+ propriedades bindadas, a redução de código é significativa.
Extension members: extensões de verdade
O C# sempre teve extension methods, mas a sintaxe era limitada — apenas métodos estáticos em classes estáticas. O C# 14 introduz extension members com o novo bloco extension, permitindo definir métodos, propriedades, indexers e operadores como extensões de qualquer tipo.
// ❌ Antes (C# 13) — extension method tradicional
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string? s) => string.IsNullOrEmpty(s);
public static string Truncate(this string s, int maxLength)
=> s.Length <= maxLength ? s : s[..maxLength] + "…";
public static int WordCount(this string s)
=> s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
// ✅ C# 14 — extension block com propriedades e métodos
public static class StringExtensions
{
extension(string? s)
{
public bool IsNullOrEmpty => string.IsNullOrEmpty(s);
}
extension(string s)
{
public string Truncate(int maxLength)
=> s.Length <= maxLength ? s : s[..maxLength] + "…";
public int WordCount
=> s.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}
}
A diferença principal: WordCount e IsNullOrEmpty agora são propriedades, não métodos. O consumo fica mais natural:
var texto = "C# 14 trouxe novidades incríveis para desenvolvedores";
// Antes: texto.WordCount() — parece um método, mas é só uma leitura
// Agora: texto.WordCount — propriedade, como deveria ser
Console.WriteLine(texto.WordCount); // 8
Console.WriteLine(texto.IsNullOrEmpty); // false
Console.WriteLine(texto.Truncate(20)); // "C# 14 trouxe novidad…"
Extension members em tipos genéricos
A nova sintaxe funciona com generics e constraints:
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source) where T : IComparable<T>
{
public T? SecondMax
{
get
{
var sorted = source.Distinct().OrderByDescending(x => x).Take(2);
return sorted.Count() >= 2 ? sorted.Last() : default;
}
}
}
extension<T>(IList<T> list)
{
public void Swap(int i, int j)
=> (list[i], list[j]) = (list[j], list[i]);
}
}
var numeros = new List<int> { 5, 3, 8, 1, 8, 5 };
Console.WriteLine(numeros.SecondMax); // 5
numeros.Swap(0, 2);
// numeros: [8, 3, 5, 1, 8, 5]
Extension members estáticos
O C# 14 também suporta extensões de membros estáticos, permitindo adicionar factory methods e constantes a tipos existentes:
public static class GuidExtensions
{
extension(Guid)
{
public static Guid CreateV7() => Guid.CreateVersion7();
public static bool IsValid(string input)
=> Guid.TryParse(input, out _);
}
}
// Uso — parece nativo do tipo
var id = Guid.CreateV7();
var valido = Guid.IsValid("não-é-guid"); // false
Null-conditional assignment: atribuição segura com ?.
O operador ?. já era indispensável para ler propriedades de objetos possivelmente nulos. Mas na hora de escrever, você precisava de um if explícito. O C# 14 resolve isso com null-conditional assignment.
// ❌ Antes (C# 13) — if guard para atribuição condicional
if (pedido?.Cliente != null)
{
pedido.Cliente.UltimaCompra = DateTime.UtcNow;
}
if (config?.Logging != null)
{
config.Logging.Level = LogLevel.Debug;
}
// ✅ C# 14 — atribuição condicional direta
pedido?.Cliente?.UltimaCompra = DateTime.UtcNow;
config?.Logging?.Level = LogLevel.Debug;
Se qualquer parte da cadeia for null, a atribuição simplesmente não acontece — sem NullReferenceException, sem if verboso.
Cenários práticos do null-conditional assignment
Configuração opcional de serviços:
public void ConfigurarServico(ServicoConfig? config)
{
config?.Timeout = TimeSpan.FromSeconds(30);
config?.RetryCount = 3;
config?.Headers?.Add("X-Request-Id", Guid.NewGuid().ToString());
}
Atualização parcial de entidades:
public void AtualizarEndereco(Cliente? cliente, EnderecoDto dto)
{
cliente?.Endereco?.Rua = dto.Rua;
cliente?.Endereco?.Cidade = dto.Cidade;
cliente?.Endereco?.Cep = dto.Cep;
cliente?.AtualizadoEm = DateTime.UtcNow;
}
Encadeamento com eventos:
// Disparar evento apenas se o handler existir
viewModel?.PropertyChanged?.Invoke(viewModel, new PropertyChangedEventArgs("Status"));
Combinando com ??=
O null-conditional funciona com null-coalescing assignment para inicialização condicional:
// Inicializa a lista somente se o objeto pai existir
pedido?.Itens ??= new List<ItemPedido>();
First-class Span<T>: performance sem atrito
O Span<T> e ReadOnlySpan<T> existem desde o C# 7.2, mas sempre foram cidadãos de segunda classe — não podiam ser usados em delegates, generics de interface, nem em conversões implícitas com arrays e strings. O C# 14 muda isso com suporte first-class a Span.
Conversões implícitas
// ❌ Antes (C# 13) — conversão explícita necessária
void ProcessarDados(ReadOnlySpan<byte> dados) { }
byte[] buffer = new byte[1024];
ProcessarDados(buffer.AsSpan()); // precisava de .AsSpan()
// ✅ C# 14 — conversão implícita de array para Span
ProcessarDados(buffer); // funciona direto
// String para ReadOnlySpan<char> também é implícito
void Analisar(ReadOnlySpan<char> texto) { }
Analisar("hello world"); // sem .AsSpan()
Span em delegates e lambdas
// ❌ Antes — Span não podia ser usado em Func<>/Action<>
// Func<ReadOnlySpan<char>, bool> validator; // ERRO de compilação
// ✅ C# 14 — Span funciona em delegates
Func<ReadOnlySpan<char>, bool> validarCpf = (ReadOnlySpan<char> cpf) =>
{
if (cpf.Length != 11) return false;
foreach (var c in cpf)
{
if (!char.IsDigit(c)) return false;
}
return true;
};
Console.WriteLine(validarCpf("12345678901")); // true
Console.WriteLine(validarCpf("123")); // false
Span em interfaces genéricas
// ✅ C# 14 — LINQ-like operations com Span via interfaces
ReadOnlySpan<int> numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Countable com Span
var pares = 0;
foreach (var n in numeros)
{
if (n % 2 == 0) pares++;
}
Exemplo real: parser de alta performance
O first-class Span permite escrever parsers com zero allocation de forma muito mais natural:
public static class CsvParser
{
public static void ParseLinha(ReadOnlySpan<char> linha, Span<Range> campos)
{
int campoIndex = 0;
int inicio = 0;
for (int i = 0; i < linha.Length; i++)
{
if (linha[i] == ',' || i == linha.Length - 1)
{
int fim = linha[i] == ',' ? i : i + 1;
campos[campoIndex++] = new Range(inicio, fim);
inicio = i + 1;
}
}
}
}
// Uso — zero allocation
var linha = "João,Silva,42,São Paulo";
Span<Range> campos = stackalloc Range[4];
CsvParser.ParseLinha(linha, campos);
foreach (var campo in campos)
{
Console.WriteLine(linha.AsSpan()[campo]); // João, Silva, 42, São Paulo
}
Comparativo de performance: Span vs String
| Operação | string (heap) | Span<char> (stack) |
|---|---|---|
| Substring | Aloca nova string | Zero allocation |
| Split | Array de strings | Ranges no stack |
| Comparação | GC pressure | Inline comparison |
| Parsing numérico | int.Parse(str) | int.Parse(span) |
| Uso de memória (1M ops) | ~500MB alocados | ~0 alocado |
Outras novidades do C# 14 que valem conhecer
params com coleções
O params agora aceita Span<T>, ReadOnlySpan<T>, IEnumerable<T> e qualquer tipo com collection expression — não apenas arrays:
// ✅ C# 14 — params com ReadOnlySpan (zero allocation!)
public static int Somar(params ReadOnlySpan<int> valores)
{
var total = 0;
foreach (var v in valores)
total += v;
return total;
}
// O compilador usa stackalloc em vez de alocar um array
var resultado = Somar(1, 2, 3, 4, 5); // sem allocation
nameof em mais contextos
O nameof agora funciona em atributos referenciando membros de instância e em contextos que antes exigiam o nome literal:
public class Pedido
{
[MemberNotNullWhen(true, nameof(Cliente))]
public bool TemCliente => Cliente is not null;
public Cliente? Cliente { get; set; }
}
Escape sequence \e
Para códigos ANSI de terminal, agora existe \e em vez do pouco legível \x1B:
// ❌ Antes — o que é \x1B mesmo?
Console.Write("\x1B[31mErro!\x1B[0m");
// ✅ C# 14 — claro e intencional
Console.Write("\e[31mErro!\e[0m"); // texto vermelho no terminal
Console.Write("\e[1;32mSucesso!\e[0m"); // verde bold
Tabela resumo: C# 14 vs versões anteriores
| Feature | Antes (C# 13) | C# 14 | Impacto |
|---|---|---|---|
field keyword | Backing field manual | field no accessor | Menos boilerplate |
| Extension members | Só métodos estáticos | Propriedades, indexers, estáticos | APIs mais naturais |
| Null-conditional assignment | if (x != null) x.Prop = val | x?.Prop = val | Código mais seguro e limpo |
| First-class Span | Conversões explícitas, sem delegates | Implícitas, delegates, interfaces | Performance sem atrito |
params collections | Apenas params T[] | params Span<T>, IEnumerable<T> | Zero allocation em params |
\e escape | \x1B | \e | Legibilidade |
Requisitos para usar C# 14
O C# 14 é a versão de linguagem padrão do .NET 10. Para utilizá-lo:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<!-- C# 14 é o padrão — não precisa especificar LangVersion -->
</PropertyGroup>
</Project>
Se você está em um projeto .NET 8 ou 9 e quer experimentar features que não dependem de runtime (como field keyword), pode forçar a versão:
<LangVersion>14</LangVersion>
Atenção: features que dependem de APIs do runtime (como first-class Span em interfaces) precisam do .NET 10 como target framework.
Conclusão
O C# 14 não reinventa a linguagem, mas polui as arestas que mais incomodavam no dia a dia. A keyword field elimina backing fields manuais, extension members tornam APIs de extensão mais naturais, null-conditional assignment remove ifs defensivos, e o first-class Span finalmente trata performance como cidadão de primeira classe.
A recomendação é direta: se você está no .NET 10, use tudo. Se está migrando, comece pelo field keyword e null-conditional assignment — são as mudanças com maior impacto e menor risco de adoção.
Quer migrar para .NET 10 e aproveitar tudo do C# 14? A DevPlus ajuda seu time com migração, refatoração e adoção de novas features. Fale com a gente.