Tuesday, May 15, 2007

MAIS UMA INTRO À .NET FRAMEWORK 2.0 - Parte III

Artigo anterior: MAIS UMA INTRO À .NET FRAMEWORK 2.0 - Parte II


3. Classes

A .NET Framework possui milhares de classes e cada uma delas possui diferentes métodos e propriedades. Contudo, a .NET Framework está implementada de uma forma bastante consistente, que permite que diferentes classes implementem os mesmos métodos e/ou propriedades da mesma forma.
Esta consistência é possível devido à existência dos conceitos de herança e interfaces, de que vou falar de seguida.

3.1. Herança

A herança de classes deve ser utilizada quando se pretende criar novas classes a partir de classes existentes, ou seja, quando se pretende criar uma classe nova que reaproveite funcionalidades de uma classe existente.
Por exemplo, a classe Bitmap herda da classe Image, mas a classe Bitmap implementa funcionalidades que não existem na classe Image. Isto significa que é possível utilizar uma instância da classe Bitmap da mesma maneira que se utilizaria uma instância da classe Image. Contudo, a classe Bitmap disponibiliza métodos adicionais que permitem ao programador realizar outro tipo de tarefas com imagens.
Outro exemplo é a criação de excepções customizadas que herdem da classe System.ApplicationException ou da classe System.Exception:
class DerivedException : System.Exception {
public override string Message {
get { return “Ocorreu uma excepção na Aplicação!”; }
}
}
É possível capturar e lançar excepções utilizando esta classe, visto que ela herda o seu comportamento da sua classe base (System.Exception):
try {
throw new DerivedException();
} catch (DerivedException dex) {
Console.WriteLine(“Origem: {0}, Erro: {1}”, dex.Source, dex.Message);
}
De notar que, para além desta classe suportar o comportamento de lançamento e captura de excepções, também suporta o membro Source (entre outros), uma vez que este foi herdado da classe System.Exception.
Outro benefício da herança é a possibilidade de utilizar classes derivadas (outro termo para classes herdadas) numa perspectiva de intercâmbio. Por exemplo, existem cinco classes derivadas de System.Drawing.Brush: HatchBrush, LinearGradientBrush, PathGradientBrush, SolidBrush e TextureBrush. O método Graphics.DrawRectagle requer um objecto do tipo Brush como um dos seus parâmetros. Mas, neste caso, nunca se passa um objecto do tipo da classe base (Brush), mas sim um objecto do tipo de uma das suas classes derivadas. Uma vez que todos eles herdam da mesma classe base, o método Graphics.DrawRectangle aceitará qualquer um deles. Da mesma forma, se criarmos uma classe derivada de Brush, esta poderia ser utilizada para passar para o método Graphics.DrawRectangle.

3.2. Interfaces

Os Interfaces, também conhecidos como contratos, definem um conjunto de membros que todas as classes que implementem o interface devem disponibilizar. Por exemplo, o interface IComparable define um método CompareTo, que permite que duas instâncias de uma classe sejam comparadas para verificar semelhança. Assim, todas as classes que implementem este interface, sejam elas classes costumizadas ou classes pertencentes à .NET Framework, devem implementar funcionalidade para o método CompareTo, de forma a serem comparáveis por semelhança.
É ainda possível criarmos os nossos próprios interfaces. Os interfaces são úteis quando é necessário criar múltiplas classes que têm um comportamento semelhante e podem ser utilizadas numa perspectiva de intercâmbio. Por exemplo, este bloco de código define um interface com três membros:
interface IMessage {
// Envia a mensagem. Retorna true se bem sucedida, false se falhar.
bool Send();
// A mensagem a enviar.
string Message { get; set; }
// O endereço para onde enviar.
string Address { get; set; }
}

Depois, se uma classe implementasse este interface, teríamos algo do género:
class EmailMessage : IMessage {
// Temos de implementar os membros do interface
public bool Send() {
.... // Implementação
}
public string Message {
get { ... }
set { ... }
}
public string Address {
get { ... }
set { ... }
}
// Outros métodos
.... // Implementação
}
As classes podem implementar vários interfaces. Assim, é possível uma classe implementar os interfaces IComparable e IDisposable, entre outros.

3.3. Classes Parciais

Classes parciais são um conceito novo na .NET Framework 2.0. Basicamente, as classes parciais permitem que a definição de uma classe (e a sua implementação) seja dividida por diferentes ficheiros de código-fonte. A vantagem desta aproximação está no facto de se esconderem detalhes da definição de uma classe, permitindo que as classes derivadas se concentrem nas partes da implementação mais relevantes para si.
Um exemplo de classes parciais são as classes que definem o inteface gráfico (GUI) de um Windows Form. Num ficheiro (normalmente com o nome [Nome do Form].Designer.cs) temos a definição e declaração de todos os aspectos relacionados com o desenho e propriedades dos controlos que o Form contém. Noutro ficheiro (para o mesmo exemplo teríamos [Nome do Form].cs) teremos a outra parte da classe parcial, em que se definiria a implementação propriamente dita das funcionalidades do Form (Event Handlers, métodos internos, variáveis de trabalho, etc.).

3.4. Genéricos

Genéricos também são um conceito novo da .NET Framework 2.0. Basicamente são uma parte do Sistema de Tipos da .NET Framework que permite a definição de um tipo deixando alguns detalhes por especificar. Em vez de se especificar os tipos dos parâmetros ou classes membros, pode-se permitir que o código que usa o nosso tipo que especifique esses detalhes. Isto permite que o código que consome os tipos especifique o tipo dos membros que usa de acordo com as suas necessidades.
A .NET Framework 2.0 inclui várias classes genéricas no namespace System.Collections.Generic, incluindo o Dictionary, Queue e SortedList. Estas classes funcionam da mesma forma que os seus equivalentes não genéricos no namespace System.Collections mas oferecem melhor performance e segurança de tipos.
Entre as vantagens de utilizar genéricos, pode-se destacar:
  • Menor número de erros de execução de código (runtime) – O compilador não consegue detectar erros de conversão de tipos de e para objectos do tipo Object. De qualquer forma, podem especificar-se restrições para as classes que usam genéricos, permitindo assim ao compilador que detecte tipos incompatíveis.
  • Melhor performance – Efectuar conversões de tipos requer encapsulamento (mais informação no ponto seguinte: Conversões entre tipos), que requer tempo de processador e abranda a performance. A utilização de genéricos não precisa de conversão ou encapsulamento, aumentado a performance.

3.4.1. Como criar Tipos Genéricos

Vamos observar a diferença entre duas classes, uma normal (Obj) e outra genérica (Gen):

class Obj {
public Object t;
public Object u;
// Construtor
public Obj(Object _t, Object _u){
t = _t;
u = _u;
}
}

class Gen<T, U> {
public T t;
public U u;
// Construtor
public Gen(T _t, U _u){
t = _t;
u = _u;
}
}

Como se pode observer, a classe Obj tem dois membros do tipo Object. A classe Gen, por sua vez, tem dois membros do tipo T e U. O código que consumir esta classe genérica é que vai determinar os tipos de T e de U. Dependendo da forma como esse código irá usar a classe Gen, T e U podem ser do tipo string, int, uma classe qualquer ou qualquer combinação daí resultante.

Contudo, existe uma limitação importante na criação de uma classe genérica. Esta será válida apenas e só se compilar com todas as possíveis construções do genérico, sejam elas do tipo int, string ou de qualquer outra classe. Basicamente, estamos limitados ao objecto de base Object quando escrevemos código genérico. Estas limitações não se aplicam ao código que consome o genérico, uma vez que este declara os tipos do código genérico.

3.4.2. Como consumir Tipos Genéricos

Quando se consome um tipo genérico, deve-se especificar os tipos de cada genérico utilizado. Pegando no exemplo anterior, poderíamos ter:

// Adicionar duas strings utilizando a classe Obj
Obj oa = new Obj(“Olá,”, “ Mundo!”);
Console.WriteLine((string)oa.t + (string)oa.u);

// Adicionar duas strings utilizando a classe Gen
Gen<string, string> ga = new Gen<string, string>(“Olá,”, “ Mundo!”);
Console.WriteLine(ga.t + ga.u);

// Adicionar um double e um int utilizando a classe Obj
Obj ob = new Obj(10.125, 2005);
Console.WriteLine((double)ob.t + (int)ob.u);

// Adicionar um double e um int utilizando a classe Gen
Gen<double, int> gb = new Gen<double, int>(10.125, 2005);
Console.WriteLine(gb.t + gb.u);

Como se pode facilmente observar do código acima, ambas as classes produzirão exactamente o mesmo resultado, contudo a classe Gen executará mais rapidamente devido ao facto de não necessitar de encapsulamento (boxing e unboxing) a partir da classe Object.

3.4.3. Como utilizar restrições em Tipos Genéricos

Os genéricos seriam extremamente limitados se apenas se pudesse escrever código que compilasse para qualquer classe, uma vez que estaríamos limitados às capacidades da classe base Object. Para superar esta limitação, os genéricos podem utilizar restrições para definir requerimentos nos tipos que o código que consome o genérico usa para esse mesmo genérico. Os genéricos suportam quatro tipos de restrições:

  • Interface – Determina que apenas tipos que implementem interfaces possam consumir o genérico.
  • Classe Base – Apenas tipos que equivalem ou herdam de uma determinada classe base podem consumir o genérico.
  • Construtor – Requer que o código que consome o genérico implemente um construtor sem parâmetros.
  • Tipo de Referência ou Valor – Requer que o código que consome o genérico seja um tipo de referência ou de valor.

Para definir uma restrição a um genérico, deve usar-se a cláusula where na definição do genérico:

class LimGen<T>
where T: IComparable {
public T t1;
public T t2;
// Construtor
public LimGen(T _t1, T _t2){
t1 = _t1;
t2 = _t2;
}
public Max(T _t1, T _t2){
if (t2.CompareTo(t1) <>

return t1;

} else {

return t2;

}

}

}

Esta classe irá compilar correctamente. Contudo, se removermos a cláusula where, o compilador irá retornar um erro indicando que o tipo genérico T não contém uma definição para o método CompareTo. Com esta restrição, garante-se que o método CompareTo estará sempre disponível.

3.5. Eventos

Um evento é uma mensagem enviada por um objecto para notificar a ocorrência de uma acção. A acção pode ser causada por intervenção do utilizador ou por qualquer outra situação da lógica de um programa. O objecto que despoleta o evento é denominado por event sender (originador do evento). O objecto que captura o evento e responde ao mesmo é denominado de event receiver (receptor do evento).

Na comunicação de eventos, o objecto que despoletou o evento não sabe que objecto ou método irá capturar o evento. Assim, é necessário um intermediário (um mecanismo do tipo apontador) entre a origem e o destino do evento. A .NET Framework define um tipo especial que proporciona a funcionalidade de um apontador de funções – o Delegate.

3.5.1. Delegates

Um Delegate é uma classe que pode armazenar uma referência para um método. Ao invés das outras classes, um Delegate possui uma assinatura e pode armazenar referências apenas para métodos que possuam a mesma assinatura (que receba o mesmo número de parâmetros e do mesmo tipo).


Enquanto os Delegates podem ser utilizados para outros fins, a sua principal utilidade tem a ver com a interligação entre objectos que geram eventos e métodos que os capturam e tratam. A declaração de um Delegate é suficiente para definir uma classe do tipo Delegate. A declaração define a assinatura e o CLR (Common Language Runtime) trata da implementação. A seguir temos um exemplo de uma declaração de um Delegate:

public delegate void DelegateDeUmEvento(object sender, EventArgs e);

A assinatura normal de um evento define um método que não retorna valor nenhum, recebe um parâmetro do tipo Object (que referencia a instância do objecto que despoletou o evento) e outro parâmetro derivado do tipo EventArgs que armazena os dados do evento.

EventHandler é um tipo de Delegate pré-definido que representa especificamente um método de tratamento de um evento que não gera dados. Se tivermos a necessidade de criar um evento que gere dados, temos que definir o nosso próprio tipo de dados do evento e, ou criar um delegate cujo segundo parâmetro seja do tipo de dados criado por nós, ou então usar a classe delegate com EventHandler pré-definido e substituir o nosso tipo de dados do evento pelo tipo de dados genérico definido no EventHandler de defeito.

3.5.2. Como responder a um evento

Para responder a um evento é necessário efectuar dois passos:

  • Criar um método que responda ao evento. Este método tem de possuir a mesma assinatura do delegate do evento:
    public void button1_click(object sender, EventArgs e) {
    // Implementação do método
    }
  • Adicionar uma referência indicando qual o método que trata o evento
    this.button1.Click += new System.EventHandler(this.button1_Click);

Desta forma, quando o evento ocorrer, o método especificado como aquele que trata o evento, será chamado e executará o código que implementa.

3.5.3. Como despoletar um evento

