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).

Você vai aprender
  • 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, CS8862 e CS9058.

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 construtor private impediria o new de 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, nem int, nem nada antes do nome. Um construtor não devolve valor; quem "devolve" o objeto pronto é o new. É 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 propriedade Nome (maiúscula) do objeto que está nascendo.
public string Nome { get; set; }
Repare: sumiu o = "", e não há aviso CS8618. O compilador vê que o construtor sempre preenche Nome, então nenhum produto nasce com nome null.
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.
Por que 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 construtorCom 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 nomeO 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 erro CS0200 ("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.WriteLine aparece nas quatro criações.
new Produto();
Um new sem 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).
Parâmetros opcionais: outra forma

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 que Nome = 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

FormaExemploQuando usar
Construtor padrão + atribuiçõesnew Produto(); p.Nome = …;Só em exemplos; fácil esquecer um dado
Um construtor com todos os dadosnew Produto("maçã", 7.90m)O caso mais comum: todo objeto precisa desses dados
Construtor que confere os dadosif (preco < 0) throw … no construtorQuando 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# 12class Produto(string nome, decimal preco)O construtor só copiaria os parâmetros
Valor inicial na propriedadepublic int Estoque { get; set; } = 10;O valor é o mesmo para todos e não depende de parâmetro
Inicializador de objetonew Produto { Nome = "maçã" }Preencher propriedades na criação, sem construtor; veja Inicializadores e required

Quando dá errado

CS7036: faltou argumento
$ 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(…).

CS1503: argumentos na ordem errada
$ 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çã").

CS1729: nenhum construtor com essa quantidade de argumentos
$ 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.

CS0542: construtor com 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.

CS0516: o construtor chama a si mesmo
$ 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).

CS8862: construtor extra sem passar pelo primário
$ 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) { }.

O construtor rodou, mas uma propriedade ficou vazia (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

EscritaSignificado
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ãoCriado pelo C# só se a classe não tiver nenhum construtor; sem parâmetros, vazio
Vários construtoresPermitido, 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# 12Construtor primário: os parâmetros valem na classe inteira
.ctorNome 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:

public (string titulo)

2. Criar com argumentos

O construtor é Conta(string titular, decimal saldo). Complete para criar a conta da Ana com 300:

Conta c =

3. Encadear

Complete o construtor sem parâmetros para chamar o construtor Produto(string) com "sem nome":

public Produto() { }

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 porque Nome e nome sã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.