A tecnologia da Microsoft veio para ficar. Este blog regista o evoluir do meu conhecimento sobre .NET. No entanto este blog não pretende focar apenas a tecnologia em si mas também todas as aplicações desta.

Terça-feira, Fevereiro 08, 2005

Delegates e Eventos

Temos estado a implementar o interface do jogo Intersecções mas, na última mensagem, deparamo-nos com algo novo, os eventos. Esta mensagem serve para estudar os eventos e os delegates, mecanismo subjacente aos eventos.

A utilização de apontadores para funções é algo muito útil noutras linguagens/plataformas. Pode ser utilizado, por exemplo, para o mecanismo de callback. Este mecanismo consiste em indicar o endereço de uma função que será chamada posteriormente. Por exemplo, com a API Win32, quando se pretende criar uma janela deve-se preencher uma estrutura WNDCLASS com diversos dados sobre essa janela. Um dos dados é precisamente o endereço da função responsável pelo processamento das mensagens que chegam à janela.

No entanto o mecanismo que é utilizado tem os seus problemas. Como estamos apenas a falar de um endereço podemos fazer todos os tipos de conversões e estragar a aplicação das formas mais imagináveis. Podemos utilizar o endereço de uma função que não tenha o mesmo número ou tipo dos parâmetros ou podemos até converter o endereço de algo que não seja sequer uma função para um apontador para uma função.

A plataforma .NET oferece uma versão mais segura dos apontadores para funções na forma do mecanismo de delegate. Um delegate não é mais que um tipo que encapsula uma referência para uma função. Podemos definir um delegate da seguinte forma:

delegate void Func(Int32 i);

Estamos, com esta linha de código, a criar um tipo que encapsula endereços de funções que recebem um inteiro e não devolvem qualquer valor. A utilização é igual à utilização de qualquer outro tipo.

class App {
    static void Main() {
        Func f;
        f = new Func(MyFuncInt);
        f(3);
    }

    private static void MyFuncInt(Int32 i) {
        Console.WriteLine(i);
    }
}

Na função Main estamos a criar uma instância do delegate, em que o construtor recebe o nome da função. Após a criação da instância, estamos a chamar a função que está encapsulada. Para esta acção o sintaxe é igual à chamada de uma função. Se executarmos este código, vemos o valor 3 no ecrã, confirmando que a função é executada.
De notar que, como os delegates são tipos, estes podem ser colocados ao nível da classe. Assim sendo, Func pode ser colocado antes da definição de App.

Então e tendo um delegate com a assinatura igual ao de cima, como é que é definido o tipo? O tipo será definido assim:

class Func : MulticastDelegate {
    public Func(Object obj, Int32 methodPtr) { /* ... */ }
    public void Invoke(Int32 i) { /* ... */ }
    public IAsyncResult BeginInvoke(Int32 i,
        IAsyncCallback callback, Object obj) { /* ... */ }
    public void EndInvoke(IAsyncResult result) { /* ... */ }
}

É importante referir o construtor e o método Invoke. Quando foi utilizado o delegate ainda há pouco, este não foi construído com os parâmetros que são indicados no construtor. O sintaxe utilizado na linguagem C# é um sintaxe simplificado que permite abstrair dos pormenores de implementação dos delegates. Quando um delegate é construído, é indicado no construtor a instância da qual pertence o método (ou null se for um método de tipo) e um valor inteiro que representa o método.
Da mesma forma, quando o foi chamado o método representado pelo delegate, não foi chamada explicitamente o método Invoke. Mas foi isso que aconteceu. De notar que o método Invoke tem exactamente a mesma assinatura do delegate.

Para confirmar estes pormenores pode ser utilizada a ferramenta ILDasm, com a qual podemos ver o tipo criado e o código em IL deste.

Então e os eventos? Um tipo que expõe um evento oferece a funcionalidade de notificar outros tipos da ocorrência de algo. Vamos considerar este exemplo: pretende-se um tipo capaz de gerar mensagens e que deve notificar outros tipos quando existir uma nova mensagem. Podemos escrever o seguinte código:

class MessageProducer {
    public delegate void MessageEventHandler(String message);
    public event MessageEventHandler MessageEvent;
    public void SendMessage(String message) {
        if(null == MessageEvent) {
            return;
        }
        MessageEvent(message);
    }
}

De notar que o tipo do evento é um delegate. Isto significa que todos os tipos que pretendam ser notificados da ocorrência do evento devem indicar um método com a assinatura definida no delegate para ser chamada aquando da ocorrência. Podemos, então, definir o tipo que irá receber as mensagens da seguinte forma:

class MessageConsumer {
    public MessageConsumer(MessageProducer mp) {
        mp.MessageEvent +=
        new MessageProducer.MessageEventHandler(ShowMessage);
    }

    private void ShowMessage(String message) {
        Console.WriteLine("Message: " + message);
    }
    private static void Main() {
        MessageProducer mp = new MessageProducer();
        MessageConsumer mc = new MessageConsumer(mp);
        mp.SendMessage("Some Message");
    }
}

Não é díficil, como se pode confirmar. Claro que existem mais coisas para referir, mas delego essa tarefa para uma mensagem posterior, para que se possa seguir com o jogo Intersecções. O objectivo desta mensagem era apenas fazer uma referência superficial ao tema.