Article Details                  
 
Novidades C# 2.0 - parte I (Generics)

Apresenta agumas das novidades a nível da linguagem C# na nova versão da framework.

Novidades C# 2.0 - parte I (Generics)


Autor: Luís Abreu

Conteúdo: Novidades existentes na nova framework 2.0 (Generics)

Ferramentas: Visual C# Express


Hoje vamos iniciar uma nova série sobre algumas das novidades existentes na nova framework utilizando para tal a linguagem C#. Iremos abordar os seguintes temas: generics, anonymous methods, Iterators e partial types. Para iniciarmos esta sequência, vamos falar acerca dos Generics e das principais regras associadas a esta nova feature.


O que são e para que servem


Uma das forma mais fáceis de introduzir um conceito é descrevê-lo de forma resumida. Os generics permitem parametrizar classes, estruturas, interfaces, delegates e métodos. A utilização desta nova funcionalidade será imediata para aqueles que utilizam o C++ (dada a sua semelhança a nível de sintaxe com os templates).


Actualmente a framework não permite a construção de código genérico adaptável a cada situação. Ou melhor, permite, mas não da forma mais correcta. Para percebermos melhor o que se passa, vamos introduzir uma classe que será utilizada ao longo deste artigo. O objectivo é construir uma classe que funcione como uma pilha (stack) e que consiga ser reutilizada para vários tipos. O código seguinte demonstra a aproximação seguida a nível da versão 1.1:



public class Stack
{
private ArrayList _elems = new ArrayList();
public Stack()
{
}
public void Push(object elem)
{
_elems.Add( elem );
}
public object Pop()
{
object aux = null;
if( IsEmpty ) return aux;
aux = Top;
_elems.RemoveAt(_elems.Count - 1);
return aux;
}
public object Top
{
get
{
return IsEmpty ? null : _elems[_elems.Count - 1];
}
}
public bool IsEmpty
{
get
{
return _elems.Count == 0;
}
}
}

Antes de continuarmos, convém referir que o exemplo não prima pela originalidade! Contudo não era esse o objectivo desta classe. O código apresentado permite utilizar a classe com qualquer tipo de elemento. Isto deve-se ao facto da classe utilizar o tipo object (que, como sabemos, é a classe base de todos os elementos definidos em .Net).


Bem, mas se já temos uma classe "genérica" que se adapta bem a qualquer elemento, então porque será necessário recorrer aos generics? Devido à falta de "tipificação" dos elementos! Sim, é verdade que construímos uma stack que consegue guardar qualquer tipo de elementos; contudo, o código actual permite guardar na mesma pilha elementos de tipos diferentes ( o que, como é óbvio, não é adequado à maioria dos casos). O código seguinte demonstra este problema:



Stack st = new Stack( );
st.Push( 1 );
st.Push( "OLA" );

while ( !st.IsEmpty )
{
Console.WriteLine( st.Pop( ).ToString( ) );
}


Solução para este problema? Existem algumas...uma delas consiste em definirmos a nossa classe Stack através de Generics.


Introdução aos Generics


A utilização de generics facilita a criação de tipos através de tipos parametrizados. O exemplo seguinte demonstra a utilização básica de generics:



public class StackGenerics<T>
{
ArrayList _elems = new ArrayList( );
public StackGenerics( )
{
}
public void Push( T elem )
{
_elems.Add( elem );
}
public object Pop( )
{
T aux = default( T );
if ( IsEmpty ) return aux;
aux = Top;
_elems.RemoveAt( _elems.Count - 1 );
return aux;
}
public T Top
{
get
{
return IsEmpty ? default( T ) : ( T )_elems [_elems.Count - 1];
}
}
public bool IsEmpty
{
get
{
return _elems.Count == 0;
}
}
}

Como tinha referido anteriormente, os programadores de C++ irão automaticamente adaptar-se facilmente a esta nova feature devido às semelhanças existentes a nível sintático entre os generics e os templates. Contudo, convém salientar que as semelhanças acabam aí, como iremos ver ao longo do artigo. Os principios básicos são simples: em vez de trabalharmos com um determinado tipo (como por exemplo objecto, como acontecia no excerto anterior), trabalhamos com um tipo genérico que só irá ser conhecido aquando da instanciação da classe. A nível sintático temos apenas de delimitar os parâmetros genéricos utilizando os caracteres < e >. Os tipos indicados entre esses caracteres são utilizados para parametrizar a classe StackGenerics, de forma a que esta classe passe a controlar os tipos armazenados no seu interior .


A instanciação de uma classe destas obriga à definição do tipo T. Assim, se quisermos construir uma classe StackGenerics que armazene apenas inteiros iremos escrever o seguinte código:


 

StackGenericsT<int> st = new StackGenerics( );
st.Push( 1 );
st.Push( 2 );

while ( !st.IsEmpty )
{
Console.WriteLine( st.Pop( ).ToString( ) );
}
Console.Read( );


