Generics - Facilitando a vida do desenvolvedor

Juliano Carvalho

Nesta página

Introdução - Coleções
Coleções tipadas - Generics
Métodos Genéricos - Generics Methods
Regras Genéricas - Generics Constraints

Introdução - Coleções

O .Net framework tem um conjunto especial de classes, chamadas coleções. As coleções são estruturas de dados que auxiliam no trabalho de agrupar diversos itens de uma forma que nos ajude a resolver algum problema. Por exemplo: criar um sistema de atendimento por fila, uma lista de participantes de um evento ou uma pilha de tarefas.

Outro problema que as coleções nos ajudam a resolver são questões de orientação a objetos. Pensar em uma solução orientada a objetos é muito mais que usar classes e objetos do framework. É necessário criar diversos objetos do mesmo tipo, agrupá-los em uma coleção e associar isto a outro objeto, criando a noção de Master/Details, comum no cenário de bancos de dados relacional.

As coleções pré-fabricadas são uma das grandes sacadas dos sistemas que embutem frameworks. Não é necessário criar uma classe de fila, uma classe de lista ou uma classe de pilha. Criar estes tipos de estruturas de dados requer trabalho adicional somado a tediosos e delicados controles internos. No .Net framework não é necessário programar nada. Basta pegar e usar. Todas as coleções do .Net framework estão agrupadas dentro do namespace System.Collections. As principais são:

  • ArrayList - Organiza os itens na forma de uma lista.

  • HashTable- Organiza os itens na forma um dicionário.

  • Queue - Organiza os itens na forma de uma fila.

  • SortedList - Organiza os itens na forma de uma lista indexada.

  • Stack - Organiza os itens na forma de uma pilha.

Coleções tipadas - Generics

Embora as coleções tradicionais já sejam extremamente úteis, elas possuem alguns problemas. O principal é o fato de só terem a capacidade de colecionar itens do tipo objetc. Mesmo que você coloque algo diferente de um objeto simples, a coleção ira armazenar um objetc.

Na primeira vez em que tive contato com uma linguagem de programação orientada a objetos moderna me lembro bem como era tediosa a tarefa de ficar tipando manualmente os itens de uma coleção. Quando colocamos um item numa coleção tradicional é feito boxing da objetc. Quando retiramos um item, unboxing, baseado no tipo que fornecemos. Estas operações produzem drawbacks e são tediosas para o desenvolvedor, pois exigem codificação manual.

Quem trabalha com coleções há de concordar comigo: a primeira coisa que você pensa ao criar uma coleção é o compilador deveria saber que você só quer um tipo de dados lá dentro. É o mais natural. Dificilmente se modela uma coleção que suporte diversos tipos de dados diferentes. Não faz muito sentido. É mais fácil criar diversas coleções, uma pra cada tipo de dado.

Era um contra-senso. Você pensava uma coisa e na hora de programar tinha que ficar informando ao compilador que tipo de dado estava lá dentro. Você colocava uma classe Pessoa, e ele coletava um objetc. Você colocava uma Nota fiscal e ele coletava um objetc. Era tedioso, além de pouco performático.

O exemplo abaixo leva o programa a lançar uma Exception porque não há nenhuma garantia durante a compilação que somente um tipo de dado possa ser empilhado. São inseridos três elementos: uma string, um int e um objetc. Como tratar isto de modo a garantir que um elemento é do tipo que você espera?

Stack PilhaDeQualquerCoisa = new Stack();
PilhaDeQualquerCoisa.Push("string");
PilhaDeQualquerCoisa.Push(100);
PilhaDeQualquerCoisa.Push(new Object());

foreach (int item in PilhaDeQualquerCoisa)
Console.WriteLine(item);

O pior vem na hora de recuperar. Sendo possível colocar qualquer coisa dentro de uma coleção, como saber o que há lá dentro? Não há! Você tem que dizer a ele que tipo de coisa você colocou.

Por exemplo: se você tem uma pilha de pratos para lavar e todos são iguais, é razoável pensar numa estratégia padrão para lavá-los rapidamente. Imagine se a cada prato a ser lavado você precise de um tempo para estudá-lo e garantir de que fato é um prato e não uma colher? Irritante e desnecessário se você tem certeza que todos são pratos.

