Herança: Famílias de Objetos

Herança: Famílias de Objetos

Como evitar a repetição entre classes parecidas concentrando o que é comum numa classe base. A aula explica a sintaxe da herança, o critério da relação "é um" para decidir quando usá-la, como os construtores se encadeiam, e por que o C# permite apenas uma classe base por classe.
Linguagem C#

• • 12 min de leitura

Na fase anterior, aprendemos a construir objetos individuais robustos. Nesta, estudamos como os objetos se relacionam — e o primeiro e mais fundamental relacionamento é o de especialização: a ideia de que um tipo de objeto pode ser uma variação, ou uma versão mais específica, de outro. Um gerente é um tipo de funcionário; um cachorro é um tipo de animal; um carro é um tipo de veículo. Em cada caso, o tipo específico é um tipo do mais geral, compartilhando suas características e comportamentos, mas acrescentando os seus próprios. O mecanismo que representa essa relação em C# chama-se herança, e é o tema desta aula. Com ela, você aprenderá a criar famílias de classes relacionadas, evitando repetição e espelhando, no código, as hierarquias naturais do mundo real.

O problema: repetição entre classes parecidas

Sintamos o problema que a herança resolve. Suponha que você esteja modelando os funcionários de uma empresa. Há gerentes e há vendedores. Ambos têm nome e salário, e ambos sabem se apresentar — mas o gerente tem também uma equipe sob sua responsabilidade, e o vendedor tem uma meta de vendas. Sem herança, você escreveria duas classes com muito em comum:

class Gerente
{
    public string Nome { get; set; }
    public decimal Salario { get; set; }
    public int TamanhoEquipe { get; set; }
    public void Apresentar() => Console.WriteLine($"Sou {Nome}.");
}

class Vendedor
{
    public string Nome { get; set; }       // REPETIDO
    public decimal Salario { get; set; }    // REPETIDO
    public decimal Meta { get; set; }
    public void Apresentar() => Console.WriteLine($"Sou {Nome}.");   // REPETIDO
}

Repare na repetição: Nome, Salario e Apresentar aparecem idênticos nas duas classes. Isso viola o princípio "não se repita" que vimos na Aula #16, com as mesmas consequências ruins: se você precisar mudar algo comum (dar um aumento uniforme, mudar a forma de se apresentar), terá de alterar em dois lugares, com risco de inconsistência. E imagine dez tipos de funcionário — a repetição se multiplicaria. Precisamos de uma forma de dizer "gerente e vendedor são funcionários, e compartilham o que é comum a funcionários, cada um acrescentando o seu específico". Essa forma é a herança.

A herança: uma classe baseada em outra

A herança permite criar uma classe baseada em outra, de modo que a nova classe herde automaticamente as características e comportamentos da original. A classe original chama-se classe base (ou "mãe", ou "superclasse"); a que herda dela chama-se classe derivada (ou "filha", ou "subclasse"). Primeiro, definimos o que é comum a todos os funcionários numa classe base:

// Classe BASE: contém o que é comum a TODOS os funcionários.
class Funcionario
{
    public string Nome { get; set; }
    public decimal Salario { get; set; }

    public void Apresentar()
    {
        Console.WriteLine($"Sou {Nome} e ganho R$ {Salario}.");
    }
}

Agora, criamos as classes específicas herdando de Funcionario. A sintaxe da herança usa o sinal de dois-pontos (:) após o nome da classe derivada, seguido do nome da classe base:

// Gerente HERDA de Funcionario (o ': Funcionario').
class Gerente : Funcionario
{
    // Já tem Nome, Salario e Apresentar herdados. Acrescenta o seu:
    public int TamanhoEquipe { get; set; }
}

class Vendedor : Funcionario
{
    // Também herda tudo de Funcionario. Acrescenta o seu:
    public decimal Meta { get; set; }
}

O : Funcionario significa "esta classe é um Funcionario e herda tudo dele". Com isso, Gerente e Vendedor ganham automaticamente as properties Nome e Salario e o método Apresentar, sem repeti-los — eles vêm da classe base. Cada uma acrescenta apenas o que lhe é próprio. A repetição desapareceu. Veja o resultado em uso:

Gerente g = new Gerente();
g.Nome = "Ana";              // herdado de Funcionario
g.Salario = 8000m;           // herdado de Funcionario
g.TamanhoEquipe = 5;         // próprio de Gerente
g.Apresentar();              // método herdado: "Sou Ana e ganho R$ 8000."

Vendedor v = new Vendedor();
v.Nome = "Beto";             // herdado de Funcionario
v.Meta = 50000m;             // próprio de Vendedor
v.Apresentar();              // mesmo método herdado