Neste caso, estamos a definir a classe para armazenar inteiros. O exemplo apresentado é bastante simples e apresenta apenas uma classe parametrizada através de um único parâmetro; contudo, se for necessário, podemos utilizar vários parâmetros aquando da definição de uma classe: neste caso temos de separá-los utilizando o caracter , e delimitá-los utilizando os já conhecidos caracteres < e >. Por exemplo:




public class Test<A,B>{ ....}

Instanciação de tipos Generics


Antes de apresentarmos às restantes regras aplicáveis aos generics, convém termos uma ideia do que se passa a nível da compilação e instanciação de um tipo destes. Como costuma acontecer com as outras classes, uma classe parametrizada é representada em IL após ser compilada. A diferença reside na imposição da especificação dos parâmetros associados à classe que, como é óbvio, têm de ficar associados à classe parametrizada. Por exemplo, o excerto seguinte é retirado do IL associado ao método Push:



.method public hidebysig instance void Push(!0 elem) cil managed

Ao ser criada a primeira instância de um determinado tipo, o JIT converte o IL para linguagem nativa e efectua a substituição dos parâmetros pelos tipos correctos. Referências futuras a elementos desse tipo (ou seja, do tipo da classe parametrizada) que utilizem parâmetros do mesmo tipo reutilizam o mesmo código nativo. Por outras palavras, se o nosso código contivesse uma segunda instância de StackGenerics<int> então o código nativo gerado aquando da primeira instanciação de um elemento desse tipo iria ser reaproveitado.


Resumindo, a framework é responsável por criar (em código nativo) uma especialização para cada tipo desde que esse tipo seja um value type. No caso dos reference types é apenas gerada uma instanciação em código nativo. A explicação para este facto é simples: na prática, todos os elementos deste tipo (ao nível nativo, isto é) são armazenados como apontadores (e por isso ocupam o mesmo espaço). Só para concluir esta secção, convém referir que este processo de instanciação tem um nome: generic type instantiation.


Métodos genéricos


Para além das classes, também é possível parametrizar outros tipos de elementos. Nesta secção vamos apenas demonstrar (de uma forma rápida) como podemos parametrizar um método que não pertence a uma classe parametrizada. Para demonstrar essa funcionalidade, cá vai um exemplo que não faz (praticamente) nada:




static public void TestMethod<T>( T par )
{
Console.WriteLine( par.ToString( ) );
}


 


Restrições aos parâmetros


Devido ao facto dos parâmetros poderem ser substituídos pelos mais diversos tipos, existem várias restrições aplicáveis a esse tipo de parâmetros:



  • Um parâmetro ( também conhecido por type parameter) não pode ser utilizado como classe base ou como interface;

  • A procura de eventuais membros associados ao parâmetro é especificada através das chamadas constraints (mais pormenores nos próximos parágrafos);

  • Não é possivel atribuir o valor null a um parâmetro a não ser que esse parâmetro seja explicitamente relacionado com uma classe. Para inicializar uma variável do tipo parâmetro genérico podemos recorrer a expressões de default value (mais pormenores em seguida);

  • A expressão new só pode ser utilizada com um elemento se esse tipo contiver uma restrição relativa ao contrutor desse tipo (mais informações nos parágrafos seguintes);

  • Um parâmetro não pode ser utilizado num atributo;

  • Um parâmetro não pode ser utilizado para aceder a um membro estático de um tipo.


Membros de classes genéricas


Todos os membros de uma classe genérica podem utilizar os parâmetros definidos nas respectivas classes a que pertencem. Uma variável estática (static) é partilhada por todas as instâncias associadas a um determinado tipo "fechado". Um tipo fechado é um tipo em que o parâmetro genérico é substituído pelo tipo real (por exemplo, nos samples apresentados anteriormente, a classe StackGenerics<int> é considerada um tipo "fechado", enquanto que a classe StackGenerics<T> é considerada um tipo aberto).


Os construtores estáticos permitem inicializar eventuais campos estáticos de um tipo "fechado". Estes métodos podem aceder aos parâmetros genéricos e são evocados durante a primeira vez que uma instância do tipo "fechado" é criada ou quando um dos seus membros estáticos é acedido pela primeira vez.


O acesso a membros protegidos segue as mesmas regras definidas ao nível das classes "normais". Também é possível efectuarmos overloadings dos membros de uma classe genérica. A única regra que temos de ter presente está relacionada com o facto de os overloads serem efectuados tendo em atenção um tipo "fechado". Em seguida são apresentados alguns exemplos:



class Test<T>
{
void Method1( U u );
void Method1( long a ); //invalido: dois overloads com mm definicao se T=long

void Method2( int a, string b );
void Method2( U a, U b ); //valido: impossivel U ser int e string ao mm tempo
}

