Monday, July 09, 2007

MAIS UMA INTRO À .NET FRAMEWORK 2.0 - Parte V

Capítulo 2 – Operações de Input e Output (I/O)

Este capítulo fala de um tipo de operações mais comum em programação, ou seja, a manipulação de ficheiros e/ou do sistema de ficheiros. As características básicas do sistema de I/O da .NET Framework 2.0 incluem aceder a ficheiros e pastas no sistema de ficheiros, trabalhar com streams de leitura e escrita, a utilização de streams de compressão e utilizar dispositivos de armazenamento.
1. Navegar no Sistema de Ficheiros

1.1. Quais são as classes do Sistema de Ficheiros?

Dentro do namespace System.IO existe um conjunto de classes que são utilizados para navegar e manipular ficheiros, pastas e drives. As classes do sistema de ficheiros estão divididas em dois tipos de classes: informativas e utilitárias.
A maioria das classes informativas derivam da classe base FileSystemInfo e expõem toda a informação sobre objectos do sistema de ficheiros. Como exemplos, temos as classes FileInfo e DirectoryInfo.
Adicionalmente, a classe DriveInfo representa uma drive no sistema de ficheiros, mas embora seja uma classe informativa, não deriva da classe FileSystemInfo devido ao facto de não partilhar um conjunto comum de comportamentos (por exemplo, é possível apagar ficheiros e pastas, mas não é possível apagar drives).

As classes utilitárias disponibilizam um conjunto de métodos estáticos para realizar determinadas operações em objectos do sistema de ficheiros tais como ficheiros, pastas e caminhos do sistema de ficheiros. Estas classes utilitárias incluem as classes File, Directory e Path, por exemplo.

1.2. A classe FileSystemInfo

A classe FileSystemInfo disponibiliza funcionalidades básicas para todas as classes informativas do sistema de ficheiros. As suas propriedades mais importantes são:
· Attributes: Obtém ou define FileAttributes de um ficheiro ou directório;
· CreationTime: Obtém ou define a hora em que um dado ficheiro ou directório foi criado;
· Exists: Verifica se um ficheiro ou directório existe no sistema de ficheiros;
· Extension: Obtém uma string com a extensão de um ficheiro ou directório;
· FullName: Obtém o caminho completo no sistema de ficheiros para um dado ficheiro ou directório;
· LastAccessTime: Obtém ou define a hora do último acesso a um ficheiro ou directório;
· LastWriteTime: Obtém ou define a hora da última escrita num ficheiro ou directório;
· Name: Obtém o nome simples de um ficheiro ou directório. Para um ficheiro, retorna o nome de um ficheiro num directório, para um directório retorna o último nome de directório na hierarquia do directório.

Para além destas propriedades, os métodos mais importantes desta classe são:
· Delete: Elimina um ficheiro ou directório do sistema de ficheiros;
· Refresh: Actualiza a informação da classe, com a informação mais actualizada sobre o sistema de ficheiros.

1.3. A classe FileInfo

A classe FileInfo disponibiliza funcionalidades que permitem aceder e manipular um ficheiro do sistema de ficheiros. As suas propriedades mais importantes são:

· Directory: Obtém o objecto DirectoryInfo que representa o directório em que o ficheiro está armazenado;
· DirectoryName: Obtém o nome do directório em que o ficheiro está armazenado;
· IsReadOnly: Obtém ou define a possibilidade do ficheiro ser alterado e/ou apagado;
· Length: Obtém o comprimento do ficheiro.

Os métodos mais importantes desta classe são:

· AppendText: Cria um novo objecto do tipo StreamWriter que permite a adição de texto ao ficheiro;
· CopyTo: Copia o ficheiro para uma nova localização;
· Create: Cria um novo ficheiro baseado na informação corrente sobre o ficheiro;
· CreateText: Cria um novo objecto do tipo StreamWriter e um novo ficheiro para escrita de texto;
· Decrypt: Decifra um ficheiro que foi cifrado pelo utilizador actual;
· Encrypt: Cifra um ficheiro de forma a que apenas o utilizador actual possa decifrar a informação nele constante;
· MoveTo: Move um ficheiro para uma nova localização;
· Open: Abre um ficheiro com privilégios específicos;
· OpenRead: Abre um ficheiro apenas com permissões de leitura;
· OpenText: Abre um ficheiro e devolve um objecto do tipo StreamReader para permitir a leitura do texto do ficheiro;
· OpenWrite: Abre um ficheiro apenas com permissões de escrita;
· Replace: Substitui um ficheiro com a informação no objecto FileInfo corrente.
Para obter informação sobre um determinado ficheiro é necessário criar um novo objecto do tipo FileInfo utilizando o caminho para o ficheiro. De seguida, aceder às propriedades do ficheiro, como se pode ver no exemplo abaixo:
FileInfo oMeuFicheiro = new FileInfo(@”c:\boot.ini”);
if (oMeuFicheiro.Exists())
{
Console.WriteLine(“Nome do Ficheiro : {0}”, oMeuFicheiro.Name);
Console.WriteLine(“Caminho : {0}”, oMeuFicheiro.FullName);
}
Utilizando o objecto FileInfo desta forma permite-nos obter informação sobre um ficheiro no sistema de ficheiros.
Da mesma forma, a classe FileInfo permite operações sobre ficheiros sem ser apenas consulta das suas propriedades. Como exemplo deste tipo de operações, temos o código seguinte que copia um ficheiro para outro local no sistema de ficheiros:
FileInfo oMeuFicheiro = new FileInfo(@”c:\boot.ini”);
oMeuFicheiro.CopyTo(“c:\boot.bak”);
O mesmo tipo de procedimento pode ser utilizado para criar ou mover ficheiros.
1.4. A classe DirectoryInfo