Tanto o gerente quanto o vendedor usam Nome, Salario e Apresentar como se fossem seus — e, num sentido real, são: foram herdados. A classe base Funcionario define o que é comum uma única vez, e cada classe derivada o recebe de graça, acrescentando suas particularidades. Se um dia você mudar o método Apresentar na classe base, a mudança se refletirá automaticamente em todas as classes que herdam dela — o oposto da manutenção em múltiplos lugares que a repetição exigia.

A relação "é um"

O teste fundamental para saber se a herança é apropriada é a chamada relação "é um" (em inglês, is a): a herança faz sentido quando você pode dizer, com verdade, que a classe derivada é um tipo de classe base. Um gerente é um funcionário — verdadeiro, então a herança cabe. Um cachorro é um animal — verdadeiro. Um carro é um veículo — verdadeiro. Sempre que essa frase soa natural, a herança provavelmente é a modelagem certa.

Cuidado, porém, com relações que parecem herança mas não passam no teste "é um". Um motor faz parte de um carro, mas um motor não é um carro — a relação aqui é de "tem um" (o carro tem um motor), não de "é um", e a modelagem correta seria o carro conter um objeto motor como uma de suas características, não herdar dele. Confundir "tem um" com "é um" é um erro comum de projeto. A pergunta a fazer sempre é: "a classe derivada é, genuinamente, um tipo da classe base?". Se sim, herde; se a relação é de posse ou composição ("tem um", "usa um"), prefira que uma classe contenha a outra como característica.

Construtores e herança

Um detalhe prático sobre construtores na herança, que costuma gerar dúvida. Quando uma classe base tem um construtor que exige dados, a classe derivada precisa fornecer esses dados ao criar-se, repassando-os à base. Isso se faz com a palavra base, que se refere à classe base, chamada no construtor da derivada:

class Funcionario
{
    public string Nome { get; set; }
    public Funcionario(string nome)   // construtor da base exige um nome
    {
        Nome = nome;
    }
}

class Gerente : Funcionario
{
    public int TamanhoEquipe { get; set; }

    // O construtor do Gerente repassa o nome à base, com ': base(nome)'
    public Gerente(string nome, int tamanhoEquipe) : base(nome)
    {
        TamanhoEquipe = tamanhoEquipe;
    }
}

Gerente g = new Gerente("Ana", 5);
Console.WriteLine(g.Nome);   // Ana — definido pela base, via base(nome)

O : base(nome) no construtor do Gerente chama o construtor da classe base Funcionario, entregando-lhe o nome. Assim, a parte "funcionário" do gerente é inicializada pela base, e a parte "gerente" pelo próprio construtor derivado. É uma divisão natural de responsabilidades: cada nível da hierarquia inicializa o que lhe pertence. (Se a classe base tiver um construtor sem parâmetros, ou nenhum construtor explícito, esse repasse pode ser desnecessário; o base(...) é exigido quando a base precisa de dados para se inicializar.)

Herança única: uma classe, uma base

Uma regra importante do C#: uma classe pode herdar de apenas uma classe base. Não existe herdar de duas classes ao mesmo tempo. Um Gerente pode ser um Funcionario, mas não pode simultaneamente herdar de Funcionario e de outra classe qualquer. Essa restrição — chamada herança única — existe para evitar ambiguidades complicadas que a herança múltipla causaria (se duas bases tivessem um método de mesmo nome, qual valeria?). Isso pode parecer limitante, mas na prática raramente é um problema, e o C# oferece um mecanismo alternativo e mais seguro — as interfaces — para os casos em que um objeto precisa cumprir vários "papéis" ao mesmo tempo. Veremos as interfaces em duas aulas; por ora, guarde que a herança de classes é sempre única: cada classe tem, no máximo, uma mãe.

A repetição entre classes parecidas ganhou solução. A herança permite que uma classe aproveite tudo o que outra já define e acrescente apenas o que lhe é particular, concentrando o que é comum num único lugar — o mesmo princípio dos métodos, agora aplicado a tipos inteiros.

O critério para usá-la é a relação "é um": herda-se quando a classe derivada é, de fato, um caso particular da base. Quando a relação correta é "tem um", a composição serve melhor. Vale reter também que o C# admite uma única classe base por classe, uma restrição deliberada que evita ambiguidades e que tem sua contrapartida em outro recurso da linguagem.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Crie uma classe base Animal com uma property Nome e um método Comer() que exiba uma mensagem genérica. Depois, crie duas classes derivadas, Cachorro e Gato, que herdem de Animal e cada uma acrescente uma property própria (por exemplo, Raca no cachorro). Crie objetos de cada uma e demonstre que ambos usam Nome e Comer() herdados.

Ver resposta

✓ Resposta: Exemplo:

class Animal
{
    public string Nome { get; set; }
    public void Comer() => Console.WriteLine($"{Nome} está comendo.");
}
class Cachorro : Animal
{
    public string Raca { get; set; }
}
class Gato : Animal
{
    public bool CacaRatos { get; set; }
}
Cachorro c = new Cachorro { Nome = "Rex", Raca = "Vira-lata" };
Gato g = new Gato { Nome = "Mimi" };
c.Comer();   // Rex está comendo. (método herdado)
g.Comer();   // Mimi está comendo. (mesmo método herdado)

