C# base

Na aula de Herança, o Cachorro não compilava quando o construtor de Animal pedia um nome. Faltava uma forma de a classe filha conversar com a mãe. Essa forma é a palavra base: com ela, a classe derivada chama o construtor da classe base e, quando precisa, os métodos dela.

Você vai aprender
  • Por que a classe filha precisa chamar o construtor da base.
  • : base(…): passar dados para o construtor da base.
  • A ordem em que os construtores rodam, inclusive com vários níveis.
  • Como escolher entre vários construtores da base, e a diferença entre base(…) e this(…).
  • base.Metodo(): reaproveitar a versão da base de um método.
  • Os construtores primários C# 12: class Cachorro(string nome) : Animal(nome).
  • Os erros CS7036, CS1729 e CS1511.

A ideia: montar a base primeiro

Pense numa casa de dois andares. Antes de erguer o segundo andar, alguém precisa construir o térreo, e o pedreiro do térreo precisa de informações: o tamanho do terreno, onde fica a porta. O pedreiro do segundo andar não pode simplesmente começar: ele primeiro passa as informações para o pedreiro do térreo e espera ele terminar.

Um objeto Cachorro é assim. Ele tem uma "parte animal" (o térreo, que vem de Animal) e uma "parte cachorro" (o andar de cima). Quando você faz new Cachorro(…), o construtor de Animal roda primeiro. Se ele precisa de um nome, o construtor de Cachorro é quem tem de entregar: : base(nome).


O problema: o construtor da base pede dados

var rex = new Cachorro("Rex", "vira-lata");
Console.WriteLine($"{rex.Nome} é um {rex.Raca}.");

class Animal
{
    public Animal(string nome)
    {
        Nome = nome;
    }

    public string Nome { get; }
}

class Cachorro : Animal
{
    public Cachorro(string nome, string raca)
    {
        Raca = raca;
    }

    public string Raca { get; }
}
$ dotnet run sem-base.cs
/home/ana/cs/sem-base.cs(16,12): error CS7036: Não há nenhum argumento fornecido que corresponda ao parâmetro necessário 'nome' de 'Animal.Animal(string)'

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O erro aponta o construtor de Cachorro (linha 16). Sem instrução nenhuma, o C# tenta montar a parte animal chamando Animal(), sem argumentos. Mas o único construtor de Animal exige o parâmetro nome: "não há nenhum argumento fornecido que corresponda ao parâmetro necessário 'nome'". Em inglês: There is no argument given that corresponds to the required parameter 'nome' of 'Animal.Animal(string)'.

Repare que o Cachorro até recebe o nome como parâmetro. Ele só não está entregando para ninguém.


: base(…), passando o nome para a base

var rex = new Cachorro("Rex", "vira-lata");
Console.WriteLine($"{rex.Nome} é um {rex.Raca}.");

class Animal
{
    public Animal(string nome)
    {
        Console.WriteLine($"1. Animal montado: {nome}");
        Nome = nome;
    }

    public string Nome { get; }
}

class Cachorro : Animal
{
    public Cachorro(string nome, string raca) : base(nome)
    {
        Console.WriteLine($"2. Cachorro montado: {raca}");
        Raca = raca;
    }

    public string Raca { get; }
}
$ dotnet run base-ctor.cs
1. Animal montado: Rex
2. Cachorro montado: vira-lata
Rex é um vira-lata.
public Cachorro(string nome, string raca)
O construtor da filha recebe tudo o que precisa: o que é dela (raca) e o que é da base (nome).
: base(nome)
Fica entre os parênteses e a chave de abertura. Quer dizer: "antes do meu corpo, chame o construtor da classe base com estes argumentos". O nome aqui é o parâmetro que o Cachorro recebeu.
1. Animal montado
A prova da ordem: o corpo do construtor de Animal roda primeiro…
2. Cachorro montado
…e só depois o corpo do construtor de Cachorro. Quando ele começa, o Nome já está preenchido.

O base(…) funciona como uma chamada de método comum: você pode passar valores fixos (base("sem nome")), contas (base(nome.Trim())) ou qualquer parâmetro do construtor da filha.

