Tudo sobre Generics - .Net Magazine
Este artigo descreve os fundamentos da programação utilizando o recurso Generics. O Generics permite que tenhamos algoritmos que possam ser aplicados independentes de tipo.
Este artigo descreve os fundamentos da programação utilizando o recurso Generics. Generics permite que tenhamos algoritmos que possam ser aplicados independentes de tipo. Ou seja, criamos um código cujo parâmetro é o tipo ao qual ele é aplicado. O uso mais comum de Generics está em algoritmos para implementação de coleções.
Para que serve
Generics foi introduzida na versão 2.0 da plataforma .NET. Sem a tecnologia de Generics, LINQ e outras novidades mais recentes não seriam viáveis. Coleções mais complexas também só foram introduzidas na plataforma .NET a partir da introdução de Generics. Sem Generics tais coleções seriam inviáveis, pois sua operação seria muito ineficiente.
Em que situação o tema é útil
O conceito de Generics é aplicado mais comumente em coleções. Generics permite a criação de um modelo de código que pode ser aplicado para tipos. Por exemplo, um algoritmo que representa o conceito de fila. Sem Generics, o mesmo código teria que ser criado para cada tipo, ou baseado em algum modelo de herança. O que na prática torna ambos inviáveis ou muito inflexíveis.
Resumo do DevMan
Este artigo desvenda todos os detalhes do uso de Generics no C#, tanto em métodos, classes, interfaces e structs. Examina suas vantagens e demonstra exemplos práticos de utilização.
Quando utilizamos coleções em qualquer plataforma estamos considerando algoritmos que possam ser aplicados com vários tipos, como string, número e tipos customizados. Por exemplo, quando consideramos o conceito de fila, procuramos alguma coleção que possa oferecer este tipo de algoritmo. Qualquer biblioteca/plataforma de uso profissional tem suporte para coleções. A plataforma .NET tem um grande conjunto destas coleções. Na versão 1.x algumas se tornaram populares como ArrayList, Hashtable e assim por diante. Mas existe um tipo que é o mais popular de todos: Array.
Lendo a documentação das classes ArrayList ou Hashtable, vamos perceber que os métodos para manipular a coleção recebem ou retornam itens do tipo Object, por exemplo, o método ArrayList.Add:
public virtual int Add(
Object value
)
Quando atribuímos uma referência, por exemplo, System.String a um ponteiro que tem como tipo base um Object, como o parâmetro value do método Add acima, estamos fazendo com que mecanismo de execução do CLR percorra estruturas internas e aplique certas regras para garantir que tal atribuição seja válida. E neste caso a atribuição só funciona pois todos os objetos são derivados, direta ou indiretamente da classe Object.
A plataforma .NET tem dois tipos fundamentais: Reference type e Value type. Um Reference type é, em uma comparação simples, similar ao conceito de ponteiro em linguagens de programação não gerenciadas, como C, C++, Object Pascal e assim por diante. Ou seja, uma variável A do tipo Object tem o endereço de memória onde o objeto real está armazenado. Portanto, o valor de A é o endereço do objeto, ou seja, uma referência para ele. Um Value type, como um Int32, contém o objeto. Ou seja, uma variável A do tipo Int32 é o objeto e não uma referência para um endereço.
É claro que em ambientes gerenciados como a plataforma .NET, existem outras estruturas de controles além de um endereço de memória apenas. Por isto mesmo, o ambiente tem conceitos como Managed Environment e Managed Execution, ou seja, boa parte do que se faz está sob a supervisão constante do ambiente que o executa. Eu digo boa parte, pois é possível integrar código não gerenciado em código gerenciado e este código não gerenciado NÃO está sob a supervisão do ambiente gerenciado.
Voltando aos conceitos de Reference type e Value type. Um Value type representa o valor em si e, portanto, da perspectiva do CLR você não pode simplesmente atribuir um número para um Object, pois são conceitos completamente distintos. Outros ambientes utilizam conceitos de Object de maneira ortogonal, ou seja, tudo é um objeto e você pode atribuir livremente um objeto para outro, pois todos pertencem ao mesmo conceito base.
Na plataforma .NET não é assim. Mas existem meios, utilizando recursos seguros das linguagens de programação, de atribuir um Value type para uma Reference type. Um destes meios é a sobrecarga de operadores, onde você pode fazer com que um tipo Produto seja somado como um tipo Int32 e tenha como resultado um reajuste.
Também existe uma possibilidade comum que a própria plataforma .NET realiza sem intervenção do programador: O BOX e UNBOX.
Conceituando Box e Unbox
Quando você, na sua casa, quer guardar algo, logo procura uma caixa. Uma caixa aceita praticamente qualquer coisa. Bom, uma variável do tipo Object também. Se você coloca algo em uma caixa e fecha, e depois entrega para alguém, a primeira pergunta da inocente vítima é:
'- O que tem aí?'
Bem, é exatamente isto que acontece quando você atribui algo para uma variável do tipo Object. Somente quem guardou 'aquilo' na caixa sabe o que é. Veja um exemplo:
Box(Colocar na caixa):
Int32 value = 10;
Object caixa = value;
Unbox(Abrir e caixa e torcer):
String vitima = (String)caixa;
Pelo exemplo acima você já imagina que a surpresa é desagradável. Se tentar compilar este código, ele não vai apresentar erros na compilação. O compilador, sem opção, acredita que o escritor do código saiba o que está fazendo. Quando ele tenta remover da caixa, acredita que existe uma string e quando percebe que não é o que o código diz, só resta lançar uma InvalidCastException. É o equivalente àquele sorriso amarelo quando ganha um presente sem o menor sentido.
Coleções
Voltando às coleções ArrayList e Hashtable, elas podem conter objetos de vários tipos ao mesmo tempo, o que é muito complicado para se administrar. Existem muitas linguagens de script/dinâmicas que utilizam alguma forma de hierarquia de tipos para que coleções de tipos distintos possam ser criadas minimizando o impacto no desempenho ou na estabilidade do código. Tais linguagens foram projetadas com estes princípios, algo que as linguagens 'statically typed' não possuem. São classes de linguagens distintas e uma comparação direta entre elas é algo, no mínimo, sem bom senso ou um discurso vazio.
A classe Array é de uso tão comum que passa despercebida uma característica impressionante. Quando você define um Array, indica o tipo dos elementos dele, como no exemplo:
Int32[] numbers;
String[] words;
Então, considerando a explicação sobre Box/Unbox, você pode imaginar que ele também é ineficiente. O tipo Array é um tipo diferente. Ele é um tipo que faz parte do mundo CLR – Common Language Runtime. Existem instruções em CIL (Common Intermediate Language) para criação e manipulação de Array. Ele não é um tipo definido apenas na BCL (Base Class Library) da plataforma .NET. Nos dois exemplos a seguir você tem um trecho de código que cria dois Arrays:
Int32[] numbers = {0,1,2};
String[] words = {String.Empty, "Roger"};
Console.Write( numbers[0] );
O conceito de Array faz parte do CLR. Outra coisa que você pode notar é que, mesmo sendo uma classe, e portanto seguindo os princípios da OOP, a classe Array não tem um construtor. Ela tem um método para criação de tipo, o método CreateInstance, que é sobrecarregado (overload) e tem mais de uma assinatura:
public static Array CreateInstance(
Type elementType,
int length
)
O método CreateInstance não recebe um tipo Object e sim o tipo Type, que é uma outra classe que representa um conceito mais abstrato que o conceito de objeto. E como o tipo Array funciona então?
Na realidade o tipo Array é um tipo genérico, desde a sua concepção. Só que ao invés da classe ser um tipo Generic, o conceito apresentado neste artigo, é o CLR que implementa essa capacidade. Por qual motivo? Eficiência. Não seria eficiente, considerando o propósito, se a classe Array fosse apenas mais um tipo na BCL. Muitas vezes isso causa surpresa, mas é um fato.
Mas o que é um tipo genérico afinal?
Imagine que você tenha um algoritmo, como o conceito de Array ou fila que possa ser aplicado para praticamente qualquer tipo. Se você quiser uma versão deste algoritmo para cada tipo, vai precisar criar uma classe idêntica em termos de algoritmo, variando apenas o tipo, o que é impraticável. Se ao invés disto você criar um tipo genérico que contém o algoritmo, mas o tipo ao qual o algoritmo será aplicado é um parâmetro passado para o tipo genérico. Este é princípio que vamos tratar aqui. O conceito de Generics não é novidade no mundo de linguagens de programação e existe há muitos anos. Linguagens de programação como Eiffel e Ada são dois exemplos muito populares.
A declaração de um tipo Generic utiliza uma sintaxe muito interessante. Você vai notar que é uma extensão natural para o código que está habituado. O exemplo que vamos criar é para uma lista de itens, ou seja, uma coleção simples. A coleção deve ser um tipo genérico simples que permita a inclusão e remoção de itens e informe o número de itens nesta coleção através de uma propriedade. A declaração da classe genérica fica assim:
public class List<TItem> {
}
E a declaração da struct genérica fica assim:
public struct List<TItem> {
}
Após o nome do tipo (classe ou struct), você inclui um ou mais type parameters. Em nosso exemplo o type parameter é o TItem. Nós vamos conversar detalhadamente sobre type parameters e type arguments mais adiante. Agora vamos ver a nossa primeira versão do tipo List<TItem> completa (Listagens 1 e 2).
Listagem 1. Classe genérica
namespace OpenMind.Example01 {
/// <summary>
/// Coleção de itens.
/// </summary>
/// <typeparam name="TItem">parameter type que indica qual é o tipo
/// para os itens da coleção</typeparam>
public class List<TItem> {
#region Private fields
/// <summary>
/// Número máximo de itens da coleção.
/// </summary>
private const Int32 Size = 1000;
/// <summary>
/// Total de itens na coleção.
/// </summary>
private Int32 count;
#endregion
#region Constructors
/// <summary>
/// Constructor default.
/// </summary>
public List() {
#if DEBUG
Console.WriteLine( "Coleção para o tipo: ", typeof(TItem).ToString() );
#endif
}
#endregion
#region Public properties
/// <summary>
/// Total de itens na coleção.
/// </summary>
public Int32 Count {
get {
return this.count;
}
}
/// <summary>
/// Número máximo de itens da coleção.
/// </summary>
public Int32 Length {
get {
return List<TItem>.Size;
}
}
#endregion
}
}
Agora que criamos o nosso tipo genérico, vamos utilizá-lo. Na linguagem C# o operador new é utilizado para criar uma instância do tipo Generic. Veja alguns exemplos:
List<String> words = new List<String>();
List<Int32> numbers = new List<Int32>();
Parâmetros para o tipo genérico – Type Placeholder
Quando você programa com Generics, um dos primeiro conceitos que precisa assimilar é o type parameter. Este é o elemento base na declaração de qualquer tipo generic. O type parameter também é referido na documentação da Microsoft como type placeholder. Ele cria um local para que possamos informar qual é o type argument quando estamos criando uma instância de um tipo genérico. Sendo um dos elementos fundamentais para o uso de Generics, um type parameter tem uma série de regras associadas ao seu uso. A maioria destas regras está diretamente relacionada com o contexto em que o type parameter está sendo utilizado. Por contexto eu quero dizer o tipo genérico que você está declarando, por exemplo, uma classe genérica. No entanto, algumas regras são comuns para qualquer contexto.
Nomes para o type parameter
É uma prática comum, mas não obrigatória, prefixar o nome do type parameter com a letra T. No exemplo a seguir temos a classe generic List com o type parameter TItem:
public class List<TItem> {
}
A letra T significa Type (tipo). Ou seja, estamos criando um elemento que representa um tipo que será informado na criação da instância do tipo genérico. Nem todos os autores e/ou profissionais concordam com essa forma de prefixar o type parameter com a letra T. No entanto, é uma prática que tem mais aprovação do que reprovação.
Uma forma de declaração que você vai encontrar frequentemente é a utilização de apenas uma letra para o nome do type parameter. É claro que apenas uma letra não é muito descritiva, mas em muitos casos o próprio tipo genérico acaba facilitando a compreensão. Um exemplo comum é um tipo genérico que descreve o conceito de Key/Value (chave/valor), como um dicionário. A declaração pode ser feita assim:
Public class ReferenceTable< K, V > {
}
Ou:
public class ReferenceTable<TKey, TValue> {
}
No caso da utilização da letra T como prefixo padrão, observe que o restante do nome do parâmetro segue o padrão de nomenclatura Pascal Case. Agora vamos ver mais detalhes da sintaxe para tipos generic.
Declaração de uma classe / struct genérica
Quando declaramos uma classe não genérica nós não informamos o type parameter. Para todos os campos, propriedades e demais membros, definimos quais são os seus tipos na declaração da classe. Não informando o type parameter, o compilador gera o código MSIL/CIL para uma classe não genérica. O CLR quando lê os metadados gerados pelo compilador reconhece a classe como sendo não genérica. Na Listagem 2 temos o exemplo da declaração da classe MyList (não genérica).
Listagem 2. Classe não genérica
public class MyList {
/// <summary>
/// Campo de exemplo.
/// </summary>
private Int32 sampleField;
....(Código de implementação da classe non-Generic)
}
Um fato interessante é que mesmo uma classe sendo não genérica ela pode ter, por exemplo, métodos genéricos. Essa possibilidade é uma ferramenta poderosa em cenários como uma migração de código. Podemos ter um código para plataforma .NET versão 1.x e estamos interessados na utilização do recurso de Generics, disponível a partir da versão .NET 2.0. Mas não podemos fazer a migração de uma só vez e aplicar todas as características suportadas pelo Generics. Nós podemos identificar alguns pontos em que o conceito de Generics pode ser aplicado e podemos fazer a migração destes. Na Listagem 3 temosum exemplo para ilustrar este cenário, a classe MyList com um método genérico chamado Find.
Listagem 3. Método genérico
public class MyList {
public static Boolean Find<TItem>( TItem itemToFind ) {
Boolean isFounded = false;
// Code to generic method.
return isFounded;
}
}
Outra possibilidade é uma classe genérica conter métodos não genéricos. No exemplo da Listagem 4 temos a classe genérica MyList com um método Find, não genérico.
Listagem 4. Classe genérica, mas método não
public class MyList<TItem> {
public static Boolean Find( Object itemToFind ) {
Boolean isFounded = false;
// Code to generic method.
return isFounded;
}
}
Voltando para a classe não genérica MyList, vamos criar uma versão genérica. A declaração de uma classe genérica exige que você inclua pelo menos um type argument para que a classe seja reconhecida pelo compilador e pelo CLR como tal. A declaração da classe MyList genérica tem a seguinte forma:
Public class MyList<TItem> {
.......( Código de implementação da classe Generic )
}
Neste exemplo o type parameter se chama TItem. Ele representa o tipo que será passado quando nós criarmos a instância da classe MyList. As Listagens 5 e 6 mostram a implementação e uso da classe genérica MyList.
Listagem 5. Classe genérica
public class MyList<TItem> {
#region Private Fields
/// <summary>
/// Um campo com o tipo declarado para o
/// type parameter.
/// </summary>
private TItem item;
/// <summary>
/// Vetor do tipo TItem ( o type parameter ).
/// </summary>
private TItem[] internalList;
/// <summary>
/// Um campo System.Int32 ( não Generic ).
/// </summary>
private Int32 index;
#endregion
#region Constructors
/// <summary>
/// Default constructor.
/// </summary>
public MyList() {
this.item = default(TItem);
this.internalList = null;
}
#endregion
#region Generic Method
/// <summary>
/// Pesquisa pelo objeto itemToFind e verifica se este
/// existe na coleção.
/// </summary>
/// <param name="itemToFind">Objeto que deve ser encontrado.</param>
/// <returns></returns>
public Int32 Find(TItem itemToFind ) {
// Código de implementação do método.
return -1;
}
#endregion
Listagem 6. Exemplo de uso da classe MyList
public static void Main() {
MyList<Int32> numbers = new MyList<Int32>();
MyList<String> words = new MyList<String>();
numbers.Find(10);
words.Find("Roger Villela");
}
Na classe MyList você vai identificar o type argument TItem, sendo utilizado normalmente, como qualquer outro tipo da linguagem, como por exemplo um Int32 ou um tipo String.
Declaração de uma interface genérica
A declaração de uma interface genérica segue os mesmos padrões sintáticos demonstrados para a classe genérica e struct genérico. Considerando o exemplo anterior para representar uma lista, vamos agora fazê-lo com uma interface genérica:
public interface IMyList {
Int32 Find( Int32 itemToFind );
}
Como podemos notar, o método está considerando apenas objetos do tipo System.Int32. Depois de avaliarmos o código, verificamos que este é um tipo para o qual precisamos criar uma versão genérica. O primeiro passo é declarar a interface genérica como no exemplo:
public interface IMyList<TItem> {}
Criamos uma interface chamada IMyList que tem um type parameter chamado TItem, que representa o tipo que você informará na criação da instância que implementa essa interface genérica. Agora vamos incluir o método não genérico Find:
public interface IMyList<TItem> {
Int32 Find( TItem itemToFind );
}
O método Find recebe como parâmetro o type parameter TItem e retorna a posição em que o item foi encontrado na lista ou o valor -1 se o item não foi encontrado na lista. Observe que mesmo sendo um método não genérico, Find pode ter parâmetros genéricos e não genéricos, tanto como retorno quanto na lista de parâmetros. Vamos falar sobre métodos um pouco mais adiante. Agora vamos criar uma classe genérica e uma struct genérica que implementam a interface genérica IMyList. Veja o código das Listagens 7 e 8.
Listagem 7. Exemplo de classe genérica que implementa a interface IMyList
public class MyListClass <TItem>: IMyList<Titem> {
#region private fields
/// <summary>
/// Representa o tipo de item da coleção.
/// </summary>
private TItem item;
#endregion
#region Implementação da interface IMyList<TItem>
/// <summary>
/// Verifica se itemToFind existe na coleção.
/// </summary>
/// <param name="itemToFind"> Objeto que deve ser encontrado.</param>
/// <returns>
/// - Retorna um valor positivo >= 0 (zero ) se o item foi encontrado.
/// - Retorna um valor negativo se o item não foi encontrado.
/// </returns>
public Int32 Find( TItem itemToFind ) {
Int32 position = -1;
//Implementação do código de busca.
return position;
}
#endregion
}
Listagem 8. Exemplo de struct genérica que implementa a interface IMyList
public struct MyListStruct<TItem>: IMyList<TItem> {
#region private fields
/// <summary>
/// Representa o tipo de item da coleção.
/// </summary>
private TItem item;
#endregion
#region IMyList<TItem>
/// <summary>
/// Verifica se itemToFind existe na coleção.
/// </summary>
/// <param name="itemToFind"> Objeto que deve ser encontrado.</param>
/// <returns>
/// - Retorna um valor positivo >= 0 (zero ) se o item foi encontrado.
/// - Retorna um valor negativo se o item não foi encontrado.
/// </returns>
public Int32 Find( TItem itemToFind ) {
Int32 position = -1;
//Implementação do código de busca.
return position;
}
#endregion
}
}
Um aspecto interessante e muito importante na implementação de uma interface genérica é que ela pode ser utilizada para uma classe genérica ou struct genérica, mas também podem ser utilizadas classes ou structs não genéricas. Imagine que ao invés de criar uma classe genérica ou struct você queira implementar a interface com um tipo concreto, ou seja, informando um type argument. As declarações do exemplo anterior ficam como nas Listagens 9 e 10.
Listagem 9. Exemplo da classe genérica que implementa a interface genérica IMyList informando o type argument
public class MyListClass : IMyList<Int32> {
#region private fields
/// <summary>
/// Representa o tipo de item da coleção.
/// </summary>
private Int32 item;
#endregion
#region IMyList<Int32>
public Int32 Find(Int32 itemToFind ) {
Int32 position = -1;
//Implementação do código de busca.
return position;
}
#endregion
}
Listagem 10. Exemplo da struct genérica que implementa a interface genérica IMyList informando o type argument
public struct MyListStruct: IMyList< String > {
#region private fields
/// <summary>
/// Representa o tipo de item da coleção.
/// </summary>
private Int32 item;
#endregion
#region IMyList< String >
public Int32 Find(String itemToFind ) {
Int32 position = -1;
//Implementação do código de busca.
return position;
}
#endregion
}
Outras características da interface genérica incluem a possibilidade de herdar de uma interface não genérica, que por sua vez também pode herdar de outras interfaces genéricas.
Declaração de um método genérico
Um generic method pode ser declarado em: • Uma classe não genérica; • Uma struct não genérica; • Uma interface não genérica. E é claro que é declarado nos equivalentes generic: • Uma class genérica; • Uma struct genérica; • Uma interface genérica. Nota: Em todos os casos o método pode ser de instância ou estático. Vamos explorar algumas possibilidades com exemplos. Estes exemplos são pequenos trechos que têm como objetivo servirem de referência para sintaxe. Veja a Listagem 11.
Listagem 11. Declaração de um método genérico em uma classe não genérica
public class MyList {
#region Generic Method
/// <summary>
/// Verifica se itemToFind existe na coleção.
/// </summary>
/// <param name="list"> Array com os itens. </param>
/// <param name="itemToFind"> Objeto que deve ser encontrado.</param>
/// <returns>
/// - Retorna um valor positivo >= 0 (zero ) se o item foi encontrado.
/// Este valor é a posição onde o item foi encontrado.
/// - Retorna um valor negativo se o item não foi encontrado.
/// </returns>
public Int32 Find<TItem>( TItem[] list, TItem itemToFind ) {
Int32 position = ( list != null ? list.Length : 0 );
while ( ( --position >= 0 ) && ( !itemToFind.Equals(list[position]) ) );
return position;
}
#endregion
}
O exemplo declara uma class não genérica chamada MyList e um método genérico chamado Find. O método Find de exemplo retorna um objeto do tipo System.Int32. Mas vamos olhar mais detalhadamente a sintaxe para o método genérico. Como todo tipo genérico, é preciso definir um ou mais type parameters para que este seja reconhecido como tal. No caso do método genérico você declara os type parameters após o nome do método e antes da lista de parâmetros. No exemplo anterior temos apenas um type parameter, o TItem. Mas podemos ter mais de um type parameter, como nos exemplos:
public Int32 Find< TItem >
public Int32 Find< TItem, TKey >
A lista de type parameters deve ser separada por vírgula. Após a declaração dos type parameters, a lista de parâmetros do método pode conter o tipo do type parameter, como no exemplo:
public Int32 Find<TItem>( TItem[] list, TItem itemToFind )
Além destes, o método genérico também pode conter parâmetros de tipos não genéricos, como no exemplo:
public Int32 Find<TItem>( TItem[] list, TItem itemToFind, Int32 startPosition )
Nos exemplos da Listagem 12 apresento a sintaxe para este mesmo método genérico para uma struct e interface (não genéricas).
Listagem 12. Método genérico em struct e interface não genéricas
public struct MyList {
#region Generic Method
public Int32 Find<TItem>( TItem[] list, TItem itemToFind ) {
}
#endregion
public interface IMyList {
Int32 Find<TItem>( TItem[] list, TItem itemToFind );
}
Agora você deve estar imaginado por qual o motivo eu não listei, em seguida, o código de exemplo para a declaração do método genérico para uma classe, interface ou struct (todas genéricas). O motivo é que existe uma diferença fundamental com relação ao type parameter. Vamos ver um exemplo (Listagem 13).
Listagem 13. Exemplo de uma classe genérica com um método não genérico
#region Declaração de um método non-Generic em uma class Generic
public class MyList<TItem> {
/// <summary>
/// Verifica se itemToFind existe na coleção.
/// </summary>
/// <param name="list"> Array com os itens. </param>
/// <param name="itemToFind"> Objeto que deve ser encontrado.</param>
/// <returns>
/// - Retorna um valor positivo >= 0 (zero ) se o item foi encontrado.
/// Este valor é a posição onde o item foi encontrado.
/// - Retorna um valor negativo se o item não foi encontrado.
/// </returns>
public Int32 Find( TItem[] list, TItem itemToFind ){
Int32 position = ( list != null ? list.Length : 0 );
while ( ( --position >= 0 ) && ( !itemToFind.Equals(list[position]) ) );
return position;
}
};
#endregion
Exemplo de uso do código acima:
public class Program {
static void Main( String[] args ) {
MyList<Int32> numbers = new MyList<Int32>();
Int32[] source = { 0, 2, 3, 5, 6, 7, 8, 10 };
Int32 itemToFind = 10;
Console.WriteLine( "Position: ", numbers.Find( source, itemToFind ).ToString() );
}
}
}
Vamos rever a assinatura do método:
public Int32 Find(TItem[] list, TItem itemToFind )
Como você pode perceber, após o nome do método NÃO HÁ UMA LISTA DE TYPE PARAMETERS! A razão é bem simples. No caso de uma classe/struct/interface genérica, que precisa declarar pelo menos um type parameter, o nome do parâmetro é fundamental e o type parameter chamado TItem já está declarado na assinatura da classe genérica. Portanto o TItem têm escopo de classe. Se você tentar declarar ele novamente na assinatura do método desta forma:
public Int32 Find<TItem>( TItem[] list, TItem itemToFind )
O compilador C#, em particular, vai gerar a warning CS0693. Este alerta indica que o nome do type parameter do método oculta (hide) o type parameter da classe. Neste último exemplo, o TItem que você está usando no método, não é o TItem declarado na classe, ele tem escopo local. Portanto, a primeira declaração do método sem o type parameter após o nome do método NÃO É UM MÉTODO GENÉRICO. É um método tradicional que tem parâmetros cujo tipo é o type parameter. Agora vamos construir uma classe que usa este método genérico e a classe genérica para demonstrar a diferença. Vamos organizar em dois casos.
Primeiro, a declaração de um método não genérico em uma classe genérica tem a forma da Listagem 14.
Listagem 14. Método não genérico - classe/struct genérica
public class MyList<TItem> {
/// <summary>
/// Verifica se itemToFind existe na coleção.
/// </summary>
/// <param name="list"> Array com os itens. </param>
/// <param name="itemToFind"> Objeto que deve ser encontrado.</param>
/// <returns>
/// - Retorna um valor positivo >= 0 (zero ) se o item foi encontrado.
/// Este valor é a posição onde o item foi encontrado.
/// - Retorna um valor negativo se o item não foi encontrado.
/// </returns>
public Int32 Find( TItem[] list, TItem itemToFind ){}
};
O type parameter TItem ao qual o método se refere neste caso é o TItem declarado como type parameter para a classe/struct genérica. Ou seja, o TItem com escopo de classe. Agora vamos utilizar essa classe e o método Find (Listagem 15).
Listagem 15. Usando a classe e método
public class Program {
static void Main( String[] args ) {
MyList<Int32> numbers = new MyList<Int32>();
Int32[] source = { 0, 2, 3, 5, 6, 7, 8, 10 };
Int32 itemToFind = 10;
Console.WriteLine( "Position: ", numbers.Find( source, itemToFind ).ToString() );
}
}
}
Como você pode observar a chamada ao método é feita normalmente considerando que o método genérico está utilizando o type argument, neste caso o tipo System.Int32(int), fornecido para o type parameter TItem. Como o método está utilizando o TItem com escopo de classe, o código compila e executa sem maiores problemas, pois ambos estão utilizando o mesmo type parameter e type argument, ou seja, TItem da classe/struct genérica que é um System.Int32. Agora se você tentar algo como na Listagem 16 vai obter alguns erros de compilação, pois o método não é genérico e portanto não existe um type parameter em que um type argument possa ser informado.
Listagem 16. Erro de compilação com método não genérico
public class Test {
public static void Main() {
MyList<Int32> numbers = new MyList<Int32>();
Int32[] list = { 1,2,3,4,5 };
Int32 valueToFind = 10;
String[] letters = { "a", "b", "c", "d" };
String letterToFind = "c";
numbers.Find( list, valueToFind );
// Usando a sintaxe abaixo o compilador C# gera o erro CS0308 pois o método
// não é um Generic method.
//numbers.Find<Int32>( source, itemToFind );
// Gera um erro de compilação CS0308 pois este não é um Generic method.
numbers.Find<String>( letters, letterToFind );
}
Agora vamos examinar um segundo caso, a declaração de um método genérico em uma classe/struct genérica (Listagem 17).
Listagem 17. Método genérico, classe genérica
#region Declaração de um método Generic em uma classe Generic
public class MyList<TItem> {
/// <summary>
/// Verifica se itemToFind existe na coleção.
/// </summary>
/// <param name="list"> Array com os itens. </param>
/// <param name="itemToFind"> Objeto que deve ser encontrado.</param>
/// <returns>
/// - Retorna um valor positivo >= 0 (zero ) se o item foi encontrado.
/// Este valor é a posição onde o item foi encontrado.
/// - Retorna um valor negativo se o item não foi encontrado.
/// </returns>
public Int32 Find<TItem>( TItem[] list, TItem itemToFind ){ }
};
#endregion
Aqui temos uma situação interessante. Propositalmente, eu declarei o type parameter para a classe e o método, como mesmo nome. O type parameter TItem ao qual o método genérico se refere é o TItem declarado com escopo do método. Ele não tem relação alguma com o TItem com escopo de classe. Claramente essa é uma prática inadequada, pois gera confusão na leitura do código e também pode gerar algum tipo de erro com compiladores. Os exemplos a seguir (Listagens 18 e 19) demonstram que a chamada do método com ou sem o type argument faz com o que compilador chame o método genérico.
Listagem 18. Sem o type argument explícito, o compilador infere
MyList<Int32> numbers = new MyList<Int32>();
Int32[] source = { 0, 2, 3, 5, 6, 7, 8, 10 };
Int32 itemToFind = 10;
// O método Find<Int32> é inferido pelo compilador C# pelo tipo dos parâmetros.
Console.WriteLine( "Position: ", numbers.Find( source, itemToFind ).ToString() );
Listagem 19. Com o type argument indicado explicitamente
MyList<Int32> numbers = new MyList<Int32>();
String[] items = { "0", "2", "3", "5", "6", "7", "8", "10" };
String anotherItemToFind = "10";
// O método generic utiliza um type argument diferente do type argument da classe.
// Neste caso o método utiliza um objeto do tipo String: Find<String>.
Console.WriteLine("Position: ", numbers.Find<String>( items, anotherItemToFind ).ToString() );
A diferença fundamental aqui é que o TItem declarado no método genérico, apesar de ter o mesmo nome, não tem relação alguma com o TItem declarado no escopo da classe/struct genérica. Para que isto fique mais evidente, na Listagem 20 estão os exemplos com os dois métodos declarados, o genérico e o não-genérico.
Listagem 20.Método genérico e não genérico em uma classe genérica
#region Declaração de um método Generic e non-Generic em uma class Generic
public class MyList<TItem> {
/// <summary>
/// Verifica se itemToFind existe na coleção.
/// </summary>
/// <param name="list"> Array com os itens. </param>
/// <param name="itemToFind"> Objeto que deve ser encontrado.</param>
/// <returns>
/// - Retorna um valor positivo >= 0 (zero ) se o item foi encontrado.
/// Este valor é a posição onde o item foi encontrado.
/// - Retorna um valor negativo se o item não foi encontrado.
/// </returns>
public Int32 Find( TItem[] list, TItem itemToFind ) {
Int32 position = ( list != null ? list.Length : 0 );
while ( ( --position >= 0 ) && ( !itemToFind.Equals( list[position] ) ) ) ;
return position;
}
/// <summary>
/// Generic Method.
/// Verifica se itemToFind existe na coleção.
/// </summary>
/// <param name="list"> Array com os itens. </param>
/// <param name="itemToFind"> Objeto que deve ser encontrado.</param>
/// <returns>
/// - Retorna um valor positivo >= 0 (zero ) se o item foi encontrado.
/// Este valor é a posição onde o item foi encontrado.
/// - Retorna um valor negativo se o item não foi encontrado.
/// </returns>
public Int32 Find<TItem>( TItem[] list, TItem itemToFind ){
Int32 position = ( list != null ? list.Length : 0 );
while ( ( --position >= 0 ) && ( !itemToFind.Equals(list[position]) ) );
return position;
}
};
#endregion
Exemplo de uso do código acima:
public class Program {
static void Main( String[] args ) {
MyList<Int32> numbers = new MyList<Int32>();
Int32[] source = { 0, 2, 3, 5, 6, 7, 8, 10 };
Int32 itemToFind = 10;
// Chama o método non-generic
Console.WriteLine( "Position: ", numbers.Find( source, itemToFind ).ToString() );
String[] items = { "0", "2", "3", "5", "6", "7", "8", "10" };
String anotherItemToFind = "10";
// O método generic utiliza um type argument diferente do type argument da classe.
// Neste caso o método utiliza um objeto do tipo String: Find<String>.
Console.WriteLine( "Position: ", numbers.Find<String>( items, anotherItemToFind ).ToString() );
}
}
}
Conclusão
O uso de Generics é algo fundamental para a evolução da plataforma .NET e das tecnologias que ela suporta. Sem Generics não teríamos LINQ ou Entity Framework, apenas para citar duas tecnologias mais populares da plataforma .NET. É importante que o profissional aprenda os fundamentos para programação utilizando Generics para melhor explorar os seus recursos.
LinksIntroduction to Generics – C# Programming Guide
Benefits of Generics – C# Programming Guide
Generics Type Parameters – C# Programming Guide
Generic Classes - C# Programming Guide
Generic Interfaces – C# Programming Guide
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo