Na aula anterior, aperfeiçoamos as características dos nossos objetos com properties. Mas a criação de um objeto continua desajeitada e insegura. Observe como criamos uma pessoa até agora: primeiro new Pessoa() produz um objeto vazio, e depois preenchemos cada property numa linha separada. Isso é trabalhoso, mas há um problema mais grave: nada obriga a preencher as características essenciais. Podemos criar uma Pessoa e esquecer de dar-lhe um nome, deixando-a existir num estado incompleto — uma pessoa sem nome, um produto sem preço, uma conta sem titular. Objetos assim, pela metade, são fonte de erros difíceis. Nesta aula, resolvemos isso com os construtores: um mecanismo que permite exigir e definir as características de um objeto no exato momento de sua criação, garantindo que todo objeto nasça completo e válido. É um passo decisivo rumo a objetos robustos, que nunca existem quebrados.
O problema do objeto pela metade
Sintamos o problema concretamente. Com o que sabemos, criar uma pessoa exige múltiplos passos, e nada garante que os demos todos:
Pessoa p = new Pessoa();
p.Nome = "Ana";
// Esquecemos de definir a Idade e a Cidade!
Console.WriteLine(p.Idade); // exibe 0 — um valor padrão sem sentido para uma idade
Ao criar a pessoa e esquecer de preencher Idade, ela fica com o valor padrão do tipo (zero, para inteiros), que aqui é logicamente absurdo — uma pessoa de zero anos, criada por descuido, não por intenção. O objeto existe, mas está incompleto, e essa incompletude pode causar erros bem mais adiante no programa, longe da origem, dificultando o diagnóstico. Precisamos de uma forma de garantir que, ao criar uma pessoa, seus dados essenciais sejam fornecidos ali mesmo, na criação — de modo que seja impossível criar uma pessoa sem nome. Essa forma é o construtor.
O construtor: um método especial de criação
Um construtor é um método especial que roda automaticamente no momento em que um objeto é criado (no new), e cuja função é inicializar o objeto — dar-lhe seu estado inicial válido. Ele tem duas particularidades que o distinguem de um método comum: seu nome é exatamente o nome da classe, e ele não tem tipo de retorno (nem mesmo void). Veja um construtor para a classe Pessoa:
class Pessoa
{
public string Nome { get; set; }
public int Idade { get; set; }
public string Cidade { get; set; }
// CONSTRUTOR: mesmo nome da classe, sem tipo de retorno.
// Recebe os dados essenciais e os guarda no objeto que está nascendo.
public Pessoa(string nome, int idade, string cidade)
{
Nome = nome;
Idade = idade;
Cidade = cidade;
}
}
O construtor recebe parâmetros (as aulas de métodos, Fase 3!) — aqui, nome, idade e cidade — e usa esses parâmetros para preencher as properties do objeto. A partir do momento em que uma classe tem esse construtor, criar uma pessoa passa a exigir que você forneça esses dados na criação:
// Agora a criação exige os dados, tudo numa linha:
Pessoa p = new Pessoa("Ana", 30, "Recife");
Console.WriteLine(p.Nome); // Ana
Console.WriteLine(p.Idade); // 30
Compare com o antes: em vez de criar vazio e preencher em várias linhas, criamos a pessoa já completa, numa única expressão new Pessoa("Ana", 30, "Recife"). Os argumentos entre parênteses são entregues ao construtor, que os usa para inicializar o objeto. E — este é o ganho central — agora é impossível criar uma pessoa sem fornecer esses dados: new Pessoa(), sem argumentos, deixa de funcionar (já explicaremos por quê), de modo que nenhuma pessoa nasce sem nome, idade ou cidade. O objeto nasce completo por construção.
Validando na criação: objetos que nascem válidos
O construtor não serve apenas para preencher; ele é o lugar ideal para validar, garantindo que o objeto não apenas esteja completo, mas seja válido. Como o construtor é o único ponto por onde todo objeto passa ao nascer, uma verificação ali protege todos os objetos daquela classe:
class Pessoa
{
public string Nome { get; set; }
public int Idade { get; set; }
public Pessoa(string nome, int idade)
{
// Validação: garante que o objeto nasça num estado coerente.
if (string.IsNullOrWhiteSpace(nome))
{
Console.WriteLine("Aviso: nome vazio. Usando 'Desconhecido'.");
Nome = "Desconhecido";
}
else
{
Nome = nome;
}
if (idade < 0)
{
Console.WriteLine("Aviso: idade negativa. Usando 0.");
Idade = 0;
}
else
{
Idade = idade;
}
}
}
Aqui, o construtor rejeita um nome vazio (usando string.IsNullOrWhiteSpace, que aprendemos a mencionar na Aula #09, e que detecta texto vazio ou só com espaços) e uma idade negativa, substituindo-os por valores seguros. O resultado é uma garantia poderosa: toda Pessoa que existir no programa terá passado por essa validação, porque todas nasceram por este construtor. Objetos inválidos tornam-se impossíveis de criar — e isso é imensamente valioso, pois elimina, na origem, uma categoria inteira de erros que, de outra forma, apareceriam espalhados pelo programa. (Mais adiante, aprenderemos uma forma mais enérgica de rejeitar valores inválidos, interrompendo a criação com um erro em vez de apenas corrigir; por ora, corrigir para um valor seguro ilustra bem o papel do construtor.)
O construtor padrão e seu desaparecimento
Uma pergunta natural: nas aulas anteriores, criávamos objetos com new Pessoa() sem construtor nenhum escrito — como isso funcionava? A resposta é que, quando você não escreve nenhum construtor, o C# fornece automaticamente um construtor padrão invisível, sem parâmetros, que apenas cria o objeto com os valores padrão de cada característica. Era ele que permitia new Pessoa() e o preenchimento posterior.
Mas há uma regra importante que pega muitos iniciantes: assim que você escreve qualquer construtor próprio, o construtor padrão invisível desaparece. Por isso, na seção anterior, new Pessoa() (sem argumentos) deixou de funcionar depois que criamos o construtor com parâmetros — o C# passou a exigir que você use o construtor que você definiu, com seus argumentos. Isso é, na verdade, o comportamento desejado: se você criou um construtor que exige nome e idade, é porque quer que toda pessoa tenha nome e idade, e permitir a criação sem eles contrariaria sua intenção. Caso você queira, por algum motivo, permitir também a criação sem argumentos, pode escrever explicitamente um segundo construtor sem parâmetros — objetos podem ter vários construtores, oferecendo diferentes formas de criação. Mas o comum, e o recomendado para objetos que precisam de dados essenciais, é ter um construtor que os exija, tornando impossível a criação incompleta.
Um exemplo completo
Reunindo tudo, veja uma classe ContaBancaria cujo construtor garante um nascimento completo e válido:
class ContaBancaria
{
public string Titular { get; set; }
public decimal Saldo { get; set; }
public ContaBancaria(string titular, decimal saldoInicial)
{
if (string.IsNullOrWhiteSpace(titular))
Titular = "Sem nome";
else
Titular = titular;
if (saldoInicial < 0)
Saldo = 0;
else
Saldo = saldoInicial;
}
}
// Criando contas — cada uma nasce completa e validada:
ContaBancaria conta1 = new ContaBancaria("Ana", 1000m);
ContaBancaria conta2 = new ContaBancaria("Beto", 500m);
Console.WriteLine($"{conta1.Titular}: R$ {conta1.Saldo}"); // Ana: R$ 1000
Console.WriteLine($"{conta2.Titular}: R$ {conta2.Saldo}"); // Beto: R$ 500
Cada conta é criada numa única expressão, com seus dados essenciais fornecidos e validados no nascimento. Não há como criar uma conta sem titular ou com saldo negativo — o construtor cuida disso, uma vez, para todas. Este é o padrão que você adotará: dar às suas classes construtores que exijam e validem os dados essenciais, de modo que seus objetos sempre nasçam prontos para uso.
O objeto pela metade deixou de ser possível. O construtor é o método especial que roda no instante da criação e permite exigir, ali mesmo, os dados sem os quais aquele objeto não faria sentido. Quem cria é obrigado a fornecê-los, e o compilador cobra.
Combinado com a validação, isso produz uma garantia forte: um objeto que existe é um objeto válido. Não há janela entre a criação e o preenchimento em que ele esteja num estado inconsistente. Vale notar o detalhe que costuma surpreender — ao declarar um construtor próprio, o construtor padrão sem argumentos deixa de existir, e isso é intencional, porque permite proibir a criação incompleta.
Fontes e leituras recomendadas
- Construtores em C# — a referência oficial sobre construtores e inicialização de objetos; a base desta aula.
- Usando construtores — como definir e chamar construtores, com exemplos.
- O construtor padrão — o construtor implícito e por que ele desaparece ao escrever um próprio.
- string.IsNullOrWhiteSpace — a ferramenta de validação de texto vazio usada nos exemplos.
- Sobrecarga de construtores — como oferecer múltiplas formas de criar um objeto, para aprofundar.
Exercícios
Exercício 1
Adicione à classe Produto (de aulas anteriores) um construtor que receba nome, preco e estoque e os atribua às respectivas properties. Depois, crie um produto usando o construtor, numa única linha, e exiba seus dados. Compare com a forma antiga (criar vazio e preencher depois).
Ver resposta
✓ Resposta: Exemplo:
class Produto
{
public string Nome { get; set; }
public decimal Preco { get; set; }
public int Estoque { get; set; }
public Produto(string nome, decimal preco, int estoque)
{
Nome = nome; Preco = preco; Estoque = estoque;
}
}
Produto p = new Produto("Caderno", 12.5m, 100);
Console.WriteLine($"{p.Nome}: R$ {p.Preco}, {p.Estoque} un.");
Com o construtor, o produto é criado completo numa única linha, em vez da forma antiga (criar vazio com new Produto() e preencher cada property em linhas separadas), que era mais verbosa e não garantia o preenchimento.
Exercício 2
Adicione validação ao construtor de Produto: se o preço for negativo, use zero; se o nome for vazio, use "Sem nome". Teste criando um produto com valores válidos e outro com valores inválidos, confirmando que os inválidos são corrigidos no nascimento.
Ver resposta
✓ Resposta: Exemplo:
public Produto(string nome, decimal preco, int estoque)
{
Nome = string.IsNullOrWhiteSpace(nome) ? "Sem nome" : nome;
Preco = preco < 0 ? 0 : preco;
Estoque = estoque < 0 ? 0 : estoque;
}
Produto ok = new Produto("Caneta", 2m, 50); // válido
Produto ruim = new Produto("", -5m, -3); // corrigido no nascimento
Console.WriteLine($"{ruim.Nome}: R$ {ruim.Preco}, {ruim.Estoque}"); // Sem nome: R$ 0, 0
(Usei o operador condicional ? : da Aula #21 para abreviar as validações.) Os valores inválidos do segundo produto são corrigidos pelo construtor, que garante um estado coerente no nascimento.
Exercício 3
Explique, com suas palavras, o que acontece com o "construtor padrão invisível" quando você escreve um construtor próprio. Por que new Pessoa() (sem argumentos) deixa de funcionar depois que você cria um construtor com parâmetros, e por que esse comportamento é, na verdade, desejável?
Ver resposta
✓ Resposta: Quando você não escreve nenhum construtor, o C# fornece um construtor padrão invisível, sem parâmetros, que permite new Pessoa(). Assim que você escreve um construtor próprio, esse construtor padrão invisível desaparece — o C# passa a oferecer apenas o(s) construtor(es) que você definiu. Por isso new Pessoa() deixa de funcionar após criar um construtor com parâmetros: a linguagem agora exige que você use o construtor definido, com seus argumentos. Esse comportamento é desejável porque, se você criou um construtor que exige nome e idade, é sinal de que toda pessoa deve ter esses dados; permitir a criação sem eles contrariaria sua intenção e reabriria a porta para objetos incompletos.
Exercício 4
Crie uma classe Retangulo com properties Largura e Altura e um construtor que as receba e valide (ambas devem ser positivas; se não forem, use 1). Crie alguns retângulos, incluindo um com valores inválidos, e confirme o comportamento do construtor.
Ver resposta
✓ Resposta: Exemplo:
class Retangulo
{
public double Largura { get; set; }
public double Altura { get; set; }
public Retangulo(double largura, double altura)
{
Largura = largura > 0 ? largura : 1;
Altura = altura > 0 ? altura : 1;
}
}
Retangulo r1 = new Retangulo(4, 3); // válido
Retangulo r2 = new Retangulo(-2, 0); // inválido → vira 1 x 1
Console.WriteLine($"{r2.Largura} x {r2.Altura}"); // 1 x 1
O construtor garante que largura e altura sejam sempre positivas, corrigindo valores inválidos para 1.
Exercício 5
Sem código: explique por que validar no construtor é especialmente poderoso, em comparação com validar em cada lugar do programa que usa o objeto. O que o fato de "todo objeto passar pelo construtor ao nascer" garante sobre a validade dos objetos daquela classe?
Ver resposta
✓ Resposta: Validar no construtor é poderoso porque o construtor é o único ponto de passagem obrigatório na criação de qualquer objeto daquela classe: como todo objeto nasce por um construtor, uma validação ali é aplicada, sem exceção, a todos os objetos criados. Isso garante que não exista nenhum objeto daquela classe em estado inválido, pois seria impossível criar um sem passar pela verificação. Em contraste, validar em cada lugar do programa que usa o objeto é frágil e repetitivo: seria preciso lembrar de checar em todos os pontos, e esquecer um deixaria passar um objeto inválido. Concentrar a validação no construtor centraliza a garantia num só lugar e a torna inescapável — o objeto se responsabiliza por nascer válido, e ninguém precisa vigiar isso de fora.