Herança e métodos virtuais (Polimorfismo)

Supondo que quiséssemos promover novas implementações para métodos de uma classe base, na classe derivada, ou variações destes. Outra questão interessante oriunda do processo de herança é a compatibilidade de tipos de classe base de derivada. Algumas regras devem ser consideradas.

O tipo da classe base é compatível com o da classe derivada, mas somente podemos atribuir objetos do tipo derivada a um objeto do tipo base. O código abaixo exemplifica esse conceito:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
class Base
{
      ...
}
 
class Derivada: Base
{
      ...  
}
 
class MinhaClasse
{
      Static void Main()
      {    
            Base B = new Base();
            Derivada D = new Derivada()
            B = D; // Atribuição válida
            D = B; // Atribuição inválida
      }
}
...

Tendo isso claro, vejamos como se comportam métodos na atribuição de classe de tipos compatíveis. Um caso a ser resolvido é muito típico: é o de métodos com mesmo nome. Veja um exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
 
public class Base
{
      public void EscreveNome()
      {
            Console.WriteLine("Base");
      }
}
 
public class Derivada1: Base
{
      public void EscreveNome()
      {
            Console.WriteLine("Derivada1");
      }
}
 
public class Derivada2: Base
{
      public void EscreveNome()
      {
            Console.WriteLine("Derivada2");
      }
}
public class MinhaClassePolimorfismo
{
      static void Main()
      {
            Derivada1 minhaDerivada1 = new Derivada1();
            Derivada2 minhaDerivada2 = new Derivada2();
            Base minhaBase;
            string valor;
            Console.WriteLine("Digite um valor");
            valor = Console.ReadLine();
 
            if (valor == "1")
                  minhaBase = minhaDerivada1;
            else
                  minhaBase = minhaDerivada2;
            minhaDerivada1.EscreveNome();
            minhaDerivada2.EscreveNome();
            minhaBase.EscreveNome();
      }
}

Compile e veja a saída no console:

O código compila, mas gera alguns avisos. Isso ocorre porque implementamos de maneira forçada método de mesmo nome da classe base na classe derivada. O jeito fácil de resolver o problema é preceder a declaração do método com o operados “new”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
 
public class Base
{
      public void EscreveNome()
      {
            Console.WriteLine("Base");
      }
}
 
public class Derivada1: Base
{
      new public void EscreveNome()
      {
            Console.WriteLine("Derivada1");
      }
}
 
public class Derivada2: Base
{
      new public void EscreveNome()
      {
            Console.WriteLine("Derivada2");
      }
}
public class MinhaClassePolimorfismo
{
      static void Main()
      {
            Derivada1 minhaDerivada1 = new Derivada1();
            Derivada2 minhaDerivada2 = new Derivada2();
            Base minhaBase;
            string valor;
            Console.WriteLine("Digite um valor");
            valor = Console.ReadLine();
 
            if (valor == "1")
                  minhaBase = minhaDerivada1;
            else
                  minhaBase = minhaDerivada2;
            minhaDerivada1.EscreveNome();
            minhaDerivada2.EscreveNome();
            minhaBase.EscreveNome();
      }
}

Ao compilar os avisos não são emitidos:

Mas ainda temos um problema. Repare que tanto no primeiro resultado como neste último, apesar do valor informado ser 1, a última mensagem impressa se refere à classe base, apesar de termos atribuído à variável “minhaBase” a variável “minhaDerivada1”. Por que então não foi impressa a mensagem “Derivada1” ao invés de “Base”?

Tudo bem, classe base é base e ponto final, certo? Quase. A característica de polimorfismo nos permite “muitas formas” como já foi dito anteriormente. Através de um tipo ancestral, podemos obter diferentes comportamentos condicionalmente. Como fazer para que isso aconteça?

Para redefinir um método na classe derivada de forma que ele seja entendido por um objeto de classe base, estes métodos devem ser “virtuais”. A palavra chave “virtual” deve ser utilizada na declaração do método na classe base e a palavra chave “override” na declaração do método de mesmo nome da classe derivada. Veja a implementação de código a seguir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
 
public class Base
{
      public virtual void EscreveNome()
      {
            Console.WriteLine("Base");
      }
}
 
public class Derivada1: Base
{
      public override void EscreveNome()
      {
            Console.WriteLine("Derivada1");
      }
}
 
public class Derivada2: Base
{
      <strong>public override void EscreveNome()</strong>
      {
            Console.WriteLine("Derivada2");
      }
}
public class MinhaClassePolimorfismo
{
      static void Main()
      {
            Derivada1 minhaDerivada1 = new Derivada1();
            Derivada2 minhaDerivada2 = new Derivada2();
            Base minhaBase;
            string valor;
            Console.WriteLine("Digite um valor");
            valor = Console.ReadLine();
 
            if (valor == "1")
                  minhaBase = minhaDerivada1;
            else
                  minhaBase = minhaDerivada2;
            minhaDerivada1.EscreveNome();
            minhaDerivada2.EscreveNome();
            minhaBase.EscreveNome();
      }
}

Veja a saída após compilado o programa:

Note que tornamos a saída condicional. Dependendo do valor digitado, a atribuição muda e o objeto de classe base entende a implementação do objeto de classe derivada. Sim, pois uma classe derivada também é uma classe base. Se fizermos uma analogia aos conceitos apresentados no início deste capítulo, poderemos chamar a classe base de “Animal” e as classes derivadas de “Cachorro” e “Gato”, por exemplo. Adaptando os conceitos aos exemplos, podemos dizer que o cachorro e gato são animais. Veja a o código implementado sob esta ótica:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;
 
public class Animal
{
      public virtual void EscreveNome()
      {
            Console.WriteLine("Sou um Animal");
      }
}
 