Missão: o programa não compila, com o erro CS7036. Rode para ver. Depois, faça o construtor de Gato passar o nome para o construtor de Animal com : base(nome).

var mia = new Gato("Mia", 3);
Console.WriteLine($"{mia.Nome} tem {mia.Idade} anos.");

class Animal
{
    public Animal(string nome)
    {
        Nome = nome;
    }

    public string Nome { get; }
}

class Gato : Animal
{
    public Gato(string nome, int idade)
    {
        Idade = idade;
    }

    public int Idade { get; }
}
var mia = new Gato("Mia", 3);
Console.WriteLine($"{mia.Nome} tem {mia.Idade} anos.");

class Animal
{
    public Animal(string nome)
    {
        Nome = nome;
    }

    public string Nome { get; }
}

class Gato : Animal
{
    public Gato(string nome, int idade) : base(nome)
    {
        Idade = idade;
    }

    public int Idade { get; }
}

Com vários níveis

Cada classe conversa só com a mãe direta. O Cachorro passa o nome para o Mamifero, que passa para o Animal. Os corpos rodam de cima para baixo, do térreo ao último andar:

var rex = new Cachorro("Rex");

class Animal
{
    public Animal(string nome)
    {
        Console.WriteLine($"Animal: {nome}");
    }
}

class Mamifero : Animal
{
    public Mamifero(string nome) : base(nome)
    {
        Console.WriteLine("Mamífero");
    }
}

class Cachorro : Mamifero
{
    public Cachorro(string nome) : base(nome)
    {
        Console.WriteLine("Cachorro");
    }
}
$ dotnet run tres.cs
Animal: Rex
Mamífero
Cachorro

Dentro de Cachorro, base é o Mamifero, e não o Animal. Não há como "pular" um andar.


Escolhendo o construtor da base

Se a base tem vários construtores (veja Sobrecarga), o base(…) escolhe qual chamar pelo número e pelo tipo dos argumentos, como em qualquer chamada:

var mia = new Gato("Mia");
var semNome = new Gato();
Console.WriteLine(mia.Nome);
Console.WriteLine(semNome.Nome);

class Animal
{
    public Animal()
    {
        Nome = "(sem nome)";
    }

    public Animal(string nome)
    {
        Nome = nome;
    }

    public string Nome { get; }
}

class Gato : Animal
{
    public Gato() : base()
    {
    }

    public Gato(string nome) : base(nome)
    {
    }
}
$ dotnet run base-varios.cs
Mia
(sem nome)

Gato() chama Animal(), e Gato(string) chama Animal(string). O : base() vazio é opcional: sem nada, o C# já chama o construtor sem parâmetros da base. Ele está escrito aqui só para deixar a escolha à vista. Se não existir um construtor da base com aqueles argumentos, o erro é o CS1729:

$ dotnet run base-args.cs          « com : base(nome, raca), mas Animal só recebe o nome
/home/ana/cs/base-args.cs(15,49): error CS1729: "Animal" não contém um construtor que aceita 2 argumentos

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

base(…) ou this(…)?

Na aula this, você viu que um construtor pode chamar outro construtor da mesma classe com : this(…). As duas formas ficam no mesmo lugar, mas apontam para andares diferentes:

var mia = new Gato("Mia");
var tom = new Gato();
Console.WriteLine($"{mia.Nome}, {mia.Vidas} vidas");
Console.WriteLine($"{tom.Nome}, {tom.Vidas} vidas");

class Animal
{
    public Animal(string nome)
    {
        Nome = nome;
    }

    public string Nome { get; }
}

class Gato : Animal
{
    public Gato(string nome) : base(nome)
    {
        Vidas = 7;
    }

    public Gato() : this("Gato sem nome")
    {
    }

    public int Vidas { get; }
}
$ dotnet run this-base.cs
Mia, 7 vidas
Gato sem nome, 7 vidas
public Gato(string nome) : base(nome)
Chama o construtor da mãe (Animal).
public Gato() : this("Gato sem nome")
Chama o outro construtor do próprio Gato, que por sua vez chama a base. Assim, o Vidas = 7 fica escrito num lugar só.

