terça-feira, 22 de janeiro de 2013

.NET Delegates, Funcs, Actions

Delegate é um tipo especial de classe no .NET que quando instanciado, armazena referência a um método ou a uma lista de métodos, incluindo anonymous methods, desde que possuam assinatura compatível com sua definição.

Com esse recurso, podemos passar delegates como parâmetros a outros métodos e/ou guardar referências a esses métodos para uso posterior.

Delegates, assim como classes, devem ser previamente definidos e podem ser instanciados direta ou indiretamente, e após sua instanciação, se comportam como se fossem métodos comuns. Quando invocados, executam o método referenciado.

Os delegates mais usados no .NET são os do tipo: EventHandler, Action e suas variações, Func<TResult> e suas variações.

Se você utiliza .NET 3.5 ou superior, provavelmente nunca irá criar um novo tipo de delegate, basta utilizar um delegate já existente.

Declarando Delegates


Para entendermos melhor os delegates, vamos declarar alguns antes de utilizá-los. Cada linha abaixo define um delegate diferente:

    public delegate void VoidSemParametros();
    public delegate void VoidComParametros(string valor);
    public delegate int IntSemParametros();
    public delegate int IntComParametros(int x, int y);

A primeira linha define um delegate chamado VoidSemParametros, e podemos associá-lo a um método em que seu retorno seja do tipo void, e que não possua nenhum parâmetro. Vamos então declarar um método que satisfaça essas condições:

    public void MetodoSemParametros() {
        Console.WriteLine("MetodoSemParametros");
    }

Agora que temos um método com assinatura e retorno compatível, podemos instanciar o delegate passando o método como parâmetro em seu construtor:

    /* Instancia VoidSemParametros utilizando construtor (new()) */
    var delegateNew = new VoidSemParametros(MetodoSemParametros);
    delegateNew(); /* executa */

Um outro método que também é compatível com o delegate VoidSemParametros:

    public void OutroMetodoSemParametros() {
         Console.WriteLine("OutroMetodoSemParametros");
    }

Dessa vez, instanciaremos o delegate passando diretamente o método:

    /* Instancia VoidSemParametros apontando direto para um método com assinatura compatível */
    VoidSemParametros delegateExplicito = OutroMetodoSemParametros;
    delegateExplicito(); /* executa */

Quando rodarmos este programa, ao executar o delegate, veremos que o método que foi passado como referência será executado, imprimindo alguma coisa na saída do Console.

Podemos também instanciar um delegate via anonymous methods:

    /* Instancia VoidSemParametros utilizando anonymous delegate */
    VoidSemParametros delegateInline = delegate() {
        Console.WriteLine("Anonymous delegate...");
    };

Também é possível criar instâncias de delegates através da combinação de dois ou mais instâncias de delegates diferentes. Quando este delegate é executado, executará os métodos referenciados na ordem em que foram "somados":

    /* Instancia um delegate combinando vários delegates */
    var delegateCombinado = delegateInline + delegateNew;

Pode-se também utilizar concatenação de delegates, utilizando o operador += em um delegate previamente instanciado:

    /* Concatena delegates */
    delegateCombinado += delegateExplicito;

    /* O método Console.WriteLine possui várias sobrecargas, uma delas possui assinatura compatível com VoidSemParametros */
    delegateCombinado += Console.WriteLine;

    /*executa todos os delegates que foram combinados na mesma ordem*/
    delegateCombinado();

Agora vamos utilizar o segundo delegate que declaramos, na segunda linha, identificado como VoidComParametros. Para isso, vamos criar um método com assinatura compatível. O retorno deve ser void, e deve possuir um parâmetro do tipo string:

    public void Escreve(string valor) {
        Console.WriteLine("Valor: {0}", valor);
    }

Seguindo a mesma linha de raciocínio, instanciaremos o delegate VoidComParametros de duas maneiras para que fique bem claro:

    var escreve = new VoidComParametros(Escreve);
    escreve("Escrito via método programa.Escreve");

    /* O método Console.WriteLine possui várias sobrecargas, uma delas possui assinatura compatível com VoidComParametros */
    VoidComParametros escreveDireto = Console.WriteLine;
    escreveDireto("Escrito via Console.WriteLine");

O terceiro delegate identificado como IntSemParametros possui um retorno do tipo Int e não possui parâmetros. Um método com assinatura compatível ficaria assim:

    public int FuncaoQueRetornaInt(){
        return 1;
    }

Da mesma maneira, para instanciar IntSemParametros, o código é este:

    var funcao = new IntSemParametros(FuncaoQueRetornaInt);
    escreve(funcao().ToString());