Também é possível efectuarmos o overriding de métodos definidos numa classe base. No caso da base ser um tipo "fechado" ou uma classe sem parâmetros genéricos, então o método não pode ter parâmetros genéricos; por outro lado, se estivermos a falar de tipos "aberto", então um método pode possuir parâmetros genéricos na sua declaração.



class T1<T>
{
public virtual T Method1();
}
class T2:T1<string>
{
public override string Method1( ); //ok: override de acordo com a instancia
}
class T3< U, V>:T1< U>
{
pubic override U Method1(); //OK
}

As classes genéricas também podem definir operadores e podem possuir classes definidas no seu interior. De referir que os parâmetros genéricos podem ser utilizados na classe interior e que essa classe pode ainda definir parâmetros genéricos adicionais.


Para além das classes, também é possível definir estruturas, interfaces, delegates e métodos com recursos a genéricos.


Restrições (constraints)


Devido à forma como funcionam os genéricos, torna-se necessário definir determinadas restrições de forma a que seja possível efectuar a compilação de uma classe deste tipo. A necessidade de restrições torna-se clara se olharmos para o seguinte excerto de código:



interface ITest
{
void Play();
}
public class Test<T>
{
public void P( T aux )
{
aux.Play(); //erro: nao existe forma de saber que T implementa este metodo
}
}

O único problema do excerto anterior reside na evocação do método Play. Uma vez que o parâmetro T pode ser substituído por qualquer tipo não é possível ao compilador efectuar a validação do método sem a utilização de uma restrição (também conhecida por constraint). Ao declaramos um genérico podemos definir um conjunto de restrições (constraints) que são aplicadas aos parâmetros genéricos. Assim, tendo em atenção o exemplo anterior, a introdução de restrições daria origem ao seguinte código:



interface ITest
{
void Play();
}
public class Test<T>
where T:ITest
{
public void P( T aux )
{
aux.Play();
}
}

Uma restrição é definida através do termo where associado ao parâmetro genérico e seguido de uma lista de tipos aplicáveis ao parâmetro genérico. Para além de tipos, é possível definir outra restrição especial: constructor constraint - definido através do termo new().


Quando o tipo indicado é uma classe, então têm que se verificar vários aspectos:



  • a classe não pode ser sealed;

  • o tipo não pode ser um dos seguintes: System.Array, System.Delegate, System.Enum ou System.ValueType;

  • o tipo não pode ser a classe object;

  • só pode haver um parâmetro do tipo classe na lista de restrições.


No caso do tipo especificado na lista de restrições ser um interface, então a única restrição que se coloca é a não repetição desse interface na lista de restrições.


Expressões


Existem alguns aspectos que devem ser tidos em atenção aquando da utilização de genéricos. Durante a declaração de uma variável poderá ser necessário inicializá-la. Quando utilizamos genéricos a questão que se coloca é: qual o valor por defeito utilizado para inicializar uma variável do tipo do parâmetro genérico? A solução é simples e resume-se à utilização das chamadas default value expressions através do termo default. O excerto seguinte demonstra a utilização deste termo reservado:



public object Pop( )
{
T aux = default( T );
if ( IsEmpty ) return aux;
aux = Top;
_elems.RemoveAt( _elems.Count - 1 );
return aux;
}

Utilizando este tipo de expressões é possível garantir a correcta inicialização dos diferentes tipos, uma vez que o termo default será susbtituído pelo valor por defeito associado ao tipo (por exemplo, no caso dos objectos será aplicado o valor null).


Construção de novos elementos


A contrução de novos tipos no interior de classes genéricas só é possível se o parâmetro genérico estiver associado a uma restrição do tipo constructor constraint (definida através de new() ):



public class ConstructorContraint<T>
where T:new()
{
public ConstructorContraint( )
{
}
public void Build( )
{
T aux = new T( );//possivel devido a utilizacao da constraint new
}
}

Outras considerações


Convém ainda referir que a restante utilização de expressões/operadores está dependente do tipo associado ao parâmetro genérico. Por exemplo, só podemos aplicar o operador as a um parâmetro genérico que esteja associado a uma classe através de uma restrição.


Suporte da framework


A nova framework já traz várias classes que utilizam genéricos. Estas classes estão situadas no namespace System.Collections.Generics e permitem construir várias colecções fortemente tipificadas de acordo com as regras apresentadas ao longo deste artigo. A lista de classes é grande, pelo que se redirecciona o utilizador para a consulta da documentação de forma a obter mais informações sobre este assunto.


Conclusões finais


Este artigo apresentou uma das novidades da nova framework: os chamados genéricos ou generics. Ao longo deste documento foram apresentados vários aspectos importantes relativos à utilização desta nova funcionalidade utilizando a linguagem de programação C#. No próximo artigo vamos falar acerca de métodos anónimos (outra das novidades da nova framework).


Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções para progC@netmadeira.com.


Fiquem bem e boa programação! Até à próxima.


Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu


Written By: labreu
Date Posted: 4/14/2006
Number of Views: 644

Return