Novidades C# 2.0 - parte II (Métodos anónimos )
Autor: Luís Abreu
Conteúdo: Novidades existentes na nova framework 2.0 (Métodos anónimos )
Ferramentas: Visual C# Express
A definição de métodos compatíveis com delegates para efectuar o processamento de eventos sempre foi uma das tarefas menos interessantes, mas necessárias, d a versão 1.x da framework .Net. Na versão 2.0 já não irá ser necessário escrever essas instruções para efectuarmos o processamento de um evento (ou executarmos um método de callback) devido à introdução de uma nova feature designada de métodos anónimos (anonymous methods).
Este texto irá apresentar as principais regras que têm de ser tidas em conta para a utilização desta nova feature. Tal como aconteceu no primeiro artigo desta série, este artigo irá apenas apresentar excertos de código de forma a enquadrar o leitor com as novas funcionalidades disponibilizadas.
Introdução aos métodos anónimos
A utilização de métodos anónimos simplifica a evocação de event handlers e de callbacks. Para demonstrarmos as vantagens decorrentes da utilização deste método, vamos supor que temos um formulário simples que apenas possui uma Label, uma TextBox e um botão. Vamos ainda supor que queremos processar o evento de click do botão (mostrando uma mensagem de boas vindas ao utilizador). Com os métodos anónimos podemos introduzir o código necessário através das seguintes instruções:
partial class Form1: Form
{
public Form1( )
{
InitializeComponent( );
this._msg.Click += delegate
{
MessageBox.Show( "Ola " + this.textBox1.Text );
};
}
}
Se não ligarmos ao termo partial (que será alvo dum artigo futuro) e nos concentrarmos apenas no evento Click, podemos confirmar que o processamento do evento é feito através de um método anónimo em que não foram definidos vários itens geralmente associados a um método (como por exemplo o nome e a lista de parâmetros). A definição de um método anónimo é sempre feita através do termo delegate. As instruções associadas ao método anónimo são delimitadas por chavetas (por outras palavras, o bloco associado ao método anónimo tem de ser obrigatoriamente delimitado por { e } ).
Antes de analisarmos as principais regras associadas a esta nova feature, convém darmos uma olhadela ao IL associado ao nosso código. Assim, o construtor da nossa classe é transformado no seguinte código:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 36 (0x24)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [System.Windows.Forms]System.Windows.Forms.Form::.ctor()
IL_0006: ldarg.0
IL_0007: call instance void TestCode.Form1::InitializeComponent()
IL_000c: ldarg.0
IL_000d: ldfld class [System.Windows.Forms]System.Windows.Forms.Button TestCode.Form1::_msg
IL_0012: ldarg.0
IL_0013: ldftn instance void TestCode.Form1::'<.ctor>b__0'(object,
class [mscorlib]System.EventArgs)
IL_0019: newobj instance void [mscorlib]System.EventHandler::.ctor(object,
native int)
IL_001e: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::
add_Click(class [mscorlib]System.EventHandler)
IL_0023: ret
} // end of method Form1::.ctor
hum...ao que parece o método anónimo vai ser transformado num método externo, ou seja, o compilador de C# está a transformar o método anónimo num método externo e a associar esse método ao evento através da criação de um novo delegate de forma explicita. Só para terminarmos este raciocinio, vamos analisar o método <.ctor>b__0:
.method private hidebysig instance void '<.ctor>b__0'(object A_1,
class [mscorlib]System.EventArgs A_2) cil managed
{
// Code size 28 (0x1c)
.maxstack 8
IL_0000: ldstr "Ola "
IL_0005: ldarg.0
IL_0006: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox TestCode.Form1::textBox1
IL_000b: callvirt instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Text()
IL_0010: call string [mscorlib]System.String::Concat(string,
string)
IL_0015: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_001a: pop
IL_001b: ret
}
Portanto, na prática o velho código que constumávamos utilizar continua a ser gerado; acontece que agora temos um atalho para gerarmos este tipo de instruções. Para além disso, temos ainda algumas vantagens decorrentes da utilização deste tipo de instruções (ou, dependendo do ponto de vista, comportamentos inesperados que podem gerar mais preocupações e retirar-nos algum do nosso precioso tempo). Bem, agora que já apresentamos os métodos anónimos, é chegada a hora de avançarmos e apresentarmos as principais regras que regem a utilização de métodos anónimos.
Sintaxe
Os métodos anónimos devem estar de acordo com a seguinte sintaxe:
delegate[( lista_parametros)]
{
instrucoes;
}
Resumindo, os métodos anónimos começam sempre com o termo reservado delegate. Em seguida podemos definir (opcionalmente) uma lista de parâmetros, sendo essa lista (opcional) sempre seguida pelo corpo do método, que deve ser delimitado por chavetas. Os métodos anónimos podem ser utilizados em expressões de criação de delegates, sendo que estes métodos devem estar de acordo com a definição dos delegates.
Já estou a ouvir perguntas interessantes: "mas então como é que o código apresentado na secção anterior compila? afinal não definimos parâmetros...e já agora, como é que é definido o tipo de retorno de um método anónimo? A secção anterior define a sintaxe, mas não encontro nenhuma referência à forma como é definido o tipo de retorno..."
As questões apresentadas são pertinentes! Vamos lá às respostas...Comecemos pelo problema associado à lista de parâmetros. Os mais atentos já repararam que a lista de parâmetros não é obrigatória, mas sim opcional. Quando não geramos uma lista de parâmetros de forma explicita, o compilador infere essa lista através da definição do delegate e automaticamente introduz essa lista quando definir o método em IL. No exemplo apresentado não necessitávamos de aceder aos parâmetros associados ao delegate; contudo, se necessitássemos de aceder a essa informação teríamos obrigatoriamente de definir a lista de parâmetros por forma a podermos aceder a esses valores no interior do método anónimo.
Quanto ao tipo de retorno, não será surpresa se dissermos que esse tipo de informação é obtido pelo compilador a partir da definição do delegate ao qual o método está associado. Portanto, o código associado ao método deverá retornar um valor apropriado à situação em que é utilizado.
Daqui resultam as primeiras duas regras associadas aos métodos anónimos:
- Os métodos anónimos têm de ter uma lista de parâmetros compatível com o delegate ao qual são associados
- se o método anónimo não definir explicitamente uma lista de parâmetros então o delegate não pode definir nenhum parâmetro de output;
- se o método anónimo possui uma lista de parâmetros, então esta deve estar de acordo com os parâmetros definidos no delegate. Isto implica que os parâmetros devem possuir o mesmo tipo, os mesmos modificadores (por exemplo, out) e devem estar definidos pela mesma ordem do delegate ao qual estão associados.
- O tipo de retorno de um método anónimo tem de ser compatível com o tipo de retorno do delegate.
- se o tipo de retorno do delegate é void, então o método anónimo não deve utilizar o termo return associado a uma expressão (por outras palavras, se for necessário deverá apenas ser utilizada uma expressão return;);
- se o delegate definir um tipo de retorno diferente de void, então o método deverá definir as suas expressões de retorno de forma a que estas produzam um resultado do tipo de retorno (ou que possa ser implicitamente convetido para esse tipo).
Regras associadas ao corpo de um método anónimo
Como já foi referido, os métodos anónimos possuem um corpo delimitado por chavetas. O que ainda não foi referido foram as regras associadas às instruções contidas neste tipo de blocos. O primeiro aspecto importante já foi referido: se for necessário aceder aos parâmetros definidos pelo delegate é obrigatório definir a lista de parâmetros.
Todas as expressões associadas a um método normal podem continuar a ser utilizadas neste tipo de métodos. Para além disso, é possível acedermos a variáveis declaradas no elemento que contém o delegate (ou seja, podemos aceder a variáveis definidas no método que contém o método anónimo, ou mesmo a variáveis relativas à classe onde o método anónimo foi definido).
Este último aspecto é muito interessante e efectua algumas modificações que poderão não ser detectadas à primeira vista. Mas então qual será o comportamento esperado se, por exemplo, um método anónimo aceder a uma variável definida no corpo do método que definiu esse método anónimo? Se modificarmos o nosso código inicial para algo semelhante a isto:
public Form1( )
{
InitializeComponent( );
int i = 0;
this._msg.Click += delegate
{
i++;
MessageBox.Show( "Ola pela " + i.ToString( ) + "º vez " + this.textBox1.Text );
};
}
O que acontecerá à variável i? Se estivéssemos a falar de um método local ou se a variável não fosse acedida pelo método anónimo, a variável iria ter o valor zero até ao fim do ciclo e seria destruida em seguida; contudo, a partir da altura em que esta é acedida pelo delegate, algo terá de ser feito em relação ao tempo de vida desta variável. A decisão tomada foi a de alterar automaticamente o tempo de vida da variável por forma a que esta passe a ter o mesmo tempo de vida do delegate que a utiliza. Este tipo de variáveis possui um nome especial: moveable variables.
Na prática, ao escrevermos código semelhante ao anterior, estamos a criar uma nova classe que irá encapsular a variável partilhada. Provavelmente é melhor recorrermos ao excelente .Net Reflector para vermos o que está acontecer após compilarmos o nosso código. Assim, é possível aferirmos que o método anónimo foi transformado numa classe interna que encapsula a variável partilhada (como demonstra o seguinte outline obtido a partir do Reflector):
internal class Form1 : Form
{
// Methods
public Form1();
protected override void Dispose(bool disposing);
private void InitializeComponent();
// Fields
private Button _msg;
private IContainer components;
private Label label1;
private TextBox textBox1;
// Nested Types
private sealed class <>c__DisplayClass1
{
// Methods
public <>c__DisplayClass1();
public void <.ctor>b__0(object, EventArgs);
// Fields
public Form1 <>4__this;
public int i;
}
}
Já agora, o código relativo ao contrutor do formulário (onde é definido o método anónimo) é o seguinte:
public Form1()
{
Form1.<>c__DisplayClass1 class1 = new Form1.<>c__DisplayClass1();
class1.<>4__this = this;
this.InitializeComponent();
class1.i = 0;
this._msg.Click += new EventHandler(class1.<.ctor>b__0);
}
Já agora, convém também referir que dentro de um bloco associado a um método anónimo apenas podemos aceder a parâmetros ref e/ou out definidos na lista de parâmetros do método anónimo associado. Se tal não suceder, então será gerado um erro de compilação. No caso do corpo do método anónimo aceder a um parâmetro "normal", então este também terá um tratamento semelhante ao da variável definida no interior do método onde foi definido o método anónimo: será encapsulado numa classe interna à classe onde foi definido o método anónimo.
Apenas mais umas referências úteis:
- se o valor resultante da expressão this for uma valor do tipo struct, então ocorrerá um erro de compilação (este tipo de acesso poderá ocorrer de forma explicita ou implicita - ex: this.x ou então simplesmente x, em que x é um membro da classe ou da struct em que foi definido o método);
- o bloco associado a um método anónimo não pode conter uma expressão goto, break ou continue que altere o fluxo para fora do próprio bloco.
De referir ainda que as regras anteriores relativas a variáveis continuam a ser aplicáveis. Por exemplo, se quisermos imprimir uma variável, temos de atribuir um valor a essa variável. O código seguinte gera um erro devido ao facto de não termos atribuido um valor à variável antes de a imprimirmos:
public Form1( )
{
InitializeComponent( );
int i;
this._msg.Click += delegate
{
i = 0;
};
MessageBox.Show( i.ToString( ) );
}
Conversões de métodos
É possível convertermos um método num delegate de forma implicita (semelhante ao que acontece nos métodos anónimos). Por outras palavras, se tivermos um método M e um delegate D e for possível criar uma expressão do tipo new D( M ), então existe uma conversão implicita de M para D. Na prática isto quer dizer que o seguinte código:
bt.Click += new EventHandler( ProcessClick );
pode ser substituído pelo seguinte:
bt.Click += ProcessClick; //ok: é possivel criar um delegate new EventHandler( ProcessClick );
Bom, sempre são menos umas letras para escrever :)
Conclusões finais
Este artigo apresentou outra das novidades da nova framework: os métodos anónimos ou anonymous methods. Ao longo deste artigo foram apresentadas as principais regras relacionadas com esta nova feature. No próximo item desta série, vamos abordar a nova forma de construirmos classes: estou a falar de classes parciais (ou, como também são conhecidas, partial classes).
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