.Net 2.0 resolveu isto com Generics. Todas as coleções genéricas do .Net framework estão agrupadas dentro do namespace System.Collections.Generics As principais são:

  • LinkendList<> - Organiza os itens na forma de uma lista duplamente ligada.

  • List<> - Organiza os itens na forma de uma lista tradicional (como um ArrayList).

  • Queue<> - Organiza os itens na forma de uma fila.

  • SortedList<> - Organiza os itens na forma de uma lista indexada.

  • SortedDictionary - Organiza os itens na forma de um dicionario.

  • Stack<> - Organiza os teins na forma de um pilha.

O uso de Generics basicamente consiste em tipar uma coleção de modo a garantir que somente um tipo de dado pertença a ela. Mesmo que você tente inserir elementos de outro tipo, o compilador não permitirá. Da mesma forma, não é necessário fazer cast na hora de retirar um elemento. Assim:

Stack<int> PilhaDeNumeros = new Stack<int>();
PilhaDeNumeros.Push(100);
int UmNumero = PilhaDeNumeros.Pop();
Console.WriteLine(UmNumero);
foreach (String item in PilhaDeStrings) Console.WriteLine(item);

A tipagem pode ser feita de dois modos:

  1. Na definição da classe, de modo a restringir um único tipo de dado para todos os objetos daquela classe.

  2. Na criação dos objetos, de modo a restringir um único tipo de dado para aquele objeto particularmente. Assim você pode ter a mesma classe genérica suportando diversos tipos de dados. Mas somente um tipo por instancia.

Assim:

class ClasseGenerica<Tipo> : List<Tipo>
{
}

No momento de instanciar:

ClasseGenerica<int> ColecaoDeNumeros = new ClasseGenerica<int>();
ClasseGenerica<String> ColecaoDeStrings = new ClasseGenerica<String>();

Métodos Genéricos - Generics Methods

Um método genérico tem a mesma funcionalidade dos tipos genéricos, ou seja, suportar tipos fortes, definidos e genéricos.

Assim como nos tipos genéricos, os métodos genéricos existem para permitir a passagem de valores genéricos para um determinado método.

Como exemplo, imagine que iremos criar um tipo SuperPilha, uma extensão da classe genérica Stack<> do .Net framework. Nossa intenção é permitir que a SuperPilha:

  1. Suporte tipos diferentes de itens empilhados (genérica).

  2. Suporte múltiplas inserções numa única chamada, criando uma forma mais elegante de manipular a pilha.

Tipar um método genérico tem o mesmo efeito de tipar uma classe genérica. No exemplo abaixo iremos criar método "AdicionarVarios". Este método é capaz de duas coisas:

  1. Suportar o mesmo tipo genérico fornecido no objeto instanciado.

  2. Suportar quantidade de parâmetros variável com o comando params.

class SuperPilha<T> : Stack<T>
{
	public void AdicionarVarios(params T[] valores)
	{
		foreach (T valor in valores)
		{
			this.Push(valor);
		}
	}
}

Desta forma podemos utilizar a SuperPilha pra qualquer tipo e, de quebra, ainda podemos usar o mesmo tipo para um parâmetro de um método qualquer da mesma classe.

Além desta forma, que considero ideal, uma vez que é possível inserir de um a n elementos na coleção, também podemos criar um método tradicional, que espera um único parâmetro genérico. Assim:

public void Adicionar(T valor)
{
	this.Push(valor);
}

O uso é normal, com destaque para os parâmetros variáveis.

SuperPilha<int> MinhaPilhaDeNumeros = new SuperPilha<int>();
MinhaPilhaDeNumeros.AdicionarVarios(10, 20, 30, 40, 50);
MinhaPilhaDeNumeros.AdicionarVarios(60, 70);
MinhaPilhaDeNumeros.Adicionar(80);

SuperPilha<String> MinhaPilhaDeStrings = new SuperPilha<string>();
MinhaPilhaDeStrings.AdicionarVarios("Juliano", " Carvalho");
MinhaPilhaDeStrings.AdicionarVarios("Raquel", " Arthur", "Calderaro");

Regras Genéricas - Generics Constraints

Esta funcionalidade é certamente um dos aspectos mais importante dos tipos genéricos.

Quando trabalhamos com coleção não tipadas (Arraylist, p.exemplo) é impossível afirmar quais tipos de dados estão dentro da coleção uma vez que este tipo de coleção suporta qualquer objeto.

Após o surgimento dos genéricos ficou mais fácil controlar o tipo de objeto que pode ser associado a uma coleção: podemos restringir o tipo da forma com que for mais adequado a nossa solução.

No exemplo abaixo, a classe ColecaoPersistente suporta qualquer tipo de dado, mas somente um para cada instancia. O tipo é fornecido no momento de instanciar um objeto da classe, da mesma forma com que já vimos. Assim:

