Projeto Conta Bancária (classes)
Uma conta bancária é o exemplo clássico de orientação a objetos, e por um bom motivo: ela tem dados (o titular, o saldo) e regras (não dá para sacar mais do que tem, não dá para depositar um valor negativo). Neste projeto, a Ana constrói as contas da lojinha em quatro passos: primeiro uma classe ingênua, que deixa o saldo ficar negativo; depois uma classe que protege as próprias regras; depois o extrato e a transferência; e, no fim, uma conta poupança que herda tudo da conta comum.
- Criar uma classe com campos, métodos e objetos com
new. - Encapsular: propriedades com
private sete um construtor. - Recusar operações inválidas com throw e tratar com try e catch.
- Guardar o extrato numa List de records e usar parâmetros opcionais.
- Herdar com herança, chamar o construtor da classe-mãe com base e sobrescrever com virtual e override.
- Dinheiro sempre em decimal, mostrado com
:C.
O que vamos construir
No fim, a Ana tem uma conta corrente e uma poupança. Ela deposita R$ 1.500 na corrente, passa R$ 1.000 para a poupança, e a poupança rende 0,5% no mês:
$ dotnet run Conta corrente Ana R$ 500,00 Poupança Ana R$ 1.005,00 Extrato de Ana Transferência de Ana R$ 1.000,00 Rendimento de 0,5% R$ 5,00 Saldo R$ 1.005,00
- Uma classe Conta (e o problema dos campos públicos).
- Encapsular: a conta protege o saldo.
- Extrato e transferência.
- A poupança herda da conta.
Antes de começar
Você precisa ter visto Classes e Objetos, Propriedades, Construtores, Encapsulamento e throw; para a versão 4, Herança e virtual e override.
Cada classe mora no seu arquivo, como em todo projeto C# de verdade: Conta.cs, depois Movimentacao.cs e ContaPoupanca.cs. Por isso é um projeto:
$ cd projetos $ dotnet new console -o Banco O modelo "Aplicativo do Console" foi criado com êxito. Processando ações pós-criação... Restaurando /home/ana/projetos/Banco/Banco.csproj: Determinando os projetos a serem restaurados... /home/ana/projetos/Banco/Banco.csproj restaurado (em 104 ms). A restauração foi bem-sucedida. $ cd Banco
O dotnet run compila todos os .cs da pasta juntos, então o Program.cs enxerga a classe Conta sem nenhum using. Nas caixas Experimente, cada arquivo é uma aba.
Versão 1 · Uma classe Conta
Uma classe é a "planta" de um tipo de objeto: diz que dados ele guarda e o que ele sabe fazer. A conta guarda o titular e o saldo, e sabe depositar e sacar.
class Conta
{
public string Titular = "";
public decimal Saldo;
public void Depositar(decimal valor)
{
Saldo += valor;
}
public void Sacar(decimal valor)
{
Saldo -= valor;
}
}
Conta conta = new Conta();
conta.Titular = "Ana";
conta.Depositar(100m);
conta.Sacar(30m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
conta.Sacar(500m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
conta.Saldo = 1_000_000m;
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
- class Conta
- Cria o tipo
Conta. Por convenção, cada classe fica num arquivo com o mesmo nome. - public string Titular = "";
- Um campo: uma variável que cada conta tem.
publicquer dizer que qualquer parte do programa pode ler e mudar. O= ""evita o aviso de referência nula. - public decimal Saldo;
- Dinheiro sempre em
decimal. Um campo numérico começa em 0. - public void Depositar(decimal valor)
- Um método: algo que a conta sabe fazer. Dentro dele,
Saldoé o saldo desta conta (veja this). - Conta conta = new Conta();
- O
newcria um objeto a partir da planta: uma conta de verdade, com o seu próprio saldo. - conta.Depositar(100m);
- Chama o método naquele objeto. O
mfaz do100umdecimal. - {conta.Saldo:C}
- Mostra como dinheiro, no formato da máquina:
R$ 70,00(Formatar Números).
$ dotnet run Ana: R$ 70,00 Ana: -R$ 430,00 Ana: R$ 1.000.000,00
A primeira linha está certa: 100 − 30 = 70. As outras duas mostram o problema desta versão. A conta aceitou um saque de R$ 500 com R$ 70 de saldo e ficou negativa. E, pior, a última linha do Program.cs mudou o saldo direto, para um milhão, sem depósito nenhum. A classe tem os dados, mas não tem como proteger as regras: qualquer um mexe em qualquer coisa. A versão 2 conserta isso.
Missão (versão 1): cada new Conta() é uma conta independente. No fim do Program.cs, crie a conta do Bruno, deposite 50m e mostre Bruno: R$ 50,00. A conta da Ana não deve mudar.
Conta conta = new Conta();
conta.Titular = "Ana";
conta.Depositar(100m);
conta.Sacar(30m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
conta.Sacar(500m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
conta.Saldo = 1_000_000m;
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
class Conta
{
public string Titular = "";
public decimal Saldo;
public void Depositar(decimal valor)
{
Saldo += valor;
}
public void Sacar(decimal valor)
{
Saldo -= valor;
}
}
Conta conta = new Conta();
conta.Titular = "Ana";
conta.Depositar(100m);
conta.Sacar(30m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
conta.Sacar(500m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
conta.Saldo = 1_000_000m;
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
Conta bruno = new Conta();
bruno.Titular = "Bruno";
bruno.Depositar(50m);
Console.WriteLine($"{bruno.Titular}: {bruno.Saldo:C}");
Versão 2 · Encapsular: a conta protege o saldo
A ideia do encapsulamento é a do caixa do banco: você não abre o cofre, você pede ao caixa, e ele confere se pode. Três mudanças:
- O saldo vira uma propriedade que todos podem ler, mas só a própria classe pode mudar (
private set). - Um construtor obriga a dizer o titular na hora de criar a conta.
DepositareSacarconferem o valor e, se ele for inválido, lançam uma exceção (throw) em vez de fazer a conta errada.
class Conta
{
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular)
{
if (string.IsNullOrWhiteSpace(titular))
{
throw new ArgumentException("O titular não pode ficar vazio.");
}
Titular = titular;
}
public void Depositar(decimal valor)
{
if (valor <= 0)
{
throw new ArgumentException("O depósito tem de ser maior que zero.");
}
Saldo += valor;
}
public void Sacar(decimal valor)
{
if (valor <= 0)
{
throw new ArgumentException("O saque tem de ser maior que zero.");
}
if (valor > Saldo)
{
throw new InvalidOperationException($"Saldo insuficiente: o saldo é {Saldo:C} e o saque, {valor:C}.");
}
Saldo -= valor;
}
}
Conta conta = new Conta("Ana");
conta.Depositar(100m);
conta.Sacar(30m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
try
{
conta.Sacar(500m);
}
catch (InvalidOperationException erro)
{
Console.WriteLine($"Não deu: {erro.Message}");
}
try
{
conta.Depositar(-20m);
}
catch (ArgumentException erro)
{
Console.WriteLine($"Não deu: {erro.Message}");
}
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
- public string Titular { get; }
- Uma propriedade só de leitura: recebe valor no construtor e nunca mais muda.
- public decimal Saldo { get; private set; }
- Todo mundo lê (
getpúblico); só o código dentro da classe muda (private set). Assim, o único jeito de mexer no saldo é porDepositareSacar, que conferem as regras. - public Conta(string titular)
- O construtor: roda no
new. Agoranew Conta()sem titular nem compila; é precisonew Conta("Ana"). - string.IsNullOrWhiteSpace(titular)
- "É nulo, vazio ou só espaços?". Um titular assim é recusado.
- throw new ArgumentException("…");
- Lança uma exceção: a operação para ali, nada muda, e quem chamou fica sabendo.
ArgumentExceptionquer dizer "o argumento que você passou é inválido". - throw new InvalidOperationException(…)
- "A operação não pode ser feita agora": o valor é bom, mas o saldo não dá.
- catch (InvalidOperationException erro)
- No
Program.cs, quem usa a conta decide o que fazer com a recusa.erro.Messageé o texto que a conta escreveu nothrow.
$ dotnet run Ana: R$ 70,00 Não deu: Saldo insuficiente: o saldo é R$ 70,00 e o saque, R$ 500,00. Não deu: O depósito tem de ser maior que zero. Ana: R$ 70,00
O saque grande e o depósito negativo foram recusados, e o saldo continuou em R$ 70,00.
CS0272
A Ana testou a proteção com a linha que, na versão 1, transformava o saldo em um milhão:
Conta conta = new Conta("Ana");
conta.Depositar(100m);
conta.Saldo = 1_000_000m;
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
$ dotnet run /home/ana/projetos/Banco/Program.cs(3,1): error CS0272: A propriedade ou o indexador "Conta.Saldo" não pode ser usado neste contexto porque o acessador set é inacessível [/home/ana/projetos/Banco/Banco.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o set do Saldo é private, então, fora da classe Conta, ninguém pode atribuir um valor a ele (em inglês, the set accessor is inaccessible). É exatamente o que queríamos: o erro aparece na compilação, antes de o programa rodar.
Como consertar: não é para consertar a classe, é para usar a porta certa: conta.Depositar(…).
try: a exceção derruba o programa
Sem o try/catch em volta do saque grande:
$ dotnet run Ana: R$ 70,00 Unhandled exception. System.InvalidOperationException: Saldo insuficiente: o saldo é R$ 70,00 e o saque, R$ 500,00. at Conta.Sacar(Decimal valor) in /home/ana/projetos/Banco/Conta.cs:line 32 at Program.<Main>$(String[] args) in /home/ana/projetos/Banco/Program.cs:line 6 $ echo $? 134
O que significa: o rastro (stack trace) se lê de cima para baixo: a exceção nasceu em Conta.Sacar, na linha 32 do Conta.cs (o throw), que foi chamado pela linha 6 do Program.cs. Ninguém a pegou, e o programa terminou com o código 134. A mensagem é a que a própria classe escreveu: uma boa mensagem de exceção diz o que deu errado e com quais valores.
Como consertar: envolva a chamada num try e pegue a exceção que você sabe tratar, como no Program.cs da versão 2 (veja try e catch).
Missão (versão 2): o Sacar da aba Conta.cs ainda aceita sacar mais do que o saldo, e a conta fica negativa. Antes do Saldo -= valor;, recuse o saque: if (valor > Saldo), lance uma InvalidOperationException com a mensagem $"Saldo insuficiente: o saldo é {Saldo:C} e o saque, {valor:C}.".
Conta conta = new Conta("Ana");
conta.Depositar(100m);
conta.Sacar(30m);
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
try
{
conta.Sacar(500m);
}
catch (InvalidOperationException erro)
{
Console.WriteLine($"Não deu: {erro.Message}");
}
try
{
conta.Depositar(-20m);
}
catch (ArgumentException erro)
{
Console.WriteLine($"Não deu: {erro.Message}");
}
Console.WriteLine($"{conta.Titular}: {conta.Saldo:C}");
class Conta
{
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular)
{
if (string.IsNullOrWhiteSpace(titular))
{
throw new ArgumentException("O titular não pode ficar vazio.");
}
Titular = titular;
}
public void Depositar(decimal valor)
{
if (valor <= 0)
{
throw new ArgumentException("O depósito tem de ser maior que zero.");
}
Saldo += valor;
}
public void Sacar(decimal valor)
{
if (valor <= 0)
{
throw new ArgumentException("O saque tem de ser maior que zero.");
}
// recuse o saque maior que o saldo
Saldo -= valor;
}
}
class Conta
{
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular)
{
if (string.IsNullOrWhiteSpace(titular))
{
throw new ArgumentException("O titular não pode ficar vazio.");
}
Titular = titular;
}
public void Depositar(decimal valor)
{
if (valor <= 0)
{
throw new ArgumentException("O depósito tem de ser maior que zero.");
}
Saldo += valor;
}
public void Sacar(decimal valor)
{
if (valor <= 0)
{
throw new ArgumentException("O saque tem de ser maior que zero.");
}
if (valor > Saldo)
{
throw new InvalidOperationException($"Saldo insuficiente: o saldo é {Saldo:C} e o saque, {valor:C}.");
}
Saldo -= valor;
}
}
Versão 3 · Extrato e transferência
Um banco não guarda só o saldo: guarda cada movimentação, para o extrato. Cada uma tem uma descrição e um valor (positivo para entradas, negativo para saídas). É um dado simples, que não muda depois de criado: um record, no seu próprio arquivo.
record Movimentacao(string Descricao, decimal Valor);
A Conta ganha a lista de movimentações, uma descrição opcional em Depositar e Sacar, e dois métodos novos: Transferir e MostrarExtrato.
class Conta
{
private readonly List<Movimentacao> _movimentacoes = new();
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular)
{
if (string.IsNullOrWhiteSpace(titular))
{
throw new ArgumentException("O titular não pode ficar vazio.");
}
Titular = titular;
}
public void Depositar(decimal valor, string descricao = "Depósito")
{
if (valor <= 0)
{
throw new ArgumentException("O depósito tem de ser maior que zero.");
}
Saldo += valor;
_movimentacoes.Add(new Movimentacao(descricao, valor));
}
public void Sacar(decimal valor, string descricao = "Saque")
{
if (valor <= 0)
{
throw new ArgumentException("O saque tem de ser maior que zero.");
}
if (valor > Saldo)
{
throw new InvalidOperationException($"Saldo insuficiente: o saldo é {Saldo:C} e o saque, {valor:C}.");
}
Saldo -= valor;
_movimentacoes.Add(new Movimentacao(descricao, -valor));
}
public void Transferir(Conta destino, decimal valor)
{
Sacar(valor, $"Transferência para {destino.Titular}");
destino.Depositar(valor, $"Transferência de {Titular}");
}
public void MostrarExtrato()
{
Console.WriteLine($"Extrato de {Titular}");
foreach (Movimentacao m in _movimentacoes)
{
Console.WriteLine($" {m.Descricao,-26} {m.Valor,12:C}");
}
Console.WriteLine($" {"Saldo",-26} {Saldo,12:C}");
}
}
Conta ana = new Conta("Ana");
Conta bruno = new Conta("Bruno");
ana.Depositar(1500m);
ana.Sacar(200m);
ana.Transferir(bruno, 350.75m);
bruno.Depositar(80m, "Venda do caderno");
ana.MostrarExtrato();
Console.WriteLine();
bruno.MostrarExtrato();
- private readonly List<Movimentacao> _movimentacoes = new();
- A lista é
private: só a conta acrescenta movimentações, e sempre junto com a mudança do saldo.readonlyquer dizer que o campo sempre aponta para a mesma lista (readonly e init). O_no começo é a convenção para campos privados. - string descricao = "Depósito"
- Um parâmetro opcional: quem não passar descrição ganha
"Depósito".ana.Depositar(1500m)continua funcionando. - new Movimentacao(descricao, -valor)
- No saque, o valor entra negativo: o extrato mostra
-R$ 200,00. - public void Transferir(Conta destino, decimal valor)
- Recebe outra conta como parâmetro. Primeiro saca desta (se o saldo não der, o
Sacarlança a exceção e o depósito nem acontece), depois deposita na outra. - {m.Descricao,-26} {m.Valor,12:C}
- Colunas alinhadas: a descrição em 26 posições à esquerda, o valor em 12 à direita.
$ ls Banco.csproj bin Conta.cs Movimentacao.cs obj Program.cs $ dotnet run Extrato de Ana Depósito R$ 1.500,00 Saque -R$ 200,00 Transferência para Bruno -R$ 350,75 Saldo R$ 949,25 Extrato de Bruno Transferência de Ana R$ 350,75 Venda do caderno R$ 80,00 Saldo R$ 430,75
Transferir usa Sacar e Depositar?
Daria para escrever Saldo -= valor; destino.Saldo += valor; (dentro da classe, o private set deixa, até na outra conta). Mas aí a transferência pularia as regras: aceitaria valor negativo e saldo insuficiente, e não apareceria no extrato. Reaproveitando os métodos, toda regra fica num lugar só.
Missão (versão 3): o Transferir da aba Conta.cs está vazio: o Bruno não recebe nada. Escreva as duas linhas: Sacar(valor, $"Transferência para {destino.Titular}"); e destino.Depositar(valor, $"Transferência de {Titular}");.
Conta ana = new Conta("Ana");
Conta bruno = new Conta("Bruno");
ana.Depositar(1500m);
ana.Sacar(200m);
ana.Transferir(bruno, 350.75m);
bruno.Depositar(80m, "Venda do caderno");
ana.MostrarExtrato();
Console.WriteLine();
bruno.MostrarExtrato();
class Conta
{
private readonly List<Movimentacao> _movimentacoes = new();
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular)
{
if (string.IsNullOrWhiteSpace(titular))
{
throw new ArgumentException("O titular não pode ficar vazio.");
}
Titular = titular;
}
public void Depositar(decimal valor, string descricao = "Depósito")
{
if (valor <= 0)
{
throw new ArgumentException("O depósito tem de ser maior que zero.");
}
Saldo += valor;
_movimentacoes.Add(new Movimentacao(descricao, valor));
}
public void Sacar(decimal valor, string descricao = "Saque")
{
if (valor <= 0)
{
throw new ArgumentException("O saque tem de ser maior que zero.");
}
if (valor > Saldo)
{
throw new InvalidOperationException($"Saldo insuficiente: o saldo é {Saldo:C} e o saque, {valor:C}.");
}
Saldo -= valor;
_movimentacoes.Add(new Movimentacao(descricao, -valor));
}
public void Transferir(Conta destino, decimal valor)
{
// saque nesta conta e depósito na conta destino, com as descrições
}
public void MostrarExtrato()
{
Console.WriteLine($"Extrato de {Titular}");
foreach (Movimentacao m in _movimentacoes)
{
Console.WriteLine($" {m.Descricao,-26} {m.Valor,12:C}");
}
Console.WriteLine($" {"Saldo",-26} {Saldo,12:C}");
}
}
record Movimentacao(string Descricao, decimal Valor);
class Conta
{
private readonly List<Movimentacao> _movimentacoes = new();
public string Titular { get; }
public decimal Saldo { get; private set; }
public Conta(string titular)
{
if (string.IsNullOrWhiteSpace(titular))
{
throw new ArgumentException("O titular não pode ficar vazio.");
}
Titular = titular;
}
public void Depositar(decimal valor, string descricao = "Depósito")
{
if (valor <= 0)
{
throw new ArgumentException("O depósito tem de ser maior que zero.");
}
Saldo += valor;
_movimentacoes.Add(new Movimentacao(descricao, valor));
}
public void Sacar(decimal valor, string descricao = "Saque")
{
if (valor <= 0)
{
throw new ArgumentException("O saque tem de ser maior que zero.");
}
if (valor > Saldo)
{
throw new InvalidOperationException($"Saldo insuficiente: o saldo é {Saldo:C} e o saque, {valor:C}.");
}
Saldo -= valor;
_movimentacoes.Add(new Movimentacao(descricao, -valor));
}
public void Transferir(Conta destino, decimal valor)
{
Sacar(valor, $"Transferência para {destino.Titular}");
destino.Depositar(valor, $"Transferência de {Titular}");
}
public void MostrarExtrato()
{
Console.WriteLine($"Extrato de {Titular}");
foreach (Movimentacao m in _movimentacoes)
{
Console.WriteLine($" {m.Descricao,-26} {m.Valor,12:C}");
}
Console.WriteLine($" {"Saldo",-26} {Saldo,12:C}");
}
}
Versão 4 · A poupança herda da conta
A Ana quer uma poupança para a reserva da loja. Uma poupança é uma conta (tem titular, saldo, depósito, saque, extrato) com uma coisa a mais: ela rende juros. Copiar a classe Conta inteira seria duplicar tudo. Com herança, a ContaPoupanca herda tudo da Conta e só acrescenta o que é dela.
Na Conta, uma linha nova: uma propriedade que diz o tipo da conta, marcada como virtual (as filhas podem trocá-la):
public virtual string Tipo => "Conta corrente";
class ContaPoupanca : Conta
{
public decimal TaxaMensal { get; }
public override string Tipo => "Poupança";
public ContaPoupanca(string titular, decimal taxaMensal) : base(titular)
{
TaxaMensal = taxaMensal;
}
public void RenderJuros()
{
decimal juros = Math.Round(Saldo * TaxaMensal, 2);
if (juros > 0)
{
Depositar(juros, $"Rendimento de {TaxaMensal:P1}");
}
}
}
Conta corrente = new Conta("Ana");
ContaPoupanca reserva = new ContaPoupanca("Ana", 0.005m);
corrente.Depositar(1500m);
corrente.Transferir(reserva, 1000m);
reserva.RenderJuros();
List<Conta> contas = [corrente, reserva];
foreach (Conta conta in contas)
{
Console.WriteLine($"{conta.Tipo,-15} {conta.Titular,-6} {conta.Saldo,12:C}");
}
Console.WriteLine();
reserva.MostrarExtrato();
- class ContaPoupanca : Conta
- O
:diz "é umaConta": a poupança herdaTitular,Saldo,Depositar,Sacar,TransferireMostrarExtrato. - public virtual string Tipo => "Conta corrente";
- Na
Conta: uma propriedade calculada (o=>devolve o valor), que as classes filhas podem substituir. - public override string Tipo => "Poupança";
- Na poupança: substitui (override) a versão da mãe.
- : base(titular)
- O construtor da poupança chama primeiro o construtor da
Conta, que confere e guarda o titular (base). Depois guarda a taxa. - Math.Round(Saldo * TaxaMensal, 2)
- Os juros, arredondados para centavos.
Saldoé a propriedade herdada: a poupança pode ler, e muda só porDepositar. - {TaxaMensal:P1}
- Formato de porcentagem com 1 casa:
0.005mvira0,5%. - List<Conta> contas = [corrente, reserva];
- Uma lista de
Contaaceita a poupança, porque toda poupança é uma conta. - conta.Tipo
- Para cada conta da lista, o C# usa o
Tipodo objeto de verdade: "Conta corrente" para uma, "Poupança" para a outra. Isso é o polimorfismo.
$ ls Banco.csproj bin Conta.cs ContaPoupanca.cs Movimentacao.cs obj Program.cs $ dotnet run Conta corrente Ana R$ 500,00 Poupança Ana R$ 1.005,00 Extrato de Ana Transferência de Ana R$ 1.000,00 Rendimento de 0,5% R$ 5,00 Saldo R$ 1.005,00
: base(titular): erro CS7036
$ dotnet run /home/ana/projetos/Banco/ContaPoupanca.cs(6,12): error CS7036: Não há nenhum argumento fornecido que corresponda ao parâmetro necessário 'titular' de 'Conta.Conta(string)' [/home/ana/projetos/Banco/Banco.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: todo construtor de uma classe filha chama um construtor da mãe. Sem o : base(…), o C# tenta chamar o construtor sem parâmetros da Conta, mas ele não existe: o único pede o titular. O erro aponta o construtor da poupança, na linha 6.
Como consertar: public ContaPoupanca(string titular, decimal taxaMensal) : base(titular).
virtual: erro CS0506
$ dotnet run /home/ana/projetos/Banco/ContaPoupanca.cs(4,28): error CS0506: "ContaPoupanca.Tipo": não é possível substituir o membro herdado "Conta.Tipo" porque ele não está marcado como virtual, abstract ou override [/home/ana/projetos/Banco/Banco.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: no C#, a classe-mãe decide o que pode ser substituído. Sem virtual no Tipo da Conta, o override da poupança não é permitido.
Como consertar: public virtual string Tipo => "Conta corrente"; na Conta (veja virtual e override).
Missão (versão 4): a lista mostra as duas contas como "Conta corrente". Na aba ContaPoupanca.cs, substitua o tipo: public override string Tipo => "Poupança";.
Conta corrente = new Conta("Ana");
ContaPoupanca reserva = new ContaPoupanca("Ana", 0.005m);
corrente.Depositar(1500m);
corrente.Transferir(reserva, 1000m);
reserva.RenderJuros();
List<Conta> contas = [corrente, reserva];
foreach (Conta conta in contas)
{
Console.WriteLine($"{conta.Tipo,-15} {conta.Titular,-6} {conta.Saldo,12:C}");
}
Console.WriteLine();
reserva.MostrarExtrato();
class ContaPoupanca : Conta
{
public decimal TaxaMensal { get; }
public ContaPoupanca(string titular, decimal taxaMensal) : base(titular)
{
TaxaMensal = taxaMensal;
}
public void RenderJuros()
{
decimal juros = Math.Round(Saldo * TaxaMensal, 2);
if (juros > 0)
{
Depositar(juros, $"Rendimento de {TaxaMensal:P1}");
}
}
}
class Conta
{
private readonly List<Movimentacao> _movimentacoes = new();
public string Titular { get; }
public decimal Saldo { get; private set; }
public virtual string Tipo => "Conta corrente";
public Conta(string titular)
{
if (string.IsNullOrWhiteSpace(titular))
{
throw new ArgumentException("O titular não pode ficar vazio.");
}
Titular = titular;
}
public void Depositar(decimal valor, string descricao = "Depósito")
{
if (valor <= 0)
{
throw new ArgumentException("O depósito tem de ser maior que zero.");
}
Saldo += valor;
_movimentacoes.Add(new Movimentacao(descricao, valor));
}
public void Sacar(decimal valor, string descricao = "Saque")
{
if (valor <= 0)
{
throw new ArgumentException("O saque tem de ser maior que zero.");
}
if (valor > Saldo)
{
throw new InvalidOperationException($"Saldo insuficiente: o saldo é {Saldo:C} e o saque, {valor:C}.");
}
Saldo -= valor;
_movimentacoes.Add(new Movimentacao(descricao, -valor));
}
public void Transferir(Conta destino, decimal valor)
{
Sacar(valor, $"Transferência para {destino.Titular}");
destino.Depositar(valor, $"Transferência de {Titular}");
}
public void MostrarExtrato()
{
Console.WriteLine($"Extrato de {Titular}");
foreach (Movimentacao m in _movimentacoes)
{
Console.WriteLine($" {m.Descricao,-26} {m.Valor,12:C}");
}
Console.WriteLine($" {"Saldo",-26} {Saldo,12:C}");
}
}
record Movimentacao(string Descricao, decimal Valor);
class ContaPoupanca : Conta
{
public decimal TaxaMensal { get; }
public override string Tipo => "Poupança";
public ContaPoupanca(string titular, decimal taxaMensal) : base(titular)
{
TaxaMensal = taxaMensal;
}
public void RenderJuros()
{
decimal juros = Math.Round(Saldo * TaxaMensal, 2);
if (juros > 0)
{
Depositar(juros, $"Rendimento de {TaxaMensal:P1}");
}
}
}
O programa completo
A versão final tem quatro arquivos na pasta ~/projetos/Banco. O Program.cs e o ContaPoupanca.cs estão na versão 4, e o Movimentacao.cs na versão 3. A Conta.cs completa:
class Conta
{
private readonly List<Movimentacao> _movimentacoes = new();
public string Titular { get; }
public decimal Saldo { get; private set; }
public virtual string Tipo => "Conta corrente";
public Conta(string titular)
{
if (string.IsNullOrWhiteSpace(titular))
{
throw new ArgumentException("O titular não pode ficar vazio.");
}
Titular = titular;
}
public void Depositar(decimal valor, string descricao = "Depósito")
{
if (valor <= 0)
{
throw new ArgumentException("O depósito tem de ser maior que zero.");
}
Saldo += valor;
_movimentacoes.Add(new Movimentacao(descricao, valor));
}
public void Sacar(decimal valor, string descricao = "Saque")
{
if (valor <= 0)
{
throw new ArgumentException("O saque tem de ser maior que zero.");
}
if (valor > Saldo)
{
throw new InvalidOperationException($"Saldo insuficiente: o saldo é {Saldo:C} e o saque, {valor:C}.");
}
Saldo -= valor;
_movimentacoes.Add(new Movimentacao(descricao, -valor));
}
public void Transferir(Conta destino, decimal valor)
{
Sacar(valor, $"Transferência para {destino.Titular}");
destino.Depositar(valor, $"Transferência de {Titular}");
}
public void MostrarExtrato()
{
Console.WriteLine($"Extrato de {Titular}");
foreach (Movimentacao m in _movimentacoes)
{
Console.WriteLine($" {m.Descricao,-26} {m.Valor,12:C}");
}
Console.WriteLine($" {"Saldo",-26} {Saldo,12:C}");
}
}
O que o projeto juntou: classes e objetos, propriedades com private set, construtores, throw e try/catch, um record numa List private readonly, parâmetros opcionais, herança com base, virtual e override e decimal com :C e :P1.
Desafios extras
- Exceção própria. Troque a
InvalidOperationExceptionpor umaSaldoInsuficienteExceptioncom as propriedadesSaldoeValor(veja Exceções Próprias). - Cheque especial. Crie uma
ContaCorrentecom umLimite, que deixa o saldo ficar negativo até o limite. Para isso, oSacardaContaprecisa servirtual, ou a regra "quanto pode sacar" pode virar uma propriedadevirtual decimal Disponivel. - Extrato só de leitura. Exponha as movimentações com
public IReadOnlyList<Movimentacao> Movimentacoes => _movimentacoes;e mova oMostrarExtratopara oProgram.cs: a conta guarda os dados, e quem mostra é o programa. - Testes. Num projeto de testes, confira que um saque maior que o saldo lança a exceção e não muda o saldo (veja Testes (xUnit)).
- Salvar as contas. Guarde as contas num arquivo JSON, como na Agenda. Cuidado: com o
private set, oJsonSerializernão consegue preencher o saldo sozinho (veja JSON).
Exercícios
1. O que public decimal Saldo { get; private set; } permite?
2. Por que o Sacar lança uma exceção em vez de só mostrar "Saldo insuficiente" com Console.WriteLine?
3. Em class ContaPoupanca : Conta, o que a poupança tem?
4. Na lista List<Conta>, por que conta.Tipo mostra "Poupança" para a reserva?
5. Complete o construtor da poupança, que chama o construtor da Conta:
6. Complete o modificador que deixa o saldo ser mudado só dentro da classe:
Resumo
- Uma classe junta dados e comportamento; cada
newcria um objeto independente. - Campos públicos deixam qualquer um quebrar as regras. Com
{ get; private set; }, o saldo só muda pelos métodos da classe (fora dela,CS0272). - O construtor obriga a criar o objeto num estado válido; os métodos recusam operações inválidas com
throw, e quem chama trata comtry/catch. - Um record numa lista
private readonlyguarda o extrato; reaproveitarSacareDepositarna transferência mantém as regras num lugar só. - Com herança, a filha recebe tudo da mãe e chama o construtor dela com
: base(…)(senão,CS7036). - Só membros
virtualpodem ser substituídos comoverride(senão,CS0506); numa lista da mãe, vale a versão do objeto de verdade.
Próximo projeto: Estoque da Lojinha (LINQ).