C# - 3 motivos importantes para usar Generics


 Neste artigo eu vou rever os conceitos fundamentais sobre Generics usando a linguagem C#.

O conceito de Generics foi introduzido a partir da versão 2.0 da .NET Framework. Nesta versão foi introduzido o namespace System.Collections.Generic, que contém as classes que suportam este conceito, como as classes List, Queue, Stack , LinkedList e outros recursos que podem ser usados efetivamente em seus programas.

Nas versões anteriores a 2.0, a generalização era realizada pela conversão de tipos de, e, para a base do tipo universal System.Object; que fornece uma solução para essa limitação em tempo de execução.

Essa limitação pode ser demonstrada com a ajuda da classe de coleção ArrayList do NET Framework. ArrayList é uma classe de coleção altamente conveniente que pode ser usada sem modificação para armazenar qualquer tipo de referência ou valor.

Mas esta conveniência tem um custo. Qualquer tipo de referência ou valor que é adicionado a um ArrayList é implicitamente convertido via typecast para System.Object. Se os itens são tipos de valor, eles devem sofrer um boxing quando adicionado à lista, e unboxing quando eles são recuperados.

O conversão forçada (coersão) , boxing e unboxing são operações que degradam o desempenho, o efeito de boxing e unboxing pode ser bastante significativo em cenários onde você deve percorrer grandes coleções.

Nota:  A plataforma .NET define duas principais categorias de tipos de dados chamadas de tipos por valor e tipos por referência para representar uma variável.

Aqui é que as operações de boxing e unboxing atuam e são usadas.

A operação Boxing é um mecanismo para converter explicitamente um tipo por valor para um tipo por referência armazenando a variável em System.Object.

Quando você faz o boxing de um valor, a CLR aloca um novo objeto na heap, e copia os valores dos tipos por valor nesta instância.

Exemplo de boxing:

int a = 20;
object b = a;
 
  // ===> boxing

A operação inversa é fazer o Unboxing que é o processo de converter de volta o tipo por referência no tipo por valor. Este processo verifica se o tipo de dados recebido é equivalente ao tipo que sofreu o boxing:

int c = (int)b;  // ===>  unboxing

O compilador C# vê a atribuição de int para object e vice-versa.

Usando Classes Genéricas

Uma classe genérica pode ser definida pela utilização do sinal <T> depois do nome da classe: Ex:  public class MinhaClasse<T>{}

Colocar a palavra 'T' na definição de um tipo Generic não é obrigatório, você também pode usar qualquer palavra na declaração da classe: MinhaClasse<>.

O namespace System.Collections.Generic contém interfaces e classes que definem coleções genéricas, que permitem aos usuários criarem coleções fortemente tipadas que fornecem melhor segurança de tipos e desempenho do que coleções não genéricas fortemente tipadas.

A seguir algumas classes mais usadas deste namespace :

Comparer<T> Fornece uma classe base para implementações da interface genérica IComparer<T> que compara dois objetos genéricos.
Dictionary<TKey, TValue> Representa uma coleção de chaves e valores.
List<T> Representa uma lista de objetos fortemente tipados que podem ser acessados pelo índice. Fornece métodos para pesquisar, classificar e manipular listas.
Queue<T> Representa uma coleção first-in, first-out (primeiro entrar, primeiro a sair) dos objetos
Stack<T> Representa uma coleção do tamanho variável : último a entrar, primeiro a sair  das instâncias do mesmo tipo especificado.
SortedList<Tkey,TValue> Representa uma coleção de pares chave/valor que são classificadas por chave com base na implementação de IComparer(Of T) associado.

A seguir vamos criar um exemplo que mostra a manipulação de um tipo genérico bem simples.

Vamos criar um projeto no Visual Studio 2015 Community do tipo Console e definir uma classe genérica chamada :  ClasseGenerica<M>   (note que estou usando M e não T)

A classe genérica deverá ser implementada de acordo com o seguintes requisitos:

Assim abaixo vemos o código desta classe:

       public class ClasseGenerica<M>
       {
            // define um Array do tipo Generic com tamanho 5
            M[] obj = new M[5];
            int contador = 0;
            // adiciona itens ao tipo genérico
            public void Adicionar(M item)
            {
                //verifica o tamanho
                if (contador + 1 < 6)
                {
                    obj[contador] = item;
                }
                contador++;
            }
            //indexador para a iteração da instrução foreach
            public M this[int index]
            {
                get { return obj[index]; }
                set { obj[index] = value; }
            }
        }

No método Main() usamos a classe acima conforme o seguinte código:

          static void Main(string[] args)
        {
            //instacia a classe Generic com Integer
            ClasseGenerica<int> intObj = new ClasseGenerica<int>();
            //adicoina valores inteiros na coleção
            intObj.Adicionar(10);
            intObj.Adicionar(20);
            intObj.Adicionar(30);     //Sem boxing
            intObj.Adicionar(40);
            intObj.Adicionar(50);
            Console.WriteLine("Classe ClasseGenerica<M> de objetos int");
            //Exibe os valores
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine(intObj[i]);   //Sem unboxing
            }
            Console.ReadKey();
        }
 
 

Este código cria uma instância da nossa classe genérica e usa o método Adicionar para incluir valores na coleção e depois usando um laço for/next lê e exibe os valores no Console.