Para finalizar vamos criar dois métodos compatíveis com a assinatura do último delegate, identificado como IntComParametros:

    public int Soma(int x, int y) {
        return x + y;
    }

    public int Multiplica(int x, int y){
        return x * y;
    }

Vamos inicializar IntComParametros primeiro apontando para o método Soma, executá-lo, e depois vamos alterar o delegate para que aponte para o método Multiplica:

    var calculadora = new IntComParametros(Soma);
    var resultadoSoma = calculadora(3,3);
    Console.WriteLine("Soma: {0}", resultadoSoma);

    calculadora = Multiplica; //mudou a referência para outro método
    var resultadoMultiplicacao = calculadora(3,3);
    Console.WriteLine("Multiplicação: {0}", resultadoMultiplicacao);

Encontramos neste útlimo exemplo um caso de uso real para a utilização de delegates. Imagine uma aplicação do tipo calculadora. O usuário troca a operação (soma, multiplicação, divisão, etc) e trocamos o método referenciado pelo delegate, retornando resultados diferentes.

Action

Disponibilizado no .NET 3.5, amplamente utilizado em sintaxe LINQ e em expressões Lambda, Action nada mais é que um delegate com a seguinte assinatura:
   
    public delegate void Action();

O .NET Framework possui previamente já definidos uma série de delegates/Actions com parâmetros genéricos para suportar LINQ: Action<T>, Action<T1,T2>, Action<T1, T2, T3>, Action<T1, T2, T3, T4>, etc. 

    public delegate void Action<T>(T parametro);
    public delegate void Action<T1, T2>(T param1, T2 param2);
    public delegate void Action<T1, T2, T3>(T param1, T2 param2, T3 param3);
//etc...

Cada "T" representa um tipo de parâmetro que o método deve possuir. Por exemplo, Action<string> corresponde a um delegate do tipo void com um parâmetro string. Action<string, int> corresponde a um delegate do tipo void que recebe dois parâmetros: string, int. De forma genérica, podemos utilizar esses delegates sem a necessidade de declarar delegates específicos. Vejamos alguns exemplos:

    /* Action é um delegate semelhante ao VoidSemParametros */
    var action = new Action(MetodoSemParametros);
    Console.WriteLine(action.Method.Name);

    /* Action<T> é um delegate semelhante ao VoidComParametros se T for do tipo String */
    Action<string> actEscreve = Escreve;
    actEscreve("Escrito via Action<string>");

Func<TResult>

Semelhantes a Action's, exceto pelo fato de que possuem retorno do tipo <TResult>. A assinatura do delegate Func<TResult> é a seguinte:

    public delegate TResult Func<out TResult>();

Do mesmo modo que Actions possuem diversas implementações disponíveis no .NET Framework, Func's também. Um detalhe importante é que o último parâmetro é sempre o tipo genérico de retorno que o método deve possuir em sua assinatura.


    public delegate TResult Func<T, out TResult>(T parametro);
    public delegate TResult Func<T1, T2, out TResult>(T1 param1, T2 param2 );
    public delegate TResult Func<T1, T2, T3, out TResult>(T1 param1, T2 param2, T3 param3);
    //etc.

Veja algumas maneiras de utilizar Func:

    Func<int> funqInt = FuncaoQueRetornaInt;
    Console.WriteLine("Func<int>: {0}"funqInt());

    Func<int> funqLambda = () => { return FuncaoQueRetornaInt(); };
    Console.WriteLine("Lambda: {0}"funqLambda());

    Func<intintint> funqParameters = Soma;
    Console.WriteLine("funqParameters: {0}"funqParameters(23));

    funqParameters = Multiplica;
    Console.WriteLine("funqParameters: {0}"funqParameters(23));

Eventos

Delegates são utilizados na implementação de eventos em aplicações .NET. Num próximo post, mostrarei como criar e implementar seus próprios eventos.

LINQ

A partir da versão .NET 3.5, com o advento do LINQ, delegates (na forma de Actions e Func's) são amplamente utilizados.

Resumo

Neste post, apresentei como declarar e utilizar delegates. É importante notar que a partir do .NET 3.5, com o advento do LINQ, foi disponibilizado os delegates genéricos Action e Func, e provavelmente você nunca mais precisará declarar um delegate novo. Basta utilizar uma das versões de Action e Func que se encaixa nas suas necessidades.

Código

O código completo para este programa encontra-se no Gist

Nenhum comentário:

Postar um comentário