Novidades do C# 12 - Primary Constructors
Os primary constructors, ou construtores primários, chegaram às classes no C# 12, lançado em novembro de 2023 junto com o .NET 8. Antes disso, a equipe da Microsoft apresentou o recurso nos previews do .NET 8, e é a partir deles que vamos explorar a novidade.
Neste artigo, vamos revisar o que é um construtor, entender como a sintaxe nova simplifica as classes e ver as diferenças em relação aos records. Além disso, vamos analisar herança, construtores sem parâmetros e os cuidados que o compilador cobra.
Antes de começar
Quando publicamos a primeira versão deste conteúdo, o recurso existia apenas no .NET 8 Preview 3. Ou seja, tratava-se de uma pré-visualização, sujeita a mudanças.
Já vimos casos no passado, como o operador !!, que a equipe removeu antes da versão final. Por isso, a recomendação na época era não se apegar aos recursos de preview.
Hoje, o C# 12 já está disponível na versão final do .NET 8. Ainda assim, vale entender como habilitar a linguagem, porque o mesmo processo se repete a cada nova versão em preview.
Como instalar o .NET 8
Você pode fazer o download do .NET 8 no site oficial da Microsoft. Basta acessar a página de download do .NET 8 e escolher o instalador do seu sistema operacional.
Se ainda não tem o ambiente configurado, o guia de instalação e primeiros passos com o .NET mostra o processo completo.
Verificando a versão instalada
Com o .NET instalado, feche os terminais e abra novamente. Em seguida, execute o comando abaixo para verificar a versão instalada:
dotnet --version
# 8.0.100-preview.3.23178.7 (versão usada nos testes originais)
Durante os previews, o .NET 8 ainda mantinha o C# 11 como linguagem padrão. Por isso, era preciso habilitar a configuração LangVersion com o valor preview no arquivo .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- Garante que o projeto usa o .NET 8 -->
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Necessário apenas durante o preview -->
<LangVersion>preview</LangVersion>
</PropertyGroup>
</Project>
Com a versão final do .NET 8, o C# 12 passou a ser o padrão para projetos net8.0. Assim, a linha LangVersion deixou de ser necessária.
IMPORTANTE: durante os previews, a IDE podia não reconhecer a sintaxe nova e marcar o código como erro. Nas versões atuais do Visual Studio e do C# Dev Kit, esse problema não acontece mais.
Primary constructors chegam às classes
Se você já trabalha com C# e acompanha as melhorias da linguagem, provavelmente já usou a sintaxe de parâmetros na declaração de records. Desde o C# 9, um record aceita parâmetros logo após o nome do tipo.
No C# 12, essa possibilidade se estende a qualquer class e a qualquer struct. Dessa forma, podemos simplificar muito a criação de classes com primary constructors.
// Primary constructor em records (C# 9 em diante)
public record Person(int Id, string Name);
// Primary constructor em structs (novidade do C# 12)
public struct Point(int X, int Y);
// Primary constructor em classes (novidade do C# 12)
public class Customer(int id, string name);
Repare que só os records existiam antes. Por outro lado, as structs e classes comuns ganharam o recurso ao mesmo tempo, na versão 12 da linguagem.
O que é um construtor
Construtor, ou constructor em inglês, é um método especial de uma classe que roda toda vez que criamos um objeto. Além disso, ele obrigatoriamente tem o mesmo nome da classe.
Então, supondo que temos uma classe chamada Name, podemos ter um construtor chamado Name nela:
public class Name
{
public Name()
{
// Método construtor
}
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
Desta forma, toda vez que instanciarmos um objeto dessa classe, o método Name entra em ação. Note que ele não tem retorno e pode ou não receber parâmetros.
var name = new Name();
// Neste momento o construtor executa
var other = new Name
{
FirstName = "John",
LastName = "Doe",
};
// Outro exemplo, com object initializer
Um construtor sem parâmetros recebe o nome de parameterless, que significa exatamente "sem parâmetros" em inglês. No entanto, também podemos criar construtores com parâmetros.
Construtores com parâmetros
Um construtor com parâmetros recebe os valores no momento da criação do objeto. Veja o exemplo:
public class Name
{
public Name(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; set; }
public string LastName { get; set; }
}
O único detalhe é que, uma vez definido apenas um construtor com parâmetros, somos obrigados a informar os valores na instância:
var name = new Name();
// ❌ ERRO: precisa informar os valores
var other = new Name("John", "Doe");
// ✅ OK: informou os valores
Sobrecarga de construtores
Podemos combinar construtores e ter mais de um na mesma classe, incluindo um com e outro sem parâmetros:
public class Name
{
public Name() { }
public Name(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
Nesse caso, temos uma sobrecarga de método, que permite informar ou não os argumentos na chamada. O artigo sobre sobrescrita e sobrecarga aprofunda esse conceito.
var name = new Name();
// ✅ OK: passa pelo construtor parameterless
var other = new Name("John", "Doe");
// ✅ OK: informou os valores
Construtores protected e o Entity Framework
Também é comum vermos construtores com o modificador protected no lugar de public. Isso acontece porque o Entity Framework, por exemplo, precisa de um construtor parameterless para materializar objetos.
public DbSet<Product> Products { get; set; }
// ❌ Pode falhar se Product não tiver um construtor parameterless
Entretanto, uma boa prática é explicitar no construtor quais propriedades a classe exige para funcionar. Para conciliar as duas coisas, tornamos o construtor parameterless protected, já que apenas o EF e as classes derivadas o utilizam.
public class Name
{
// 👇 Agora é protected
protected Name() { }
public Name(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
}
Dessa forma, evitamos que alguém instancie a classe sem valores fora do seu escopo. Ainda assim, as classes filhas e os proxies do EF continuam usando o construtor parameterless.
var name = new Name();
// ❌ Erro: não pode instanciar sem passar valores
var other = new Name("John", "Doe");
// ✅ OK: informou os valores
public DbSet<Product> Products { get; set; }
// ✅ O EF consegue criar as instâncias
Parâmetros opcionais
Para finalizar, podemos usar os optional parameters para obter um efeito parecido:
public class Measurement
{
public Measurement(
int width = 0, // 👈 Parâmetro opcional, já possui um valor
int height = 0) // 👈 Parâmetro opcional, já possui um valor
{
Width = width;
Height = height;
}
public int Width { get; set; }
public int Height { get; set; }
}
Isso torna a instância do objeto mais flexível, pois os valores do construtor passam a ser opcionais:
// Inicia com os valores padrão
var first = new Measurement();
// Inicia com 10 para width e 0 para height
var second = new Measurement(10);
// Inicia com 10 para width e 10 para height
var third = new Measurement(10, 10);
De qualquer forma, as possibilidades são muitas. Na prática, tudo depende do seu cenário e de como você quer tratar a criação do objeto.
O que são primary constructors
Agora suponha uma classe com apenas duas propriedades obrigatórias. Com a sintaxe tradicional, o código ficaria assim:
public class Name
{
public Name(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public string FirstName { get; set; }
public string LastName { get; set; }
}
Pois é, muito código para algo simples, não acha? Inclusive, sempre vale verificar se um record resolve o problema no lugar da classe:
public record Name(string FirstName, string LastName);
public record struct FullName(string FirstName, string LastName);
Os records já contavam com primary constructors antes do C# 12. Com esse recurso, nem precisamos abrir chaves, porque tudo fica na declaração do tipo.
var name = new Name();
// ❌ ERRO: precisa informar os valores
var other = new Name("John", "Doe");
// ✅ OK: informou os valores
Então, um primary constructor nada mais é do que a possibilidade de declarar os valores obrigatórios na própria definição do tipo. No C# 12, as classes também ganharam essa sintaxe enxuta:
// Sintaxe reduzida, sem corpo
public class Name(string firstName, string lastName);
// Ou com corpo, para adicionar comportamento
public class FullName(string firstName, string lastName)
{
public override string ToString() => $"{firstName} {lastName}";
}
Como resultado, temos um comportamento semelhante ao anterior, em que somos obrigados a informar os valores:
var name = new FullName();
// ❌ CS7036: There is no argument given that corresponds to the required parameter 'firstName'
var other = new FullName("John", "Doe");
// ✅ OK
Isso acontece porque, ao declarar primary constructors em uma classe, o compilador deixa de gerar o construtor parameterless implícito. Por outro lado, em uma struct o construtor sem parâmetros implícito continua existindo e inicializa tudo com o valor padrão.
Primary constructors em classes e em records
Existe uma diferença importante entre primary constructors em classes e a mesma sintaxe em records. Nos records, o compilador gera automaticamente uma propriedade pública com o mesmo nome de cada parâmetro.
public record Name(string FirstName, string LastName);
var name = new Name("John", "Doe");
Console.WriteLine(name.FirstName);
Console.WriteLine(name.LastName);
Se trocarmos record por class, o comportamento muda. Em classes e structs comuns, os parâmetros de primary constructors continuam sendo apenas parâmetros, visíveis somente dentro do tipo.
// 👇 usando class no lugar de record
public class Name(string FirstName, string LastName);
var name = new Name("John", "Doe");
Console.WriteLine(name.FirstName); // ❌ FirstName não existe
Console.WriteLine(name.LastName); // ❌ LastName não existe
Nos previews, havia a dúvida se esse comportamento mudaria até a versão final. Contudo, a documentação oficial confirma que o compilador só gera propriedades públicas para tipos record.
Expondo parâmetros como propriedades
Para obter o mesmo resultado de um record, precisamos expor os parâmetros explicitamente com membros public:
public class Name(string firstName, string lastName)
{
public string FirstName => firstName;
public string LastName => lastName;
}
Desta forma, temos dois parâmetros de entrada, visíveis apenas dentro da classe, e dois membros públicos de saída.
var name = new Name("John", "Doe");
Console.WriteLine(name.FirstName); // ✅
Console.WriteLine(name.LastName); // ✅
Outra opção é inicializar propriedades a partir dos parâmetros, como em public string FirstName { get; } = firstName;. Nesse caso, o valor fica guardado na propriedade e o parâmetro não precisa de armazenamento próprio.
Convenção de nomes
Note também que não há necessidade de nomear os parâmetros com inicial maiúscula nas classes. Afinal, eles não viram membros públicos.
public record Person(string FirstName, string LastName);
// Em records, use maiúscula, pois viram propriedades públicas
// Em classes o comportamento inverte,
// por isso use minúscula, como em qualquer parâmetro
public class Author(string firstName, string lastName)
{
public string FirstName => firstName;
public string LastName => lastName;
}
Seguir essa convenção deixa claro, só de bater o olho, o que é parâmetro e o que é propriedade.
Primary constructors em structs
As structs comuns ganharam o recurso junto com as classes, e as regras são praticamente as mesmas. Assim como nas classes, os parâmetros não viram propriedades públicas.
Um uso interessante é calcular propriedades somente leitura a partir dos parâmetros. Veja um exemplo inspirado na documentação oficial:
public readonly struct Distance(double dx, double dy)
{
// Os parâmetros aparecem apenas nos inicializadores
public double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
public double Direction { get; } = Math.Atan2(dy, dx);
}
Como os parâmetros aparecem apenas nos inicializadores, o compilador não cria campos extras para dx e dy. Na prática, o código gerado equivale a um construtor tradicional que atribui as duas propriedades.
Há, porém, uma diferença importante em relação às classes. Em uma struct, o construtor parameterless implícito sempre existe e inicializa todos os campos, inclusive os parâmetros capturados, com o valor padrão.
Se você declarar um construtor sem parâmetros explícito, ele também precisa chamar o primary constructor com this(...). Dessa forma, você define valores padrão diferentes de zero para os parâmetros.
Herança com primary constructors
Para herdar de uma classe que usa primary constructors, precisamos fornecer os dados para ela. Esse é o mesmo comportamento que já temos com construtores que não são parameterless.
public class Student(string firstName, string lastName, int grade)
: Name(firstName, lastName)
{
}
Ao usar a herança com :, somos obrigados a informar firstName e lastName. Por isso, o Student também precisa receber esses valores no seu próprio construtor.
Um ponto interessante é que a classe Student recebe um parâmetro a mais, chamado grade, que ela não expõe. Logo, ninguém de fora consegue acessar esse valor.
var student = new Student("John", "Doe", 10);
Console.WriteLine(student.FirstName); // ✅ Pública em Name
Console.WriteLine(student.LastName); // ✅ Pública em Name
Console.WriteLine(student.Grade); // ❌ Somente Student conhece
Construtores parameterless e primary constructors
Outro cenário interessante envolve a herança de uma classe com primary constructors quando queremos um construtor parameterless na classe filha. Em outras palavras, Name usa a sintaxe nova, mas Student precisa de um construtor sem parâmetros.
public class Student(string firstName, string lastName, int grade)
: Name(firstName, lastName)
{
// Construtor com valores padrão
public Student() : this("First", "Last", 5)
{
}
public int Grade => grade;
}
Nesse caso, usamos o this para chamar o primary constructor e informar valores padrão. Isso é obrigatório, porque todo construtor adicional precisa chamar o primary constructor, direta ou indiretamente.
Essa regra garante que todos os parâmetros tenham valor em qualquer caminho de criação do objeto. Portanto, o compilador recusa um construtor extra que não use this(...).
Cuidados ao usar primary constructors
Os parâmetros de primary constructors se comportam como parâmetros, e não como campos. Isso tem algumas consequências práticas que vale conhecer.
Primeiro, o compilador só cria armazenamento para um parâmetro quando você o usa dentro de um membro, como um método ou uma propriedade com corpo. Se o parâmetro aparece apenas em inicializadores, nenhum campo oculto surge.
Além disso, esses parâmetros não são readonly. Ou seja, um método da classe pode reatribuir o valor, o que pode surpreender quem esperava imutabilidade.
public class Counter(int start)
{
// O compilador captura start em um campo oculto
public int Next() => ++start;
}
Se você precisa de imutabilidade, atribua o parâmetro a um campo ou propriedade somente leitura. Por exemplo, private readonly int _start = start; resolve a questão.
Outro cuidado envolve o armazenamento duplicado. Se você usa o parâmetro em um membro e também o passa para a classe base, o compilador emite o aviso CS9107, pois o valor pode existir em duas cópias.
Da mesma forma, o aviso CS9124 aparece quando você captura o parâmetro e também o usa para inicializar um campo ou propriedade. Nesse caso, prefira usar apenas o membro inicializado.
Primary constructors e injeção de dependência
Um uso muito comum de primary constructors aparece na injeção de dependência. Com eles, uma classe declara as suas dependências em uma única linha.
public class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
public async Task CreateAsync(Order order)
{
// Os parâmetros ficam disponíveis em qualquer método
logger.LogInformation("Creating order {OrderId}", order.Id);
await repository.AddAsync(order);
}
}
O container de DI do ASP.NET Core resolve esses parâmetros normalmente, como faria com um construtor tradicional. Para entender esse mecanismo, veja o artigo sobre injeção de dependência no ASP.NET Core.
Quando usar primary constructors
Primary constructors brilham em classes que recebem dependências ou valores obrigatórios e apenas os utilizam internamente. Serviços, handlers e controllers são bons exemplos desse cenário.
Por outro lado, quando o objetivo é transportar dados com igualdade por valor, um record costuma ser a escolha mais adequada. Afinal, ele já gera propriedades, Equals, GetHashCode e suporte à expressão with, que explicamos no artigo sobre a expressão with no C#.
Já para entidades de domínio com validações e regras, o construtor tradicional ainda pode ser mais claro. Nesse caso, você mantém o controle total sobre a ordem das atribuições e das verificações.
Também vale saber que o Visual Studio oferece a regra de estilo IDE0290, que sugere converter construtores simples em primary constructors. Se a sua equipe preferir outro padrão, desative a regra com a opção csharp_style_prefer_primary_constructors no .editorconfig.
Por fim, para aplicar atributos ao construtor gerado, use o alvo method:, como em [method: Obsolete]. Sem esse alvo, o atributo fica na classe, e não no construtor.
Conclusão
Os primary constructors são uma ótima adição às classes no C# 12, pois deixam o código mais simples e direto. Com eles, a declaração do tipo já mostra o que ele exige para existir.
Por outro lado, lembre que, fora dos records, os parâmetros não viram propriedades e podem ser reatribuídos. Assim, use primary constructors para reduzir código repetitivo, mas exponha e proteja os valores de forma explícita quando necessário.
Para se aprofundar, consulte o tutorial oficial de primary constructors no Microsoft Learn.
FAQ
Em qual versão do C# as classes ganharam primary constructors?
No C# 12, lançado em novembro de 2023 junto com o .NET 8. Antes disso, apenas os records aceitavam essa sintaxe.
Primary constructors em classes criam propriedades públicas?
Não, apenas records geram propriedades automaticamente. Em classes e structs, você precisa declarar as propriedades e atribuir os parâmetros a elas.
Posso ter outros construtores além do primary constructor?
Sim, desde que cada construtor adicional chame o primary constructor com this(...). Essa regra garante que todos os parâmetros recebam valor.
Os parâmetros do primary constructor são imutáveis?
Não, eles podem ser reatribuídos dentro da classe. Para garantir imutabilidade, copie o valor para um campo ou propriedade somente leitura.
Preciso configurar LangVersion para usar primary constructors?
Não em projetos que usam net8.0 ou superior, pois o C# 12 já é o padrão. A configuração preview era necessária apenas durante os previews do .NET 8.