A classe DirectoryInfo disponibiliza funcionalidades que permitem aceder e manipular um directório do sistema de ficheiros. As suas propriedades mais importantes são:

· Parent: Obtém o objecto DirectoryInfo que representa o directório-pai do directório actual;
· Root: Obtém a parte da raíz do caminho do directório numa string.

Os métodos mais importantes desta classe são:

· Create: Cria um novo directório baseado na informação corrente sobre o directório;
· CreateSubdirectory: Cria um novo directório como directório-filho do directório corrente;
· GetDirectories: Obtém um array de objectos DirectoryInfo que representam os subdirectórios do directório corrente;
· GetFiles: Obtém um array de objectos FileInfo que representam todos os ficheiros constantes no directório corrente;
· GetFileSystemInfos: Obtém um array de objectos FileSystemInfo que representam todos os ficheiros e subdirectórios constantes no directório corrente;
· MoveTo: Move um directório para uma nova localização.
Aceder aos ficheiros de um directório é muito semelhante a aceder à informação sobre um ficheiro. Basta criar um objecto do tipo DirectoryInfo válido utilizando o caminho para o directório e, posteriormente, invocar o método GetFiles para enumerar os ficheiros de um directório:
DirectoryInfo oMeuDirectorio = new DirectoryInfo(@”c:\windows”);
Console.WriteLine(“Directório : {0}”, oMeuDirectorio.FullName);
foreach (FileInfo ficheiro in oMeuDirectorio.GetFiles())
{
Console.WriteLine(“Ficheiro : {0}”, ficheiro.Name);
}
1.5. A classe DriveInfo

A classe DriveInfo disponibiliza funcionalidades básicas que permitem aceder e manipular uma drive do sistema de ficheiros. As suas propriedades mais importantes são:

· AvailableFreeSpace: Obtém o tamanho de espaço disponível na drive. O espaço retornado pode ser diferente do valor retornado pela propriedade TotalFreeSpace, dependendo das quotas de disco;
· DriveFormat: Obtém o formato da drive (FAT32 ou NTFS);
· DriveType: Obtém o tipo de drive na forma da enumeração DriveType;
· IsReady: Obtém o estado da drive, indicando se está pronta a ser utilizada;
· Name: Obtém o nome da drive;
· RootDirectory: Obtém um objecto DirectoryInfo que representa o directório raíz da drive;
· TotalFreeSpace: Obtém o espaço total livre da drive;
· TotalSize: Obtém o tamanho total da drive;
· VolumeLabel: Obtém ou define a etiqueta da drive. Apenas pode ser definido para drives que não são apenas de leitura (read only).
O método mais importante desta classe é o método GetDrives. É um método estático que retorna todas as drives do sistema corrente.
1.6. A enumeração DriveType

A enumeração DriveType disponibiliza os tipos possíveis de drives que podem ser representados por um objecto DriveInfo. Esta é composta pelos seguintes membros:

· CDRom: Uma drive óptica. Pode ser um CD-Rom um DVD, etc.;
· Fixed: Um disco rígido;
· Network: Uma drive de rede mapeada;
· NoRootDirectory: Uma drive que não possui directório raíz;
· Ram: Uma drive RAM;
· Removable: Uma drive amovível;
· Unknown: Uma drive para a qual não se consegue determinar o tipo.
Para enumerar as drives de um sistema de ficheiros, é necessário chamar o método GetDrives da classe DriveInfo. Depois, percorre-se o array de objectos DriveInfo retornados pelo método:
DriveInfo[] drives = DriveInfo.GetDrives();
foreach (DriveInfo drive in drives)
{
Console.WriteLine(“Drive: {0}”, drive.Name);
Console.WriteLine(“Tipo: {0}”, drive.DriveType);
}

De notar que todas as drives ópticas (CD, CD/R, DVD, DVD/R, etc.) são marcadas como sendo do tipo DriveInfo.CDRom.
1.7. A Classe Path

A classe Path disponibiliza métodos que permitem aceder e manipular um caminho do sistema de ficheiros. Os métodos mais importantes desta classe são:

· ChangeExtension: Utiliza um caminho existente e retorna um novo caminho com a extensão do nome do ficheiro alterada. De notar que apenas a string do caminho é alterada, a extensão do ficheiro mantém-se;
· Combine: Combina duas strings de caminho compatíveis;
· GetDirectoryName: Retorna o nome do directório do caminho especificado;
· GetExtension: Retorna o nome da extensão de um ficheiro no caminho especificado;
· GetFileName: Retorna o nome de um ficheiro no caminho especificado;
· GetFileNameWithoutExtension: Retorna o nome de um ficheiro sem a extensão, no caminho especificado;
· GetFullPath: Retorna o caminho completo para o caminho especificado. Este método pode ser utilizado para resolver caminhos relativos;
· GetPathRoot: Retorna o nome do directório raíz no caminho especificado;
· GetRandomFileName: Gera um nome de ficheiro aleatoriamente;
· GetTempFileName: Gera um ficheiro temporário no sistema e retorna o caminho completo para esse ficheiro;
· GetTempPath: Retorna o caminho do directório de ficheiros temporários para o utilizador ou sistema correntes;
· HasExtension: Indica se o nome do ficheiro de um determinado caminho possui extensão;
· IsPathRooted: Indica se o caminho especificado inclui um directório raíz.
A classe Path permite-nos interrogar e trabalhar as partes individuais de um caminho do sistema de ficheiros. Em vez de escrevermos o nosso próprio código de tratamento dessas partes individuais de um caminho, a classe Path permite-nos responder às questões mais comuns sobre um caminho do sistema de ficheiros. Se quisermos, por exemplo, alterar a extensão de um ficheiro, poderíamos efectuá-lo, da seguinte forma:

string oMeuCaminho = @“C:\boot.ini”;
Console.WriteLine(oMeuCaminho);
Console.WriteLine(“Extensão: {0}, Path.GetExtension(oMeuCaminho));
Console.WriteLine(“Alterado: {0}, Path.ChangeExtension(oMeuCaminho, “bak”));
1.8. A Classe FileSystemWatcher

Uma das tarefas mais comuns em programação é monitorizar o sistema de ficheiros para determinar quando acontecem alterações e reagir de acordo com elas. A classe FileSystemWatcher disponibiliza funcionalidades básicas que permitem monitorizar um sistema de ficheiros e verificar alterações. As suas propriedades mais importantes são:

· EnableRaisingEvents: Obtém ou define se o objecto de monitorização deve despoletar eventos. Normalmente, esta propriedade é utilizada para ligar ou desligar a monitorização de ficheiros e/ou directórios;
· Filter: Obtém ou define o filtro de ficheiros que é utilizado para determinar quais as alterações de ficheiros a monitorizar. Um filtro vazio indica todos os ficheiros;
· IncludeSubDirectories: Obtém ou define se a monitorização de um directório deve ou não estender-se aos seus subdirectórios;
· NotifyFilter: Obtém ou define o tipo de alterações a monitorizar. Por defeito, todas as alterações (criação, eliminação, renomeação e modificação) são notificados;
· Path: Obtém ou define o caminho do directório a monitorizar.
O método mais importante desta classe é o método WaitForChanged. É um método síncrono utilizado para monitorizar alterações a um directório e retornar uma estrutura que contém todas as alterações efectuadas.
Para além disso, esta classe possui um conjunto de eventos utilizados no processo de monitorização de um directório, designadamente:

· Changed: Ocorre quando um ficheiro ou directório foram alterados no directório monitorizado;
· Created: Ocorre quando um ficheiro ou directório foram criados no directório monitorizado;
· Deleted: Ocorre quando um ficheiro ou directório foram eliminados no directório monitorizado;
· Renamed: Ocorre quando um ficheiro ou directório foram renomeados no directório monitorizado.
Assim, para monitorizar as alterações a um directório, é necessário criar um objecto FileSystemWatcher, especificando a propriedade Path. Depois, registar os eventos que se pretendem monitorizar e, finalmente, permitir a captura de eventos, colocando a propriedade EnableRaisingEvents a true:
// Criar o objecto FileSystemWatcher
FileSystemWatcher bigbrother = new FileSystemWatcher();
bigbrother.Path = @”C:\”;
// Registar os eventos
bigbrother.Created += new FileSystemEventHandler(bigbrother_changed);
bigbrother.Deleted += new FileSystemEventHandler(bigbrother_changed);
// Começa a monitorização
bigbrother.EnableRaisingEvents = true;
// Event Handler
static void bigbrother_changed(object sender, FileSystemEventArgs e)
{
Console.WriteLine(“O directório foi alterado ({0}): {1}”,
e.ChangeType, e.FullPath);
}

Sunday, July 01, 2007

MAIS UMA INTRO À .NET FRAMEWORK 2.0 - Parte IV

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

4. Conversões entre tipos