Ao lado vemos a figura exibindo o resultado da execução do programa.

Existem algumas características importantes dos tipos Generics que se destacam dos tipos não Generics convencionais e que vamos analisar:

  1. Segurança de Tipagem;

  2. Desempenho;

  3. Reutilização de código;

Esses seriam 3 motivos importantes para usar Generics. Vamos ver o porquê.

1 - A segurança de Tipagem

Uma das características mais importantes do recurso Generics é a Tipagem Segura.

No nosso exemplo, poderíamos usar a classe ArrayList não genérica, e, neste caso poderíamos incluir na coleção objetos de qualquer tipo: int, strings, etc.

Assim poderíamos ter o seguinte código :

using System;
using System.Collections;
namespace CSharp_NaoGenerics
{
    class Program
    {
        static void Main(string[] args)
        {
            //classe Array não genérica
            ArrayList obj = new ArrayList();
            obj.Add(50);
            obj.Add("Macoratti");
            obj.Add(new Aluno());
            foreach (int i in obj)
            {
                Console.WriteLine(i);
            }
            Console.ReadLine();
        }
    }
}
 public class Aluno
 {
       public int id { get; set; }
       public string nome { get; set; }
 }

 

No código estamos incluindo valores do tipo int, string e object representando pela classe Aluno na coleção.

Mas o que esta acontecendo aqui ?

Quando executamos o código, qualquer tipo de referência ou valor que é adicionado a um ArrayList é implicitamente convertido via typecast para System.Object.

Se os itens são tipos de valor, eles devem sofrer um boxing quando adicionado à lista, e unboxing quando eles são recuperados.

A coersão , boxing e unboxing são operações que degradam o desempenho, o efeito de boxing e unboxing pode ser bastante significativo em cenários onde você deve percorrer grandes coleções.

E qual o resultado da execução desse código ?

Vamos ver...

Quando a coleção é iterada via instrução foreach os elementos int são aceitos pelo compilador mas os elementos do tipo string e object irão gerar um exceção em tempo de execução conforme mostra a figura.  Veja que isso ocorre em tempo de compilação.

Usando Generics não temos mais a necessidade de que todos os itens a sejam convertidos para Object e também permitimos que o compilador faça alguma verificação de tipo.

Vamos então tentar incluir um tipo diferente do definido na coleção de lista genérica ClasseGenerica<M>, no namespace System.Collections.Generic, a mesma operação de adicionar itens à coleção ficaria assim:

Agora temos um erro em tempo de projeto onde o compilador indica : cannot convert 'string' to 'int'.

Estamos assim detectando o erro mais cedo (o que é recomendado).

O tipo genérico M define quais tipos são permitidos, e, como definimos o M como sendo do tipo int - ClasseGenerica<int> - somente o tipo int pode ser incluído na coleção e por isso o compilador não compila, pois o método Adicionar possui argumento inválido.

2 - Desempenho

Outro destaque importante dos Generics é o desempenho.

A utilização de tipos por valor com classes de coleção não genéricas irá resultar em operações de boxing e unboxing trazendo uma sobrecarga quando o tipo por valor for convertido para o tipo por referência e vice-versa.

No exemplo abaixo a classe ArrayList armazena Object,  e,  o método Add() esta incluindo um tipo int. Assim o tipo int sofrerá o boxing e quando o valor for lido será feito o unboxing:

        static void Main(string[] args)
        {
            ArrayList obj = new ArrayList();
            obj.Add(50);           //boxing - converter o tipo por valor para tipo por referência
            int x = (int)obj[0];   //unboxing
            foreach (int i in obj)
            {
                Console.WriteLine(i);   //unboxing
            }
        }

Usando a classe Genérica (ClasseGenerica<M>)  e a definindo M como tipo int (ClasseGenerica<int>), um tipo int usado na classe é gerado automaticamente pelo compilador e as operações de boxing e unboxing não ocorrem, causando um melhor desempenho.

3 - Reutilização de Código

Usando Generics podemos reutilizar o código facilmente.

Uma classe Genérica pode ser definida uma única vez e pode ser instanciada de muitas maneiras diferentes, podendo inclusive, ser definida em uma linguagem da plataforma .NET e ser usada por outra linguagem.

Assim, no nosso exemplo, podemos instanciar a classe ClasseGenerica<M> com tipos int , string e também usar um tipo definido pelo usuário como uma classe Aluno:

    ClasseGenerica<int> obj = new ClasseGenerica<int>();
     obj.Adicionar(50);
     ClasseGenerica<string> obj2 = new ClasseGenerica<string>();
     obj2.Adicionar("Macoratti");
    ClasseGenerica<Aluno> obj2 = new ClasseGenerica<Aluno>();
    obj2.Adicionar(new Aluno { id = 1, nome = "Macoratti" });

Tente fazer isso usando uma classe não genérica !!!

Assim temos 3 importantes motivos para usar Generics.

Pegue o código aqui :   CSharp_Generics.zip

Na verdade, na verdade vos digo que quem ouve a minha palavra, e crê naquele que me enviou, tem a vida eterna, e não entrará em condenação, mas passou da morte para a vida.
João 5:24


Referências:


José Carlos Macoratti