C# Construtores
Até aqui, criar um produto levava três linhas: new Produto(), depois o nome, depois o preço. E nada impedia de esquecer uma delas. O construtor resolve isso: é um bloco especial da classe que roda no momento em que o objeto nasce, e pode exigir os dados necessários logo no new: new Produto("maçã", 7.90m).
- O que é um construtor e como ele difere de um método.
- O construtor padrão que o C# cria sozinho, e quando ele deixa de existir.
- Construtores com parâmetros, para cada objeto nascer pronto, e por que
Nome = nome;funciona em C#. - Propriedades
{ get; }, que só o construtor preenche, e a validação dos dados na entrada. - Vários construtores na mesma classe e o
: this(…), que encadeia um no outro. - O construtor primário C# 12:
class Produto(string nome, decimal preco). - Os erros reais:
CS7036,CS1503,CS1729,CS0542,CS0516,CS8862eCS9058.
Todos os programas desta aula rodaram com dotnet run arquivo.cs na máquina fictícia da Ana (usuária ana, computador notebook), com o .NET 10 SDK (C# 14), na pasta ~/cs.
A ideia antes do código: a maternidade
Quando um bebê nasce, ele não sai da maternidade "em branco": já sai com nome, data de nascimento e uma pulseira de identificação. Esse procedimento acontece uma vez, no nascimento, e é obrigatório.
O construtor é o procedimento de nascimento de um objeto. Ele roda automaticamente a cada new, uma única vez por objeto, e serve para deixar o objeto pronto para uso.
O construtor padrão
Você já usou construtores sem saber. Em new Produto(), os parênteses depois de Produto são uma chamada de construtor. Se a classe não escreve nenhum, o C# cria sozinho um construtor vazio, sem parâmetros, chamado construtor padrão. Ele só deixa os campos e propriedades com os valores iniciais e padrão.
Você pode escrever esse construtor sem parâmetros por conta própria, para ver quando ele roda:
Produto a = new Produto();
Produto b = new Produto();
Console.WriteLine($"[{a.Nome}] {a.Preco}");
class Produto
{
public string Nome { get; set; } = "";
public decimal Preco { get; set; }
public Produto()
{
Console.WriteLine("Um produto novo nasceu!");
}
}
$ dotnet run k1.cs Um produto novo nasceu! « o new da linha 1 Um produto novo nasceu! « o new da linha 2 [] 0 « as propriedades continuam com os valores iniciais
- public
- Quem pode criar objetos com este construtor.
public= qualquer parte do programa. (Um construtorprivateimpediria onewde fora da classe.) - Produto
- O nome do construtor é exatamente o nome da classe, com a mesma maiúscula.
- (sem tipo de retorno)
- Repare: não há
void, nemint, nem nada antes do nome. Um construtor não devolve valor; quem "devolve" o objeto pronto é onew. É isso que o diferencia de um método. - ()
- A lista de parâmetros. Vazia aqui.
- { … }
- O corpo, executado a cada
new Produto().
Construtor com parâmetros
O uso mais importante do construtor é receber os dados do objeto e guardá-los nas propriedades. A Ana reescreveu a classe:
Produto maca = new Produto("maçã", 7.90m);
Produto banana = new Produto("banana", 3.50m);
maca.Mostrar();
banana.Mostrar();
class Produto
{
public string Nome { get; set; }
public decimal Preco { get; set; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public void Mostrar()
{
Console.WriteLine($"{Nome}: {Preco:C}");
}
}
$ dotnet run k2.cs maçã: R$ 7,90 banana: R$ 3,50
- public Produto(string nome, decimal preco)
- Um construtor com dois parâmetros. Agora, para criar um produto, é obrigatório informar nome e preço.
- Nome = nome;
- Copia o valor recebido (o parâmetro
nome, minúsculo) para a propriedadeNome(maiúscula) do objeto que está nascendo. - public string Nome { get; set; }
- Repare: sumiu o
= "", e não há avisoCS8618. O compilador vê que o construtor sempre preencheNome, então nenhum produto nasce com nomenull. - new Produto("maçã", 7.90m)
- Cria o objeto e chama o construtor com os argumentos, na mesma ordem e tipos dos parâmetros. Uma linha, objeto pronto.
Nome = nome; não confunde o C#?
Porque, para o C#, maiúsculas e minúsculas fazem nomes diferentes: Nome é a propriedade, nome é o parâmetro. As convenções do C# (PascalCase para propriedades, camelCase para parâmetros) evitam a confusão de propósito. O problema aparece quando o campo e o parâmetro têm exatamente o mesmo nome, e é aí que entra o this, assunto da próxima aula.
Compare com o jeito antigo:
| Sem construtor | Com construtor |
|---|---|
Produto maca = new Produto();maca.Nome = "maçã";maca.Preco = 7.90m; | Produto maca = new Produto("maçã", 7.90m); |
| Dá para esquecer uma linha, e o produto fica sem nome | O compilador não deixa criar um produto sem nome e preço |
Missão: escreva um construtor para a classe Conta que receba o titular (string) e o saldo inicial (decimal). Depois troque, nas instruções do topo, as linhas de atribuição por new Conta("Ana", 500m) e new Conta("Bruno", 80m).
Conta a = new Conta();
a.Titular = "Ana";
a.Saldo = 500m;
Conta b = new Conta();
b.Titular = "Bruno";
b.Saldo = 80m;
a.Mostrar();
b.Mostrar();
class Conta
{
public string Titular { get; set; } = "";
public decimal Saldo { get; set; }
// escreva o construtor aqui
public void Mostrar()
{
Console.WriteLine($"{Titular}: {Saldo:C}");
}
}
Conta a = new Conta("Ana", 500m);
Conta b = new Conta("Bruno", 80m);
a.Mostrar();
b.Mostrar();
class Conta
{
public string Titular { get; set; } = "";
public decimal Saldo { get; set; }
public Conta(string titular, decimal saldo)
{
Titular = titular;
Saldo = saldo;
}
public void Mostrar()
{
Console.WriteLine($"{Titular}: {Saldo:C}");
}
}
Quando você escreve um construtor, o padrão some
Este é o ponto que mais pega quem está começando. O C# só cria o construtor padrão (vazio) se a classe não tiver nenhum. A partir do momento em que você escreve Produto(string, decimal), o Produto() automático deixa de existir:
Produto maca = new Produto();
class Produto
{
public string Nome { get; set; }
public decimal Preco { get; set; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
}
$ dotnet run k3.cs /home/ana/cs/k3.cs(1,20): error CS7036: Não há nenhum argumento fornecido que corresponda ao parâmetro necessário 'nome' de 'Produto.Produto(string, decimal)' Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Leia a mensagem com calma: "não há nenhum argumento que corresponda ao parâmetro obrigatório nome de Produto.Produto(string, decimal)". O Produto.Produto é "o construtor Produto da classe Produto". O único construtor que existe pede um texto e um número, e a chamada não mandou nada; o compilador reclama do primeiro que faltou. É a mesma mensagem dos métodos com argumentos faltando.
Há duas saídas: passar os argumentos (new Produto("maçã", 7.90m)) ou, se fizer sentido criar produtos "em branco", escrever também um construtor sem parâmetros. Isso leva à seção sobre vários construtores.
Propriedades que só o construtor preenche
Com o construtor, um objeto pode nascer com dados que nunca mais mudam. Basta tirar o set da propriedade: { get; }. Ela continua podendo ser gravada dentro do construtor, e em nenhum outro lugar.
E, já que todo produto passa pelo construtor, ele é o lugar perfeito para conferir os dados:
Produto p = new Produto("kiwi", -3m);
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
if (preco < 0)
throw new ArgumentException("O preço não pode ser negativo.");
Nome = nome;
Preco = preco;
}
}
$ dotnet run pc7.cs; echo "código: $?" Unhandled exception. System.ArgumentException: O preço não pode ser negativo. at Produto..ctor(String nome, Decimal preco) in /home/ana/cs/pc7.cs:line 11 at Program.<Main>$(String[] args) in /home/ana/cs/pc7.cs:line 1 código: 134
- public decimal Preco { get; }
- Só leitura para o mundo. Quem tentar
p.Preco = 5m;recebe o erroCS0200("não pode ser atribuído, pois é somente leitura"), o mesmo das propriedades calculadas. - throw new ArgumentException(…)
- Com um preço negativo, o construtor se recusa a terminar: lança uma exceção e o objeto nunca chega a existir. Não há como criar um produto inválido. Detalhes em throw.
- Produto..ctor
- Na pilha da exceção, o construtor aparece com o nome interno
.ctor(de constructor). Os dois pontos seguidos são isso:Produto+.+.ctor.
Um objeto que não muda depois de criado é chamado de imutável. Ele é mais fácil de entender (o kiwi de 3 reais será sempre o kiwi de 3 reais) e é muito usado em C# moderno. Os records levam essa ideia mais longe.
Vários construtores
Uma classe pode ter vários construtores, desde que as listas de parâmetros sejam diferentes. É a mesma regra da sobrecarga de métodos. Assim, quem cria o objeto escolhe quanta informação passar. Mas, se cada construtor preencher tudo sozinho, as mesmas linhas se repetem em todos. A solução é encadear.
: this(…): um construtor chama o outro
Escrito depois dos parênteses de um construtor, : this(…) quer dizer "antes de mim, rode outro construtor desta mesma classe, com estes argumentos". Os construtores curtos passam a delegar para o completo, que é o único a fazer o trabalho:
new Produto();
new Produto("kiwi");
new Produto("maçã", 7.90m);
new Produto("uva", 12.00m, 30);
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public int Estoque { get; }
public Produto() : this("sem nome", 0m, 0)
{
}
public Produto(string nome) : this(nome, 0m, 0)
{
}
public Produto(string nome, decimal preco) : this(nome, preco, 10)
{
}
public Produto(string nome, decimal preco, int estoque)
{
Nome = nome;
Preco = preco;
Estoque = estoque;
Console.WriteLine($"Criado: {Nome}, {Preco:C}, estoque {Estoque}");
}
}
$ dotnet run k6.cs Criado: sem nome, R$ 0,00, estoque 0 Criado: kiwi, R$ 0,00, estoque 0 Criado: maçã, R$ 7,90, estoque 10 Criado: uva, R$ 12,00, estoque 30
- public Produto() : this("sem nome", 0m, 0)
- O construtor sem parâmetros chama o de três, com valores "padrão". O C# escolhe qual construtor pelos tipos dos argumentos, como na sobrecarga. O corpo
{ }fica vazio: não há mais nada a fazer. - : this(nome, preco, 10)
- Repassa o que recebeu e completa o que falta (estoque 10).
- o construtor de três parâmetros
- O único que realmente preenche as propriedades. Todo produto, criado por qualquer construtor, passa por ele. Por isso o
Console.WriteLineaparece nas quatro criações. - new Produto();
- Um
newsem guardar o objeto numa variável é permitido; aqui só queremos ver o construtor rodar.
Regras do : this(…):
- Fica entre os parênteses do construtor e a chave
{, e roda antes do corpo. - Cada construtor pode chamar no máximo um outro.
- Um construtor não pode chamar a si mesmo: é o erro
CS0516(veja abaixo).
Os mesmos quatro jeitos de criar um produto cabem num construtor só, com parâmetros opcionais: public Produto(string nome = "sem nome", decimal preco = 0m, int estoque = 10). Funciona bem quando os valores padrão não dependem uns dos outros. Repare que, no exemplo acima, o estoque padrão é 0 sem preço e 10 com preço; isso só os vários construtores conseguem expressar.
O construtor primário C# 12
Muitas classes têm um construtor que só copia os parâmetros para as propriedades. O C# 12 criou um atalho: escrever os parâmetros direto na linha da classe. É o construtor primário:
Produto maca = new Produto("maçã", 7.90m);
maca.Mostrar();
Console.WriteLine(maca.Nome);
class Produto(string nome, decimal preco)
{
public string Nome { get; } = nome;
public decimal Preco { get; } = preco;
public void Mostrar()
{
Console.WriteLine($"{Nome}: {Preco:C}");
}
}
$ dotnet run pc1.cs maçã: R$ 7,90 maçã
- class Produto(string nome, decimal preco)
- Os parênteses depois do nome da classe são o construtor. Criar um produto continua igual:
new Produto("maçã", 7.90m). - public string Nome { get; } = nome;
- O valor inicial da propriedade é o parâmetro
nome. Faz o mesmo queNome = nome;dentro de um construtor comum. - (nenhum
public Produto(…) { … }) - As quatro linhas do construtor comum sumiram.
Os parâmetros de um construtor primário valem na classe inteira, inclusive dentro dos métodos. Isso permite guardar algo sem nem declarar uma propriedade:
Conta c = new Conta("Ana");
c.Depositar(50m);
c.Mostrar();
class Conta(string titular)
{
private decimal _saldo;
public void Depositar(decimal valor)
{
_saldo += valor;
}
public void Mostrar()
{
Console.WriteLine($"{titular}: {_saldo:C}");
}
}
$ dotnet run pc2.cs Ana: R$ 50,00
O método Mostrar usou titular, o parâmetro do construtor primário, como se fosse um campo privado. Mas atenção: ele não é uma propriedade. De fora da classe, c.titular não existe (é o erro CS1061). Para expor o dado, declare uma propriedade, como o Nome do exemplo anterior.
Outros construtores, junto com o primário
Uma classe com construtor primário pode ter outros construtores, mas todos precisam passar por ele com : this(…):
Produto kiwi = new Produto("kiwi");
Console.WriteLine($"{kiwi.Nome}: {kiwi.Preco:C}");
class Produto(string nome, decimal preco)
{
public string Nome { get; } = nome;
public decimal Preco { get; } = preco;
public Produto(string nome) : this(nome, 0m)
{
}
}
$ dotnet run pc5.cs kiwi: R$ 0,00
Sem o : this(nome, 0m), o compilador recusa com o CS8862 (veja abaixo). E, em versões antigas da linguagem, o construtor primário em classes não existe: com #:property LangVersion=11 no topo do arquivo, o erro é o CS9058.
$ dotnet run pc6.cs /home/ana/cs/pc6.cs(3,14): error CS9058: O recurso 'construtores primários' não está disponível no C# 11.0. Use a versão do idioma 12.0 ou superior. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Brinque com o código: acrescente à classe uma propriedade public int Estoque { get; set; } = 10; e mostre-a no Mostrar(). Depois tente Console.WriteLine(maca.nome); no topo e leia o erro: o parâmetro não é visível de fora.
Produto maca = new Produto("maçã", 7.90m);
maca.Mostrar();
class Produto(string nome, decimal preco)
{
public string Nome { get; } = nome;
public decimal Preco { get; } = preco;
public void Mostrar()
{
Console.WriteLine($"{Nome}: {Preco:C}");
}
}
Formas diferentes de criar objetos prontos
| Forma | Exemplo | Quando usar |
|---|---|---|
| Construtor padrão + atribuições | new Produto(); p.Nome = …; | Só em exemplos; fácil esquecer um dado |
| Um construtor com todos os dados | new Produto("maçã", 7.90m) | O caso mais comum: todo objeto precisa desses dados |
| Construtor que confere os dados | if (preco < 0) throw … no construtor | Quando existe valor inválido; o objeto nunca nasce errado |
Vários construtores + : this(…) | new Produto("kiwi") e new Produto("uva", 12m, 30) | Alguns dados são opcionais e têm um valor padrão razoável |
| Construtor primário C# 12 | class Produto(string nome, decimal preco) | O construtor só copiaria os parâmetros |
| Valor inicial na propriedade | public int Estoque { get; set; } = 10; | O valor é o mesmo para todos e não depende de parâmetro |
| Inicializador de objeto | new Produto { Nome = "maçã" } | Preencher propriedades na criação, sem construtor; veja Inicializadores e required |
Quando dá errado
$ dotnet run k3.cs /home/ana/cs/k3.cs(1,20): error CS7036: Não há nenhum argumento fornecido que corresponda ao parâmetro necessário 'nome' de 'Produto.Produto(string, decimal)' Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: nenhum construtor da classe aceita o new Produto() sem argumentos. O caso clássico: a classe ganhou um construtor com parâmetros, o padrão sumiu, e algum lugar ainda faz new Produto().
Como resolver: passe os argumentos que o construtor pede (a mensagem mostra a lista: (string, decimal)) ou acrescente um construtor sem parâmetros, de preferência com : this(…).
$ dotnet run k4.cs /home/ana/cs/k4.cs(1,28): error CS1503: Argumento 1: não é possível converter de "decimal" para "string" /home/ana/cs/k4.cs(1,35): error CS1503: Argumento 2: não é possível converter de "string" para "decimal" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: o construtor é Produto(string nome, decimal preco) e a chamada foi new Produto(7.90m, "maçã"): o número veio primeiro.
Como resolver: new Produto("maçã", 7.90m), na ordem da declaração. Ou use argumentos nomeados: new Produto(preco: 7.90m, nome: "maçã").
$ dotnet run k8.cs /home/ana/cs/k8.cs(1,17): error CS1729: "Produto" não contém um construtor que aceita 4 argumentos Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: new Produto("maçã", 7.90m, 3, 4), mas a classe só tem construtores de 2 argumentos.
Como resolver: confira quais construtores existem e quantos parâmetros cada um tem. No VS Code, ao digitar o ( depois de new Produto, a lista aparece.
void (virou método!)
Produto maca = new Produto("maçã", 7.90m);
class Produto
{
public string Nome { get; set; } = "";
public decimal Preco { get; set; }
public void Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
}
$ dotnet run k5.cs /home/ana/cs/k5.cs(8,17): error CS0542: "Produto": nomes de membro não podem ser os mesmos do seu tipo delimitador Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: com void na frente, Produto(…) deixou de ser construtor: virou um método comum, que por acaso tem o nome da classe. E o C# proíbe isso: um membro não pode ter o mesmo nome do "tipo delimitador" (a classe que o contém).
Como resolver: apague o void. Construtores nunca têm tipo de retorno.
$ dotnet run k7.cs /home/ana/cs/k7.cs(5,35): error CS0516: Construtor "Produto.Produto(string)" não pode chamar a si mesmo Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: public Produto(string nome) : this(nome). O this(nome) chamaria o próprio Produto(string), numa recursão sem fim, e o compilador a detecta antes de rodar.
Como resolver: o : this(…) deve chamar um construtor diferente, normalmente o mais completo: : this(nome, 0m).
$ dotnet run pc4.cs /home/ana/cs/pc4.cs(9,12): error CS8862: Um construtor declarado em um tipo com uma lista de parâmetros deve ter o inicializador de construtor 'this'. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que aconteceu: a classe tem um construtor primário (class Produto(string nome, decimal preco)) e um construtor public Produto(string nome) { } sem : this(…). Os parâmetros do primário ficariam sem valor.
Como resolver: public Produto(string nome) : this(nome, 0m) { }.
CS8618)
$ dotnet run pc9.cs /home/ana/cs/pc9.cs(8,12): warning CS8618: O propriedade não anulável 'Nome' precisa conter um valor não nulo ao sair do construtor. Considere adicionar o modificador "obrigatório" ou declarar o propriedade como anulável.
O que aconteceu: o construtor recebeu nome, mas esqueceu a linha Nome = nome;. O aviso aponta para o construtor (linha 8): ao terminar, ele deixou Nome com null. O programa rodou e mostrou uma linha vazia.
Como resolver: preencha todas as propriedades não anuláveis no construtor. O CS8618 é o compilador conferindo isso por você.
Referência
| Escrita | Significado |
|---|---|
public NomeDaClasse(params) { … } | Construtor: nome igual ao da classe, sem tipo de retorno |
new NomeDaClasse(args) | Cria o objeto e chama o construtor que combina com os argumentos |
| Construtor padrão | Criado pelo C# só se a classe não tiver nenhum construtor; sem parâmetros, vazio |
| Vários construtores | Permitido, com listas de parâmetros diferentes (sobrecarga) |
public C(…) : this(args) { } | Chama outro construtor da mesma classe antes do corpo |
public T Prop { get; } | Só pode ser gravada no construtor (ou no valor inicial) |
class C(tipo a, tipo b) { … } C# 12 | Construtor primário: os parâmetros valem na classe inteira |
.ctor | Nome interno do construtor, nas mensagens de exceção |
void NomeDaClasse(…) | Não é construtor: erro CS0542 |
Missão final: dois jeitos de matricular
Missão: a classe Aluno já tem o construtor Aluno(string nome, string turma). O programa não compila, porque a Carla é criada só com o nome. Escreva um segundo construtor, Aluno(string nome), que use : this(…) para matricular o aluno na turma "1A".
Aluno carla = new Aluno("Carla");
Aluno davi = new Aluno("Davi", "2B");
carla.Mostrar();
davi.Mostrar();
class Aluno
{
public string Nome { get; }
public string Turma { get; }
public Aluno(string nome, string turma)
{
Nome = nome;
Turma = turma;
}
// escreva o construtor com um parâmetro aqui
public void Mostrar()
{
Console.WriteLine($"{Nome} - turma {Turma}");
}
}
Aluno carla = new Aluno("Carla");
Aluno davi = new Aluno("Davi", "2B");
carla.Mostrar();
davi.Mostrar();
class Aluno
{
public string Nome { get; }
public string Turma { get; }
public Aluno(string nome, string turma)
{
Nome = nome;
Turma = turma;
}
public Aluno(string nome) : this(nome, "1A")
{
}
public void Mostrar()
{
Console.WriteLine($"{Nome} - turma {Turma}");
}
}
Exercícios
1. O nome do construtor
Complete a declaração do construtor da classe Livro que recebe o título:
2. Criar com argumentos
O construtor é Conta(string titular, decimal saldo). Complete para criar a conta da Ana com 300:
3. Encadear
Complete o construtor sem parâmetros para chamar o construtor Produto(string) com "sem nome":
4. A classe Produto só tem o construtor Produto(string nome). O que acontece com new Produto()?
5. Qual é a diferença de declaração entre um construtor e um método?
6. Em class Conta(string titular) { … }, o que é titular?
Resumo
- O construtor roda uma vez, no
new, para deixar o objeto pronto. Tem o nome da classe e nenhum tipo de retorno. - Sem construtor escrito, o C# cria o padrão (vazio, sem parâmetros). Escreveu algum? O padrão some (
CS7036). - Construtores com parâmetros obrigam quem cria o objeto a informar os dados:
new Produto("maçã", 7.90m).Nome = nome;funciona porqueNomeenomesão nomes diferentes. - Propriedades
{ get; }só se gravam no construtor; e o construtor é o lugar de recusar dados inválidos. - Vários construtores = sobrecarga. Use
: this(…)para os curtos chamarem o completo. - C# 12 Construtor primário:
class Produto(string nome, decimal preco); os outros construtores precisam de: this(…)(CS8862). - Próxima aula: this, para quando o parâmetro e o campo têm o mesmo nome.