Conversões entre tipos é uma das tarefas mais comuns em programação orientada a objectos. Várias vezes é necessário converter uma variável de um tipo para outro antes de a passar como parâmetro de um método ou para executar determinado cálculo.
Existem dois tipos de conversões: implícitas e explícitas. Em C# só é possível efectuar conversões implícitas entre tipos se não existir perda de precisão. Isto significa que é possível efectuar conversão implícita em C# apenas e só se o tipo de destino puder aceitar todos os valores possíveis para o tipo de origem. Isto é denominado de conversão generalista (widening conversion):

int i = 1;
double d = 1.0001;
d = i; // Conversão implícita permitida

Se o espectro de conversão ou precisão do tipo de origem excede o do tipo de destino, a operação denomina-se de conversão de estreitamento (narrowing conversion) e normalmente necessita que a conversão seja explícita. Existem várias formas de realizar uma conversão explícita:
System.Convert: Converte entre tipos que implementem o Interface System.IConvertible;
(tipo) cast: Converte entre tipos que definem operadores de conversão;
tipo.ToString e tipo.Parse: Converte entre objectos do tipo string e objectos de tipos base. Se a conversão não for possível, a operação resulta numa excepção;
tipo.TryParse e tipo.TryParseExact: Converte entre um objecto do tipo string e um objecto de um tipo base. Se a conversão não for possível, devolve um valor false;
Os métodos TryParse, TryParseExact e TryCast são novos na .NET Framework 2.0. Conversões explícitas falham se o valor de origem exceder o espectro de valores aceites pelo tipo de destino ou se a conversão entre os tipos não estiver definida, por isso este tipo de conversões deve ser incluído entre blocos Try ou utilizando os métodos TryCast ou TryParse para ser possível verificar o valor de retorno.

4.1. O que é Boxing e Unboxing?

Boxing acontece quando se converte um tipo de valor para um tipo por referência e Unboxing é, naturalmente, o processo inverso, ou seja, a conversão de um tipo de referência para um tipo de valor. No exemplo seguinte podemos ver um exemplo de Boxing através da conversão de um Integer (tipo de valor) para um Object (tipo por referência):

int i = 123;
object o = (object)i; // Conversão através da utilização de um type cast
Da mesma forma, se pode exemplificar o processo inverso, ou seja, Unboxing:
object o = 123;
int i = (int)o;
As operações de Boxing e Unboxing implicam um acréscimo de processamento (overhead), por isso devem ser evitados quando se programam tarefas que se repetem de uma forma intensa. Boxing também ocorre quando são chamados métodos virtuais de uma estrutura que herde de System.Object como, por exemplo, o método ToString(). Assim, para evitar procedimentos de Boxing desnecessários deve ter-se as seguintes sugestões em consideração:

• Implementar versões específicas por tipo (overloads) de métodos que aceitem vários tipos de valor. É uma boa prática criar várias versões do mesmo método com parâmetros diferentes e definitivamente é melhor solução do que implementar apenas uma versão desse método que aceite um parâmetro do tipo Object;
• É preferível utilizar genéricos sempre que possível em vez de utilizar argumentos do tipo Object;
• Quando estamos a criar as nossas próprias estruturas, devemos fazer as nossas próprias versões (override) dos métodos virtuais ToString, Equals e GetHash.

4.2. Como implementar conversão em tipos customizados?

Existem várias formas de definir conversões entre tipos definidos por nós. A técnica a escolher depende do tipo de conversão que pretendemos executar:

• Deve definir-se operadores de conversão para simplificar conversões implícitas e explícitas entre tipos numéricos. De notar que os operadores de conversão são uma novidade na .NET Framework 2.0;
• Deve implementar-se versões customizadas (override) do método ToString para conversões onde o objecto de destino é do tipo string e do método Parse para conversões em que o objecto de origem é do tipo string;
• Deve implementar-se o interface IConvertible para permitir conversões utilizando System.Convert, especialmente quando se implementam conversões específicas de uma cultura (Culture);
• Deve implementar-se uma classe TypeConverter para permitir conversão em Design Time, que será utilizada na janela de propriedades do Visual Studio (para mais informações, visitem http://msdn2.microsoft.com/en-us/library/37899azc.aspx).
A definição de operadores de conversão permitem a atribuição directa de um tipo de valor para um tipo customizado. Deve usar-se a instrução implicit para conversões que não impliquem perda de precisão e a instrução explicit para conversões que estão sujeitas a perda de precisão:
struct TipoA
{
public int Value;

// Permite conversão implícita de um inteiro
public static implicit operator TipoA(int arg)
{
TipoA res = new TipoA();
res.Value = arg;
return res;
}

// Permite conversão explícita para um inteiro
public static explicit operator int(TipoA arg)
{
return arg.Value;
}

// Permite conversão para String (evita Boxing)
public override string ToString()
{
return this.Value.ToString();
}
}

O exemplo anterior também efectua uma costumização do método virtual ToString par evitar o processo de Boxing. Agora seria possível atribuir inteiros a este tipo costumizado, como se pode ver abaixo:
TipoA a;
int i;
// Conversão implícita é aceite
a = 42; // Em vez de se utilizar a.Value = 42
// Conversão explícita
i = (int)a; // Em vez de i = a.Value
Console.WriteLine(“a = {0}, i = {1}”, a.ToString(), i.ToString());

Para se implementar o interface System.IConvertible é necessário especificar isso na definição do tipo costumizado. A implementação deste interface implica definir implementação para 17 métodos, incluindo GetTypeCode, ChangeType e ToType para cada tipo base com o qual se pretenda efectuar operações de conversão. Não é necessário implementar todos os métodos e alguns, como por exemplo ToDateTime, serão provavelmente inválidos. Para métodos inválidos, basta criar uma excepção. Após a implementação do interface IConvertible, pode-se converter a nossa classe utilizando System.Convert:

TipoA a;
bool b;
a = 1;
// Conversão utilizando ToBoolean
b = Convert.ToBoolean(a);
Console.WriteLine(“a = {0}, b = {1}”, a.ToString(), b.ToString());

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

Wednesday, April 25, 2007

MAIS UMA INTRO À .NET FRAMEWORK 2.0 - Parte II

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

2. Tipos por Referência

Mais uma vez, é importante referir que um dos mais comuns erros no início da programação em .NET tem a ver com confundir Tipos de Valor ou de Referência com passar argumentos para métodos por Valor ou por Referência. Volto a frisar que são conceitos diferentes e para o qual qualquer pessoa deve ter cuidado.

Os Tipos por Referência armazenam o endereço da sua informação na stack e também são conhecidos como apontadores (pointers). E o que quer isto dizer? Quer dizer que os dados propriamente ditos estão armazenados na heap, mas existe uma referência à sua localização na stack (um apontador), uma área da memória que está acessível mais rapidamente pelo código em excução.

O ambiente de execução gere a memória armazenada na heap utilizando um processo denominado Garbage Collection (“Recolha de Lixo”), que consiste em ir libertando memória periodicamente, eliminando objectos à medida que estes deixam de ser referenciados.

Uma vez que os Tipos por Referência representam o endereço de memória de um pedaço de informação em vez da informação propriamente dita, assignar uma variável por referência a outra variável por referência terá como resultado a cópia do endereço de memória e não dos dados em si, como seria o caso dos Tipos de Valor. Neste caso, ficaríamos com duas variáveis a apontar para os mesmos dados.

2.1. Tipos por Referência Nativos

Existem cerca de 2500 Tipos por Referência incluídos por defeito na .NET Framework, sendo que todos os tipos que não derivam de System.ValueType são Tipos por Referência. Os Tipos por Referência abaixo são os mais comuns, sendo que muitos outros Tipos por Referência derivam destes:

· System.Object. Este é o tipo mais genérico existente na .NET Framework. Qualquer tipo pode ser convertido para este Tipo.
· System.String. Este é um dos tipos mais utilizados na .NET Framework e serve para armazenar dados de texto.
· System.Text.StringBuilder. Armazena dados de texto de uma forma dinâmica.
· System.Array. É utilizado para arrays de dados sendo a classe base para todos os arrays.
· System.IO.Stream. Trata-se de um buffer para operações de Input/Output de ficheiros, dispositivos e rede. Trata-se de uma classe base abstracta.
· System.Exception. É utilizada para tratar excepções. Esta classe gere sobretudo excepções de Sistema, sendo que excepções específicas de tarefas em execução herdam deste tipo.

2.2. Strings e Construtores de Strings (StringBuilders)

Os Tipos são mais do que contentores de informação e fornecem meios para manipular os dados, através dos seus membros. O Tipo System.String disponibiliza um conjunto de membros para trabalhar com texto. O próximo pedaço de código mostra como se pode efectuar uma rápida operação de substituição de texto:

string s = “Texto que é para substituir!”;
Console.WriteLine(s);
s = s.Replace(“é para substituir”, “foi substituido!”);
Console.WriteLine(s);


Este pedaço de código iria produzir o seguinte resultado:

Texto que é para substituir!

Texto que foi substituido!


Contudo, objectos do tipo System.String são imutáveis, o que significa que qualquer mudança a uma string vai fazer com que o ambiente de execução crie uma nova string e abandone a anterior, de uma forma invisível para o programador.


string s = “Esta ”;
s += “operação vai “;
s += “criar 5 “;
s += “strings diferentes “;
s += “em memória!”


O código acima, utilizado várias vezes por muitos programadores com experiência em .NET, na realidade cria 5 strings diferentes em memória. Apenas a última string terá uma referência para os dados, sendo que as quatro anteriores serão descartadas no processo de Garbage Collection.
Esta operação é, portanto, menos performante que utilizar os métodos Concat, Join ou Format da Classe String ou ainda a Classe StringBuider.

A opção pela Classe StringBuilder é a mais eficiente e flexível, porque permite criar strings dinâmicas (mutáveis). O construtor de defeito da Classe cria um buffer de 16 bytes que se expande à medida que vai necessitando, embora seja possível especificar um tamanho mínimo e máximo:


System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append(“Esta ”);
sb.Append(“operação vai “);
sb.Append(“criar apenas “);
sb.Append(“uma string “);
sb.Append(“em memória!”);
string s = sb.ToString();
Console.WriteLine(s);


Outra funcionalidade importante da Classe String é que esta sobrepõe os seus operadores aos da Classe System.Object:

· Adição: +
· Igualdade(comparação): ==
· Desigualdade (comparação): !=
· Atribuição de valor: =

2.3. Criar e Ordenar Arrays

Um array não é mais que uma sequência de apontadores para valores de dados armazenados em memória, ou seja, um conjunto de apontadores de referência para variáveis do mesmo tipo.
Os arrays são declarados utilizando os nomes dos tipos seguidos de parênteses rectos ([]) como parte da declaração da variável. O próximo bloco de código, por exemplo, cria um array e ordena-o:


int[] ar = {3, 4, 1, 2 };
Array.Sort(ar);
Console.WriteLine(“{0}, {1}, {2}, {3}, ar[0], ar[1], ar[2], ar[3]);


De notar que, em C#, quando se pretende aceder ao primeiro elemento do array, ou seja, ao elemento em primeiro lugar no índice do array, se deve utilizar o índice 0 (ar[0]). No caso acima, a declaração do array foi acompanhada pela definição dos valores que o array deveria conter, o que não é obrigatório. Aliás, a grande utilidade dos arrays reside exactamente no facto de se poder utilizar arrays para criar uma referência para dados que serão gerados ou calculados mais á frente, garantindo à partida (com a criação do array) que estas referências estão agrupadas.


int[] numeros = new numeros[3];
for (int i=0;i<3;i++) {
   numeros[i] = i+1;
}


2.4. Streams

As streams são utilizadas para ler e escrever para o disco e comunicar através de uma rede. Existem vários tipos de streams, nomeadamente:

· FileStream, para ler e escrever a partir de ficheiros.
· MemoryStream, para ler e escrever a partir da memória.
· StreamReader, para ler dados de uma stream.
· StreamWriter, para escrever dados para uma stream.

Estas streams derivam de System.IO.Stream, sendo que streams para aceder a recursos de rede podem ser encontradas no namespace System.Network.Sockets e streams de cifragem/decifragem encontram-se no namespace System.Security.Criptography.

As classes mais simples são o StreamReader e o StreamWriter, que nos permitem ler e escrever em ficheiros de texto. Pode-se passar o nome do ficheiro como parte do construtor, permitindo assim a abertura do ficheiro com apenas uma linha de código. De relembrar que, após o processamento do ficheiro, deve ser executado o método Close para que o ficheiro não permaneça trancado. Por exemplo:


using System.IO;

StreamWriter sw = new StreamWriter(“texto.txt”);
sw.WriteLine(“Olá Mundo!”);
sw.Close();

StreamReader sr = new StreamReader(“texto.txt”);
Console.WriteLine(sr.ReadToEnd());
sr.Close();


Mais adiante, num artigo sobre Input e Output, falaremos mais em detalhe destas operações.

2.5. Utilização de Excepções

Excepções são eventos inesperados que interrompem o decurso normal de execução de um programa. Por exemplo, se um programa está a ler um ficheiro a partir de uma localização na rede e o utilizador desliga o cabo de rede ou pura e simplesmente a rede falha por algum motivo, o ambiente de execução irá lançar uma excepção. Isto faz sentido na perspectiva em que não existem condições para que o programa continue a ler o ficheiro a partir da rede.

Contudo, as excepções não devem causar que o programa deixe de funcionar completamente. Ao invés, as excepções devem ser previstas e a sua captura deve ser planeada, para que se possa definir convenientemente como o programa deve agir em resposta a essa excepção.


using System.IO;

try {
   StreamWriter sw = new StreamWriter(“texto.txt”);
   sw.WriteLine(“Olá Mundo!”);
   sw.Close();
} catch (Exception ex) {
   Console.WriteLine(ex.Message);
}


Neste exemplo, o ambiente de execução iria tentar executar o código incluído no bloco Try{ } e se encontrasse uma excepção, iria executar o código incluído no bloco Catch{ }. Caso não encontrasse nenhuma excepção, o programa iria continuar na linha seguinte ao bloco Catch{ }, ignorando-o.

Qualquer que fosse a excepção gerada, ela iria ser capturada pelo bloco Catch{ }, mas é possível (e desejável) que se utilize as várias classes de excepção existentes na .NET Framework para permitir uma arquitectura de manuseamento de excepções mais eficiente e performante. É ainda possível que cada programador defina as suas próprias classes de manuseamento de excepções para maior detalhe na identificação e tratamento das mesmas, criando classes derivadas de System.ApplicationException ou de System.Exception. No entanto, é considerado boa prática efectuar a definição de excepções próprias derivadas de System.Exception, uma vez que se acabou por verificar que derivar as excepções a partir de System.ApplicationException não trazia valor acrescentado.

Para além dos blocos Try{ } e Catch{ }, o manuseamento de excepções também suporta um terceiro bloco, denominado de bloco Finally{ }, que executa após a execução do bloco Try{ } e de qualquer Catch{ } que tenha sido executado, independentemente de ter ou não sido encontrada alguma excepção. Assim, a utilização do bloco Finally{ } faz sentido para executar tarefas de limpeza ou qualquer outra tarefa que tenha de ser executada sempre, haja ou não excepções a tratar.


using System.IO;

StreamWriter sw = new StreamWriter(“texto.txt”);
try {
   sw.WriteLine(“Olá Mundo!”);
} catch (Exception ex) {
   Console.WriteLine(ex.Message);
} finally {
   sw.Close();
}


De notar que a declaração do objecto StreamWriter foi movida para fora do bloco Try{ } no exemplo anterior, porque o bloco Finally{ } não consegue aceder a variáveis declaradas no âmbito do bloco Try{ }. Isto faz todo o sentido, porque, dependendo do local onde as eventuais excepções ocorrem, as declarações de variáveis dentro do bloco Try{ } podem ainda não ter sido executadas.

É fortemente recomendado que todo o código, com excepção da declaração de variáveis, deva ser executado entre blocos Try{ }/Catch{ }/Finally{ } para melhorar a experiência do Utilizador e também para melhorar a depuração (debugging) das aplicações.

De seguida (no próximo artigo) irei falar-vos de Classes.

Tuesday, April 10, 2007

MAIS UMA INTRO À .NET FRAMEWORK 2.0 - Parte I

Introdução

Este artigo foi desenvolvido a partir do meu próprio estudo para o exame de certificação Microsoft Technology Specialist e trata-se, sobretudo, de expor pelas minhas palavras aquilo que assimilei durante este processo.
Tem também referências a trabalhos de outros autores que eu acho que acrescentam valor ao conteúdo aqui exposto, sendo que este artigo é essencialmente baseado no MCTS Self-Paced Training Kit para o exame 70-536 – Microsoft .NET Framework 2.0 —Application Development Foundation.
Um outro objectivo deste artigo é também ajudar todos os que tiverem interesse em efectuar o referido exame mas, por qualquer motivo, tenham decidido não adquirir o livro que está na base deste artigo.

Convenções

Texto em Itálico – Conteúdo em língua estrangeira
Texto em Courier New (10pt) – Conteúdo que representa pedaços de código-fonte

Capítulo 1 - Aspectos Fundamentais da .NET Framework 2.0

A .NET Framework 2.0, resumidamente, é uma plataforma de desenvolvimento de aplicações criada pela Microsoft com um foco muito grande no desenvolvimento de aplicações de um modo rápido (RAD) e com o objectivo de proporcionar uma plataforma independente de linguagem e de plataforma.

Neste capítulo irei introduzir alguns dos aspectos mais básicos da .NET Framework , como Tipos, Atributos e Encapsulação, focando também algumas novidades da .NET Framework 2.0 relativamente à 1.1 e 1.0, como Genéricos, entre outros.

Eu assumo que quem está a ler este artigo tem pelo menos 2 anos de experiência de programação em .NET, independentemente da plataforma.
Todos os exemplos de código neste artigo serão em C#, excepto quando for necessário referir alguma especificidade da Framework que seja diferente em VB.

1. Tipos de Valor

Um dos erros mais comuns que a maioria dos programadores cometem quando aprendem (e programam com) a .NET Framework tem a ver com a confusão que se faz, por um lado, entre Tipos de Valor e Tipos de Referência e, por outro, com a passagem de valores por Referência ou por Valor.

Tipos de Valor são os tipos mais simples que existem na .NET Framework e incluem todos os tipos de dados numéricos, booleanos, Char e Date, estruturas (mesmo que os seus membros sejam Tipos por Referência) e enumerados (uma vez que os seus tipos intrínsecos são sempre Byte, Short, Integer e/ou Long).

1.1. Tipos de Valor Nativos

Estes tipos são os tipos base disponibilizados pela .NET Framework e são a base de construção de outros tipos. Derivam da Classe System.ValueType que, por sua vez, deriva de System.Object.

A principal característica destes tipos tem a ver com o facto de o seu valor estar armazenado na Stack em vez do Heap, ou seja, estão acessíveis mais rapidamente pelo código em excução.

Existem mais de 300 tipos de valor na .NET Framework, mas de entre os mais comuns destacam-se os seguintes:

· Byte – 1 Byte. Armazena valores de bytes entre 0 e 255, sem sinal.
· Sbyte - 1 Byte. Armazena valores de bytes entre -128 e 127.
· Int16 (ou apenas short) – 2 Bytes. Armazena valores inteiros entre – 32768 e 32767.
· Int32 (ou apenas int) -4 Bytes. Armazena valores inteiros entre – 2147483648 e 2147483647.
· Uint32 (uint em C#) - 4 Bytes. Armazena valores inteiros entre 0 e 4294967295, sem sinal.
· Int64 (ou apenas long) -8 Bytes. Armazena valores inteiros entre – 9223372036854776808 e 9223372036854776807.
· Single (float em C#) - 4 Bytes. Armazena valores de vírgula flutuante entre – 3.402823E+38 e 3.402823E+38.
· Double - 8 Bytes. Armazena valores de vírgula flutuante entre – 1.79769313486232E+308 e 1.79769313486232E+308.
· Decimal - 16 Bytes. Armazena valores de vírgula flutuante entre – 79228162514264337593543950335 e 79228162514264337593543950335.
· Char - 2 Bytes. Armazena um único caracter Unicode.
· Boolean (ou apenas bool) - 4 Bytes. Armazena valores verdadeiro/falso.
· DateTime (ou apenas date) - 8 Bytes. Armazena momentos no tempo entre 1/1/0001 e 31/12/9999.

Em tempo de execução, o desempenho dos tipos Int32 (int e uint) são optimizados, por isso é aconselhado o seu uso para contadores e outras variáveis que são utilizadas frequentemente. No caso de operações que necessitem de Tipos de vírgula flutuante, é recomendado o uso de Double, uma vez que o seu desempenho é optimizado ao nível do Hardware.

1.2. Decalaração de Tipos de Valor

Para usar um Tipo de Valor é necessário declarar uma variável do tipo desejado. Estes Tipos possuem um construtor implícito, ou seja, ao declarar uma variável deste tipo estamos a instanciá-la imediatamente. Contudo, é recomendado que este tipo de variáveis seja inicializada logo na sua declaração:


int x = 0;


Uma das funcionalidades novas na .NET Framework 2.0 são os Tipos de Valor Nulos, ou seja, agora é possível criar variáveis de Tipos de Valor que aceitam valores nulos (por exemplo booleanos nulos). Para declarar variáveis de Tipos de Valor que aceitem valores nulos deve usar-se a seguinte sintaxe:


Nullable <int>x = null;
Ou ainda:
int? x = null;


Desta forma evitamos o (mais que) comum erro “Cannot convert null to 'int' because it is a value type”. Ao declararmos uma variável como Nullable torna disponíveis os membros HasValue e Value, que tornam possível determinar se esta variável tem um valor nulo e, no caso de não ter, ler o seu valor.


if (x.HasValue) // Boa, o valor de x não é nulo!
Console.WriteLine(“O valor de x é {0}”), x.Value);
else // Ooops, o valor de x é nulo!
Console.WriteLine(“x é nulo!”);


1.3. Tipos de Valor Definidos pelo Utilizador (Estruturas)

Estruturas (structs) são Tipos de Valor definidos pelo programador, sendo um composto de outros Tipos de Valor que facilitam a utilização de dados inter-relacionados. Tal como os outros Tipos de Valor, são armazenados na Stack, o que os torna bastante eficientes (às vezes mais eficientes que classes). Para se definir estruturas deve fazer-se algo semelhante ao seguinte:


struct SerHumano
{
public float altura;
public float peso;
public short idade;

public SerHumano (float _altura, float _peso, short _idade)
{
altura = _altura;
peso = _peso;
idade = _idade;
}

public override string ToString()
{
return “Este Ser Humano mede “ + altura + " m, pesa " + peso + " kg e tem " + idade + “ anos.”;
}
}


De notar ainda um pormenor. As estruturas não podem ter um Construtor de defeito (sem parâmetros). O motivo tem a ver com o facto de, para Tipos de Valor, o compilador não gera um construtor de defeito nem uma chamada ao construtor de defeito na instanciação. Ou seja, mesmo que definissemos um construtor de defeito, ele não seria chamado. Para evitar isso, o compilador de C# não permite a sua definição.

Contudo, se utilizarmos o código abaixo, não existe uma chamada a um construtor sem parâmetros. Mas o compilador também não rejeita esta instrução. O que acontece é que os campos da Estrutura são todos inicializados a nulo ou zero, através da utilização do Opcode .InitObj do IL (Intermediate Language).


SerHumano k = new SerHumano();


As estruturas devem possuir as seguintes características:
· Representar logicamente um valor único
· Ter um tamanho de instância inferior a 16 Bytes
· Não irão ser alteradas após a sua criação
· Não irão ser convertidas para um Tipo de Referência

1.4. Enumerados

Enumerados (Enums) são tipos especiais de Tipos de Valor. São símbolos relacionados com valores fixos e imutáveis. Ou seja, pode dizer-se que se tratam de constantes com nomes mais facilmente acessíveis.

O seu objectivo é simplificar a programação através da atribuição de nomes a valores fixos de uma lista de possíveis valores. Assim, em vez de se referenciar o seu valor, referencia-se o seu nome, tornando o código mais legível e bem estruturado.


public enum Titulos : int { Sr, Sra, Dr, Dra, Prof };
...
Titulos t = Titulos.Prof;
string name = “José Pedras”;
Console.WriteLine(“{0}. {1}”, t, name);


De seguida (no próximo artigo) irei falar-vos de Tipos por Referência.