public class Cachorro: Animal
{
      public override void EscreveNome()
      {
            Console.WriteLine("Sou um Cachorro");
      }
}
 
public class Gato: Animal
{
      public override void EscreveNome()
      {
            Console.WriteLine("Sou um Gato");
      }
}
 
public class minhaClassePolimorfismo
{
      static void Main()
      {
            Cachorro meuCachorro = new Cachorro();
            Gato meuGato = new Gato();
            Animal meuAnimal;
            string valor;
            Console.WriteLine("Digite um valor");
            valor = Console.ReadLine();
 
            if (valor == "1")
                  meuAnimal = meuCachorro;
            else
                  meuAnimal = meuGato;
            meuCachorro.EscreveNome();
            meuGato.EscreveNome();
            meuAnimal.EscreveNome();
      }
}

Veja a saída no console:

Este exemplo deixa mais claro o conceito de implementação, uso e entendimento de polimorfismo.

Veremos agora um uso mais prático de métodos virtuais. Uma das implementações sobrecarregadas no método Console.WriteLine recebe um object como parâmetro. Dessa forma, podemos passar qualquer objeto já que qualquer tipo herda de object e é automaticamente compatível com este. Quando se passa um object para este método, ele automaticamente faz uma chamada ao método “ToString” da classe object. O método “ToString” é declarado como “virtual” e portanto pode ser sobrescrito usando a palavra chave “override”. Veja um exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
 
class Polegadas
{
      decimal valorMetros;
 
      public Polegadas(decimal v)
      {
            valorMetros = v * 100/2.54M;
      }
 
      public void ConverteMetros(decimal v)
      {
            valorMetros = v * 100/2.54M;      
      }
 
      public override string ToString()
      {
            return "O valor em metros é: " + valorMetros.ToString();
      }
}
 
class MinhaClasse
{
      static void Main()
      {
            Polegadas p = new Polegadas(100M);
            Console.WriteLine(p.ToString());
      }
}

Veja a saída no console:

Se mesmo com uma nova implementação (“override”) quisermos ter acesso ao método da classe base, é possível faze-lo através da palavra reservada “base”, porém, seu uso é diferenciado dos construtores. Veja um exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
 
public class Animal
{
      public virtual void EscreveNome()
      {
            Console.WriteLine("Sou um Animal");
      }
}
 
public class Cachorro: Animal
{
      public override void EscreveNome()
      {
            Console.WriteLine("Sou um Cachorro");
      }
}
 
public class Gato: Animal
{
      // redefine o método EscreveNome
      public override void EscreveNome()
      {
            Console.WriteLine("Sou um Gato");
            //chama o método EscreveNome da classe base
            Base.EscreveNome();
      }
}

Polimorfismo

Este é um dos conceitos mais complexos da POO.  Ele é conseqüência do processo de herança que teve que resolver vários problemas dado seu nível de abrangência.

Polimorfismo significa “várias formas” ou “multi-forma”. Pode-se entender este processo como a habilidade de uma classe adquirir várias formas em vários objetos, ou, de se comportar de maneira diferente dependendo de em que objeto ela atua.

A princípio é um pouco difícil de entender, mas vamos tentar interpretar de maneira simples. Imagine a classe veículo comentada no início deste capítulo. Todo veículo deve ter o método “movimentar” ou andar só pelo fato de ser um veículo. Também deve implementar um método para possibilitar a sua parada, que neste caso, poderia ser chamado de “parar”.

Até aqui estes métodos aplicados à classe veículo estão claros e concisos à aplicação da classe. Porém, quando quisermos construir uma classe carro que herdará a classe veículo, esses métodos se aplicarão, mas “movimentar” não faz muito sentido para um objeto carro. O melhor neste caso seria acelerar, que junto a outros métodos provocariam o “movimento” do carro. A mesma idéia poderia ser aplicada ao método parar, que para o carro, seria melhor “brecar” ou “frear”.

Mas não é só de nomes que estamos falando. Mesmo que na classe carro este método se chamasse “movimentar”, ele teria uma conotação bem diferente do que o “movimentar” da classe veículo, que deve ser genérico para se aplicar a todas as subclasses que a herdarão.

Ainda pode parecer confuso, mas tente agora imaginar uma outra classe que poderia herdar a classe veículo, como por exemplo, a classe bicicleta. Uma bicicleta é um veículo, e como tal, deve possuir as características deste, bem como seus métodos como o “movimentar” e o “parar”. Assim como para a classe carro, o método movimentar na classe bicicleta deverá sofrer algumas adaptações para que funcione como se fosse um método “pedalar”, e este sim provocaria movimento no veículo bicicleta. Ainda assim, o problema não são os nomes, mas sim suas implementações. Vamos confrontar as classes agora:

Polimorfismo

Os membros “Movimentar” e “Parar” seriam perfeitamente herdados, mas para seu correto funcionamento, algumas adaptações deverão ser feitas. O resultado seria algo como:

Polimorfismo

Ambas as classes possuem suas definições corretas para os membros da classe base. Esta então deve prever uma implementação genérica para que as classes derivadas sejam obrigadas a definir, adaptar ou utilizar esses métodos como se fosse um preço que se paga por querer tirar proveito de suas características. Sabemos que os membros “Pedalar” e “Acelerar” são incompatíveis entre si, tanto pelo motivo de pertencer a classes distintas, como na sua estrutura, mas são compatíveis com o membro “Movimentar” da classe base, que por sua vez serviu como base para sua implementação.

Podemos generalizar que, mesmo sendo classes distintas, bicicleta e carro podem ser considerados e “referenciados” como sendo objeto do tipo “Veículo”.

Mais adiante isso ficará claro com a utilização de código.