Tanto Cachorro quanto Gato usam Nome e Comer() sem redefini-los, pois os herdaram de Animal; cada um apenas acrescenta o que lhe é próprio.

Exercício 2

Explique, com suas palavras, o problema da repetição entre as classes Gerente e Vendedor mostrado no início da aula, e como a herança o resolve. O que acontece se você precisar mudar a forma de Apresentar depois de aplicar a herança?

Ver resposta

✓ Resposta: O problema era que Gerente e Vendedor repetiam, idênticos, a property Nome, a property Salario e o método Apresentar — código duplicado que teria de ser mantido em sincronia nos dois lugares. A herança resolve isso colocando o que é comum numa classe base Funcionario, da qual as duas herdam, escrevendo o comum uma única vez. Se depois for preciso mudar a forma de Apresentar, basta alterá-la na classe base Funcionario, e a mudança se reflete automaticamente em Gerente, Vendedor e qualquer outra classe que herde de Funcionario — sem caçar e alterar cópias, e sem risco de inconsistência.

Exercício 3

Para cada par a seguir, diga se a relação é de herança ("é um") ou de composição ("tem um"), justificando: (a) Cachorro e Animal; (b) Carro e Motor; (c) Gerente e Funcionario; (d) Biblioteca e Livro. Para os casos de composição, explique por que a herança seria inadequada.

Ver resposta

✓ Resposta: (a) Cachorro e Animal: herança ("é um") — um cachorro é um tipo de animal. (b) Carro e Motor: composição ("tem um") — um carro tem um motor, mas não é um motor; a herança seria inadequada porque um motor não é uma variação de carro, e dizer "carro é um motor" é falso. (c) Gerente e Funcionario: herança ("é um") — um gerente é um tipo de funcionário. (d) Biblioteca e Livro: composição ("tem um" / "contém") — uma biblioteca contém livros, mas não é um livro; herdar seria absurdo, pois uma biblioteca não é uma variação de livro. Nos casos de composição, a modelagem correta é uma classe conter a outra como característica (o carro tem um campo/property do tipo Motor; a biblioteca tem uma List<Livro>).

Exercício 4

Crie uma classe base Veiculo com um construtor que receba e valide a Marca, e uma classe derivada Carro que herde dela e tenha uma property adicional NumeroDePortas. Use : base(...) no construtor de Carro para repassar a marca à base. Crie um carro e confirme que ambas as properties funcionam.

Ver resposta

✓ Resposta: Exemplo:

class Veiculo
{
    public string Marca { get; set; }
    public Veiculo(string marca)
    {
        Marca = string.IsNullOrWhiteSpace(marca) ? "Desconhecida" : marca;
    }
}
class Carro : Veiculo
{
    public int NumeroDePortas { get; set; }
    public Carro(string marca, int portas) : base(marca)
    {
        NumeroDePortas = portas;
    }
}
Carro c = new Carro("Fiat", 4);
Console.WriteLine($"{c.Marca}, {c.NumeroDePortas} portas.");   // Fiat, 4 portas.

O : base(marca) repassa a marca ao construtor de Veiculo, que a valida; o construtor de Carro cuida da parte própria (NumeroDePortas). Ambas as properties funcionam, uma vinda da base e outra da derivada.

Exercício 5

Sem código: explique o que significa a "herança única" em C# — uma classe herdar de apenas uma classe base. Por que essa restrição existe, e que problema ela evita?

Ver resposta

✓ Resposta: Herança única significa que, em C#, cada classe pode herdar de no máximo uma classe base — não é possível uma classe ter duas mães. A restrição existe para evitar as ambiguidades que a herança de múltiplas classes causaria: se uma classe herdasse de duas bases que tivessem, por exemplo, um método ou property de mesmo nome, não haveria como decidir, sem regras complicadas, qual das duas versões a classe derivada usaria. Ao limitar a herança a uma única base, o C# elimina essa ambiguidade na origem, mantendo a hierarquia de classes clara e sem conflitos. Para os casos em que um objeto precisa cumprir vários papéis, a linguagem oferece as interfaces, que resolvem isso sem os problemas da herança múltipla.

Comentários

Mais em Linguagem C#

Properties: Características com Controle
Properties: Características com Controle

Por que expor campos públicos torna a classe frágil e como as properties…

Depuração: Encontrando e Corrigindo Erros
Depuração: Encontrando e Corrigindo Erros

A aula que ensina a lidar com o erro como parte do trabalho: os três tipos de…

Recursos Modernos: Pattern Matching, Tuplas e Records
Recursos Modernos: Pattern Matching, Tuplas e Records

Três recursos do C# moderno que encurtam o código do dia a dia: o pattern…