Quando se pretende despoletar um evento são necessários, pelo menos, três passos:

  • Criar um delegate:
    public delegate void OMeuEventHandler(object sender, EventArgs e);
  • Declarar um evento:
    public event OMeuEventHandler OMeuEvento;
  • Invocar o delegate dentro de um método quando é necessário despoletar o evento:

    OMeuEventHandler handler = OMeuEvento;
    EventArgs e = new EventArgs();

    if (handler != null) {
    // Invoca o delegate
    handler(this, e);
    }

De notar que, em C#, é necessário que se verifique se o EventHandler é nulo antes de o chamar.

3.6. Atributos

Atributos descrevem um tipo, método ou propriedade de uma forma que estes possam ser programaticamente acedidos através de Reflection (Reflexão). Alguns dos cenários comuns onde se usam atributos incluem:

  • Especificar que previlégios de segurança uma classe requer;
  • Especificar que previlégios de segurança estão proibidos para reduzir riscos de segurança;
  • Declarar capacidades como, por exemplo, suporte para serialização;
  • Descrever características da assembly, fornecendo um título, descrição e informação de copyright.

[assembly: AssemblyTitle("Executável da Aplicação.")]
[assembly: AssemblyDescription("Um software que faz coisas.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Easytronic,Lda.")]
[assembly: AssemblyProduct("App v1.2")]
[assembly: AssemblyCopyright("Copyright © 2006 by Easytronic,Lda., Lisboa, Portugal, EU. All rights reserved.")]
[assembly: AssemblyTrademark("App™ is a trademark of Easytronic,Lda., Portugal, EU.")]

Os atributos fazem muito mais do que descrever assemblies para outros programadores, elas podem inclusivamente declarar requisitos ou capacidades. Por exemplo, pode-se definir características relacionadas com Globalização, se está acessível a código COM, entre outros:

[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("8a19a392-aa02-4b2d-a784-7da16cfbbee8")]
[assembly: AssemblyVersion("1.2.0.0")]
[assembly: AssemblyFileVersion("1.2.0.0")]

Ou ainda, para permitir que uma classe seja serializada, é necessário adicionar o atributo Serializable a essa classe:

[Serializable]
class UmaClasse {

}

Sem este atributo, a classe do exemplo acima não seria serializável. Da mesma forma, o código seguinte utiliza atributos para declarar que necessita de aceder ao ficheiro C:\boot.ini. Devido a este atributo, o código em execução vai lançar uma excepção anterior ao acesso ao ficheiro, se não tiver previlégios suficientes para aceder ao ficheiro:

using System;
using System.Security.Permissions;

[assembly:FileIOPermissionAttribute(SecurityAction.RequestMinimum, Read=@”C:\boot.ini”)]
namespace ExemploDeclarativo {
class Classe1 {
[STAThread]
static void Main(string[] args) {
Console.WriteLine(“Olá Mundo!”);
}
}
}


3.7. Reencaminhamento de Tipos (Type Forwarding)

O Reencaminhamento de Tipos é uma nova funcionalidade da .NET Framework 2.0 e não é mais do que um atributo (implementado através de TypeForwardedTo) que nos permite mover um tipo de uma assembly (Assembly A), para outra assembly (Assembly B), mas executado de uma forma que não seja necessário recompilar os clientes que consomem a Assembly A. Após um componente ser finalizado e consumido por aplicações cliente, é possível utilizar o Reencaminhamento de Tipos para mover um tipo de uma assembly para outra e depois reenviar o componente actualizado para as aplicações clientes, que estas continuarão a funcionar sem necessidade de serem recompiladas.


O Reencaminhamento de Tipos funciona apenas para componentes referenciados por aplicações existentes. Quando se recompila a aplicação, têm de existir referências apropriadas para todos os tipos utilizados nessa aplicação.


Para executar Reencaminhamento de tipos, devem seguir-se os seguintes passos:

  • Adicionar um atributo TypeForwardedTo à assembly de origem;
  • Cortar a definição do tipo da assembly de origem;
  • Colar a definição do tipo da assembly de destino;
  • Recompilar ambas as assemblies.

O código seguinte demonstra a declaração de um atributo utilizado para mover o TipoA para a biblioteca LibDestino:

using System.Runtime.CompilerServices;
[assembly:TypeForwardedTo(typeof(LibDestino.TipoA))]


Próximo artigo: Conversões Entre Tipos