Um construtor escolhe um dos dois: ou : base(…), ou : this(…).


base.Metodo(): a versão da base

base também serve fora dos construtores. Na próxima aula, você vai ver que a filha pode substituir um método da mãe por uma versão dela (com virtual e override). Muitas vezes, a versão nova quer fazer o que a mãe já fazia, e mais um pouco. É aí que entra base.Metodo():

var rex = new Cachorro("Rex", "vira-lata");
rex.Apresentar();

class Animal
{
    public Animal(string nome)
    {
        Nome = nome;
    }

    public string Nome { get; }

    public virtual void Apresentar()
    {
        Console.WriteLine($"Eu sou {Nome}.");
    }
}

class Cachorro : Animal
{
    public Cachorro(string nome, string raca) : base(nome)
    {
        Raca = raca;
    }

    public string Raca { get; }

    public override void Apresentar()
    {
        base.Apresentar();
        Console.WriteLine($"Sou um cachorro {Raca}.");
    }
}
$ dotnet run base-metodo.cs
Eu sou Rex.
Sou um cachorro vira-lata.
public virtual void Apresentar()
virtual: a base avisa que as filhas podem trocar este método. Detalhes na próxima aula.
public override void Apresentar()
override: o Cachorro troca o Apresentar pela versão dele.
base.Apresentar();
Dentro da versão nova, chama a versão da base, que mostra "Eu sou Rex.". Sem o base., a linha Apresentar(); chamaria a própria versão do Cachorro, que chamaria de novo, e de novo, até o programa cair com Stack overflow..
Console.WriteLine($"Sou um cachorro {Raca}.");
O "mais um pouco" que só o cachorro faz.

Assim, a frase "Eu sou …" fica escrita só em Animal. Se a Ana mudar essa frase, o cachorro passa a usar a nova sem mudar nada.

base não existe em métodos static

Como this, a palavra base fala do objeto atual. Num método static (veja static), não há objeto:

Cachorro.Teste();

class Animal
{
    public string Nome { get; set; } = "";
}

class Cachorro : Animal
{
    public static void Teste()
    {
        Console.WriteLine(base.Nome);
    }
}
$ dotnet run base-static.cs
/home/ana/cs/base-static.cs(12,27): error CS1511: A palavra-chave 'base' não está disponível em um método estático

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O atalho: construtores primários C# 12

Quando o construtor só repassa valores, dá para escrever os parâmetros direto no nome da classe. Isso se chama construtor primário, e a chamada à base vai junto, depois dos dois-pontos:

var rex = new Cachorro("Rex", "vira-lata");
Console.WriteLine($"{rex.Nome} é um {rex.Raca}.");

class Animal(string nome)
{
    public string Nome { get; } = nome;
}

class Cachorro(string nome, string raca) : Animal(nome)
{
    public string Raca { get; } = raca;
}
$ dotnet run primario.cs
Rex é um vira-lata.
class Animal(string nome)
Os parâmetros do construtor ficam ao lado do nome da classe. Dentro da classe, nome pode ser usado para dar valor às propriedades.
class Cachorro(string nome, string raca) : Animal(nome)
O Animal(nome) depois dos dois-pontos faz o papel do : base(nome): diz qual a classe base e o que passar para ela.

É o mesmo programa do começo da aula, em bem menos linhas. Para classes grandes, ou com validação no construtor, a forma clássica com : base(…) continua sendo a mais clara. As duas aparecem muito em código real.


Quando dá errado

error CS7036: Não há nenhum argumento fornecido que corresponda ao parâmetro necessário '…' de '…'

O que aconteceu: a base só tem construtor com parâmetros, e o construtor da filha não tem : base(…) (ou a filha nem tem construtor).

Como resolver: escreva o construtor da filha com : base(…), passando o que a base pede. Outra saída é dar à base um construtor sem parâmetros, se fizer sentido.

error CS1729: "…" não contém um construtor que aceita N argumentos

O que aconteceu: o base(…) (ou um new) passou um número de argumentos que nenhum construtor da classe aceita.