class ColecaoPersistente<TipoColecionado> : List<TipoColecionado> 
{
}

Mas ainda existe um problema. Suponha que temos uma classe genérica modelada para ser tipada em nível de objeto (cada objeto da classe genérica pode ter um tipo diferente). Como garantir que, mesmo sendo objetos de tipos diferentes, eles tenham sempre algo em comum?

Vamos tomar por exemplo um sistema de controle de coleções (selos, chaveiros, cartões e etc.).

É razoável pensar que este sistema deve, de alguma forma, persistir os dados informados pelo usuário. Cada tipo de coleção tem uma característica própria, o que resulta em classes diferentes. Assim:

class Selo
{
}
class Chaveiro
{
}
class ColecaoSeilaDoque
{
}

Se cada classe tem suas particularidade, como garantir que todas tenham algo em comum de forma que sua coleção possa disparar operações comuns, como adicionar itens, remover itens, persistir os dados e etc.?

Suponha que criemos uma interface que todos devem implementar: IPersistivel. Vamos assumir que todas as classes que implementarem IPersistivel serão capazes de persistir seus dados.

No exemplo abaixo, as classes Selo e Chaveiro implementam IPersistivel, logo, serão capazes de salvar seus dados. A classe ColecaoSeilaDoque não implementa IPersistivel.

Imagine que dentro da coleção (ColecaoPersistente) disparamos o método que persiste cada objeto. O que acontece se um objeto não possui um determinado método? Expection!

interface IPersistivel
{
}
class Selo : IPersistivel
{
}
class Chaveiro : IPersistivel
{
}
class ColecaoSeilaDoque
{
}

No desenvolvimento do sistema chegamos à conclusão que só faz sentido lidar com classes capazes de persistir de dados: de nada vale o usuário cadastrar centenas de itens para sua coleção e perder tudo ao fim do programa!

Como garantir que nossa coleção genérica suporte apenas classes que implementam IPersistivel? A resposta esta nas regras genéricas, as Constraints. O novo comando where permite a nós impormos regras de associação do tipo "só aceito colecionar classes que implementam a interface XPTO". Assim:

class ColecaoPersistente<TipoColecionado> : List<TipoColecionado> where TipoColecionado : IPersistivel, new()
{
}

Esta definição deve ser lida como "Crie uma classe chamada ColecaoPersistente do tipo List genérica, que só aceite colecionar objetos que possuem a interface Ipersistivel e possam ser instanciados sem nenhum parâmetro".

Garantir que uma classe implementa uma interface é importante porque podemos disparar um método da classe tendo certeza de que ela possui o mesmo, sem risco de Expections.

Também podemos restringir a associação às classes alcançáveis" usando o "new()", ou seja, classes que tenham visibilidade suficiente e que possuam construtor que possa ser usado sem parâmetros.

Também podemos fornecer uma lista de Interfaces que devem ser implementadas por uma classe que pretende fazer parte de uma coleção.

No exemplo abaixo, as classes devem implementar IPersistivel e IModificavel se pretendem pertencer a uma coleção do tipo ColecaoPersistente. Assim:

class ColecaoPersistente<TipoColecionado> : List<TipoColecionado> where TipoColecionado : IPersistivel, IModificavel
{
}

O uso é normal. Ainda que você tente associar uma classe fora das especificações o compilador não permitirá. Assim:

ColecaoPersistente<Selo> ColecaoDeSelo = new ColecaoPersistente<Selo>();
ColecaoPersistente<Chaveiro> ColecaoDeChaveiro = new ColecaoPersistente<Chaveiro>();
ColecaoDeSelo.Add(new Selo());
ColecaoDeChaveiro.Add(new Chaveiro());

Existe uma forma de tipar em nível de classe usando uma expressão "where". Assim, podemos restringir a coleção a um único tipo de dados definitivamente. Particularmente acho esta forma meio esquisita: já que a classe terá somente um tipo possível, então porque não tipa-la da forma tradicional? A expressão fica assim:

class ColecaoDeSelos<T> : List<T> where T : Selo
{
}

Juliano Carvalho
Bacharel em Analise de Sistemas.
Mestrando em Ciência da Computação.
Trabalha com desenvolvimento de software há cerca 15 anos.
É integrante da equipe de analistas da Folhamatic Sistemas de Americana
Desenvolvedor 5 estrelas .NET (2 estrela 2005).

Áreas de interesse:
.NET, OO, VFP, pesquisa cientifica computacional, engenharia de software e matemática discreta.