4.5 Polimorfismo
A palavra polimorfismo vem do grego, πολύς (poli) que significa muitas e μορφή (morphos) que significa formas, ou seja, muitas formas. Em programação, é dito que um objeto é polimórfico quando o sistema de tipo da linguagem atribui mais de um tipo a este .
Linguagens tipadas convencionais, como Pascal, baseiam-se na ideia de que funções e procedimentos e, consequentemente, seus parâmetros, têm um tipo único. Tais linguagens são ditas monomórficas, no sentido de que cada variável pode ser interpretada como sendo de um tipo. Linguagens de programação monomórficas contrastam com as linguagens polimórficas em que algumas variáveis podem ter mais de um tipo .
Contudo, mesmo nas linguagens de programação mais convencionais há um certo grau de polimorfismo. Em muitas linguagens, por exemplo, o operador de soma (+), aceita tanto soma entre números inteiros (int x int -> int), quanto soma entre números reais (float x float -> float), e em algumas linguagens ainda, age como concatenador de strings (string x string -> string). Sendo assim, podemos dizer que operador de soma é polimórfico, pois possui mais de um tipo, e assume um deles de acordo com o contexto. Outro exemplo, seria a função length(), que retorna o número de elementos em um vetor de tipo T (T[] -> int), sendo T qualquer tipo.
Existem quatro tipos de polimorfismo que uma linguagem pode ter (note que nem toda linguagem orientada a objetos implementa todos os tipos de polimorfismo).
Figura 4 – Tipos de polimorfismo no paradigma orientado a objetos.
Fonte: Elaborada pelo autor.
4.5.1 Sobrecarga
Sobrecarga ocorre quando definimos numa mesma classe métodos com o mesmo nome, mas com assinaturas diferentes, ou seja, que recebem e/ou retornam parâmetros de tipos diferentes . Esse tipo de polimorfismo ocorre em tempo de compilação, pois conseguimos saber qual definição do método será invocada para um dado grupo de parâmetros antes mesmo de o código ser executado.
Exemplo 23:
public class Pessoa
{
public void Almocar()
{
Console.Write("Estou almoçando");
}
public void Almocar(string comida)
{
Console.Write("Estou almoçando " + comida);
}
public void Almocar(Pessoa pessoa)
{
Console.Write("Estou almoçando na companhia de ");
Console.Write(pessoa.nome);
}
}
4.5.2 Coerção
Coerção permite ao programador converter de maneira implícita alguns tipos para outros tipos, omitindo assim algumas conversões de tipo semanticamente necessárias .
Em C# por exemplo, é possível converter implicitamente um tipo int para double, como ilustrado no Exemplo 24.
Exemplo 24:
int a = 2;
double b = a;
Esse tipo de polimorfismo nos permite, por exemplo, definir uma função de soma, para o tipo double, e também usá-la para somar inteiros, bem como somar variáveis dos dois tipos. O Exemplo 25 ilustra a declaração da função soma com parâmetros do tipo double, seguido de seus possíveis usos no Exemplo 26.
Exemplo 25:
double soma(double x, double y)
{
return x + y;
}
Exemplo 26:
int a = 2;
double b = 3.0;
soma(a, a);
soma(a, b);
soma(b, a);
soma(b, b);
Note que para podermos usar a função soma como no exemplo anterior no polimorfismo de sobrecarga, seria necessário definir as quatro versões possíveis da função soma, como ilustra o Exemplo 27:
Exemplo 27:
int soma(int x, int y)
{
return x + y;
}
double soma(int x, double y)
{
return x + y;
}
double soma(double x, int y)
{
return x + y;
}
double soma(double x, double y)
{
return x + y;
}
4.5.3 Paramétrico
Polimorfismo paramétrico permite que uma função ou um tipo de dado seja escrito de forma genérica, de modo que ele possa lidar com valores uniformemente, sem dependendo do seu tipo .
O polimorfismo paramétrico é extremamente útil quando precisamos criar estruturas de dados genéricas, como listas, árvores, filas, pilhas, etc. Utilizando o polimorfismo paramétrico, podemos definir a estrutura de forma genérica, e especificar o tipo no momento da criação do objeto ou do uso da função.
O Exemplo 28 ilustra a criação da estrutura de dados de pilha genérica. O Exemplo 29 demonstra seu uso na criação de uma pilha para um tipo de objeto especifico.
Exemplo 28:
public class Pilha<T>
{
private T[] elementos;
private int ultimaPosicao;
private int topo;
public Pilha(int tamanho)
{
if (tamanho > 0)
{
elementos = new T[tamanho];
ultimaPosicao = tamanho - 1;
topo = -1;
}
}
public void Push(T valor)
{
if (!estaCheia())
{
topo++;
elementos[topo] = valor;
}
else
{
throw new Exception("Stack Overflow");
}
}
public T Pop()
{
if (!estaVazia())
{
T valor = elementos[topo];
topo--;
return valor;
}
else
{
throw new Exception("Stack Underflow");
}
}
public bool estaVazia()
{
return topo == -1;
}
public bool estaCheia()
{
return topo == ultimaPosicao;
}
}
Exemplo 29:
Pilha<int> pilhaInteiros = new Pilha<int>();
pilhaInteiros.Push(1);
pilhaInteiros.Push(2);
Pilha<string> pilhaStrings = new Pilha<string>();
pilhaStrings.Push("Oi");
pilhaStrings.Push("Tchau");
Já na definição de uma função genérica, podemos, por exemplo, definir uma função que imprimirá os elementos da Pilha genérica por ordem de inserção na pilha. Assim, podemos imprimir os elementos de uma Pilha de qualquer tipo.
Exemplo 30:
void ImprimePilha<T>(Pilha<T> p)
{
if (!p.estaVazia())
{
T elemento = p.Pop();
ImprimePilha(p);
Console.WriteLine(elemento);
}
}
4.5.4 Inclusão
Objetos de um subtipo (classe derivada) podem ser manipulados como sendo objetos de algum de seus supertipos (classe base) . No Exemplo 22 onde foi visto herança múltipla, a classe Estagiario herdava da classe Estudante e da classe Trabalhador, sendo que Estudante, por sua vez, herdava de Pessoa. Sendo assim, um objeto da classe Estagiario é ao mesmo tempo, dos tipos Estagiario, Estudante, Trabalhador e Pessoa, podendo ser tratado como de um de seus supertipos quando necessário.
No Exemplo 31, criamos um objeto do tipo Estudante, que será tratado como um objeto do tipo Pessoa, uma vez que um estudante também é uma pessoa.
Exemplo 31:
Pessoa estudante = new Estudante();
Esse tipo de polimorfismo é muito útil quando queremos criar uma função que recebe como parâmetro um objeto de uma classe base (supertipo) ou de qualquer uma de suas classes derivadas (subtipos), sendo, portanto, um método que aceita vários tipos de uma mesma hierarquia.
Para ilustrar esse exemplo, podemos criar um método que receba um objeto do tipo Pessoa e execute um de seus métodos. Esse método também receberá objetos que sejam subtipos de Pessoa (e.g., Estudante, Trabalhador, Estagiario, etc.), e que portanto também possuem os mesmos métodos definidos em Pessoa, ainda que com implementações diferentes para cada classe derivada.
Exemplo 32:
class Pessoa
{
virtual public string falar()
{
return "Sou uma pessoa!";
}
}
class Estudante : Pessoa
{
override public string falar()
{
return "Sou um estudante!";
}
}
class Trabalhador : Pessoa
{
override public string falar()
{
return "Sou um trabalhador!";
}
}
class Program
{
static void ApresentarSe(Pessoa p)
{
Console.WriteLine(p.falar());
}
static void Main()
{
ApresentarSe(new Pessoa());
ApresentarSe(new Estudante());
ApresentarSe(new Trabalhador());
}
}