Como resolver: confira quais construtores a base tem e passe exatamente o que um deles pede.

error CS1511: A palavra-chave 'base' não está disponível em um método estático

O que aconteceu: base. dentro de um método static.

Como resolver: tire o static do método, ou use um objeto: rex.Nome.

O programa cai com Stack overflow. dentro de um override

O que aconteceu: a versão nova chamou a si mesma (Apresentar();) em vez da versão da base.

Como resolver: escreva base.Apresentar();.


Referência

EscritaSignificado
public Filha(…) : base(a, b) { … }Chama o construtor da base antes do corpo
public Filha() : base() { }Chama o construtor sem parâmetros da base (é o que acontece sem nada escrito)
public Filha() : this(…) { }Chama outro construtor da própria classe
base.Metodo()Chama a versão da base de um método
base.PropriedadeUsa uma propriedade da base
class Filha(string x) : Mae(x) C# 12Construtor primário passando x para a base

Missão final: a conta poupança

Missão: uma ContaPoupanca é uma Conta que rende juros. Escreva o construtor public ContaPoupanca(string titular, decimal taxa): ele passa o titular para a Conta com : base(titular) e guarda a taxa na propriedade Taxa. Repare no protected set do Saldo: é ele que deixa o RenderJuros da filha mudar o saldo.

var poupanca = new ContaPoupanca("Ana", 0.01m);
poupanca.Depositar(1000m);
poupanca.RenderJuros();
Console.WriteLine($"{poupanca.Titular}: {poupanca.Saldo:C}");

class Conta
{
    public Conta(string titular)
    {
        Titular = titular;
    }

    public string Titular { get; }
    public decimal Saldo { get; protected set; }

    public void Depositar(decimal valor)
    {
        if (valor > 0)
        {
            Saldo += valor;
        }
    }
}

class ContaPoupanca : Conta
{
    // escreva o construtor aqui

    public decimal Taxa { get; }

    public void RenderJuros()
    {
        Saldo += Saldo * Taxa;
    }
}
var poupanca = new ContaPoupanca("Ana", 0.01m);
poupanca.Depositar(1000m);
poupanca.RenderJuros();
Console.WriteLine($"{poupanca.Titular}: {poupanca.Saldo:C}");

class Conta
{
    public Conta(string titular)
    {
        Titular = titular;
    }

    public string Titular { get; }
    public decimal Saldo { get; protected set; }

    public void Depositar(decimal valor)
    {
        if (valor > 0)
        {
            Saldo += valor;
        }
    }
}

class ContaPoupanca : Conta
{
    public ContaPoupanca(string titular, decimal taxa) : base(titular)
    {
        Taxa = taxa;
    }

    public decimal Taxa { get; }

    public void RenderJuros()
    {
        Saldo += Saldo * Taxa;
    }
}

Exercícios

1. Chamar a base

Complete o construtor para passar nome ao construtor da base: public Aluno(string nome, int serie) ___

2. A versão da mãe

Dentro de um override de Mostrar(), como se chama a versão da classe base? (com o ponto e vírgula)

3. O erro do argumento que falta

Qual é o código do erro quando a base exige um parâmetro e a filha não usa : base(…)?

4. new Cachorro("Rex"), com Cachorro(string nome) : base(nome). Qual corpo de construtor roda primeiro?

5. Qual a diferença entre : base(…) e : this(…) num construtor?

6. Em class Cachorro(string nome) : Animal(nome), o que é o Animal(nome)?


Resumo

  • O construtor da base roda antes do da filha. Se ele pede dados, a filha entrega com : base(…), entre os parênteses e a chave.
  • Sem : base(…), o C# chama o construtor sem parâmetros da base; se ele não existe, CS7036. Argumentos que nenhum construtor aceita: CS1729.
  • Cada classe fala só com a mãe direta. : this(…) chama outro construtor da mesma classe.
  • base.Metodo() chama a versão da base, útil dentro de um override. Não existe em métodos static (CS1511).
  • Atalho do C# 12: class Cachorro(string nome) : Animal(nome).