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.