5
Inclui execução concorrente, acesso à rede, e LINQ
DA INFORMÁTICA
Martins
ED.)
XA MÁGICA - O LINUX EM
e TECNOLOGIAS INTERACTIVAS
LIVRARIAS: LISBOA: Av. Praia da Vitória, M-1000-247 LISBOA-Tel.: 21 354 14 18, e-mail: livrarialx@lidel.pt
PORTO: R. Damiao de Gois, 452 - 4050-224 PORTO - Tel.: 22 557 35 10, e-mail: delporto@lidel.pt
Este pictograma merece uma explicação. O seu propósito é alertar o leitor para a ameaça que representa
para o futuro da escrita, nomeadamente na área da edição técnica e universitária, o desenvolvimento massivo
da fotocópia.
O Código do Direito de Autor estabelece que é crime punido por lei, a fotocópia sem autorização dos
proprietários do copyrlght No entanto, esta prática generalizou-se sobretudo no ensino superior, provocando
uma queda substancial na compra de livros técnicos. Assim, num país em que a literatura técnica é tão
escassa, os autores não sentem motivação para criar obras inéditas e fazê-las publicar, ficando os leitores
impossibilitados de ter bibliografia em português.
Lembramos portanto, que é expressamente proibida a reprodução, no iodo ou em parte, da presente obra
sem autorização da editora.
PARA o MEU AMIGO HERNÂNI
AGRADECIMENTOS
Agora que a escrita da terceira versão deste livro está concluída, chegou a hora de deixar
algumas palavras de reconhecimento aos que tornaram este projecto possível.
Antes de mais, gostaria de agradecer ao meu amigo Hernâni Pedroso, por me ter
inspirado e por ter aceite embarcar neste desafio comigo. Infelizmente, devido ao seu
trágico falecimento, a realização deste projecto conjunto foi muito mais solitária do que
alguém alguma vez poderia adivinhar. Os meus pensamentos e sentimentos estão contigo,
onde quer que te encontres.
Mais recentemente, gostaria de agradecer ao Ricardo Figueira por ter aceitado tornar-se
co-autor do livro, contribuindo com o texto relativo às funcionalidades da linguagem
C# 3.0 e da plataforma .NET 3.5. A sua ajuda foi essencial para que o livro se mantivesse
actualizado face às evoluções recentes do mundo .NET.
Uma pessoa incontornável neste projecto foi o Eng. Manuel Costa, da Microsoft, por me
ter levado ao primeiro contacto com a plataforma .NET, e por todo o apoio que me deu na
realização da primeira edição. Gostaria de agradecer ao Eng. Vitor Santos, também da
Microsoft, por me incentivar a escrever a segunda edição e por toda a documentação e
software que, de forma tão continuada, me tem vindo a fornecer ao longo dos anos.
Aos meus pais, deixo a minha enorme gratidão por me darem a educação que me deram,
por me deixarem voar e prosseguir os meus sonhos. É devido a eles que me tornei no
homem que sou hoje. É devido a eles que hoje vivo os meus sonhos.
Finalmente, todo o meu carinho vai para a Joana, que comigo caminha o dia-a-dia, com
quem. vivo todas as minhas aventuras e sonhos, em quem encontrei uma alma gémea.
Paulo Marques
Coimbra, 25 de Junho de 2008
SOBREOLJVRO
A primeira vez que encontrei o Paulo Marques, ele nunca tinha programado em C#.
Pouco tempo depois, encontrei-o novamente e ele já estava a discutir com um dos
membros da equipa de desenvolvimento do compilador de C#, algumas áreas onde se
poderia aumentar o desempenho do mesmo. Este episódio mostra bem o entusiasmo, a
energia e a capacidade técnica do Paulo, que levaram à criação deste livro. Este é um
livro que vai muito além de uma simples descrição das características da linguagem C#,
para dar indicações práticas muito precisas sobre quais as formas mais correctas de
construir bons programas nesta linguagem e quais as construções que devem ser evitadas.
Além disso, o livro não se limita à linguagem C#, também aborda as principais
bibliotecas da plataforma .NET, pelo que estou certo de que será extremamente útil a
qualquer programador que desenvolva software para esta nova plataforma.
Manuel Costa
Microsoft
l-INTRODUÇÃO l
1.1 A plataforma .NET 2
1.2 Sobre este livro... 4
2 - ELEMENTOS BÁSICOS 7
2.1 Primeiro Programa 7
2.2 Um exemplo completo 9
2.3 Tipos de dados 12
2.3.1 Tipos elementares 12
2.3.1.1 Tipos numéricos 12
2.3.1.2 Valores literais 13
2.3.1.3 Conversões entre tipos numéricos... 14
2.3.2 O tipo lógico. ....16
2.3.3 Caracteres e cadeias de caracteres 16
2.3.3.1 Cadeias de carcteres 17
2.4 Variáveis 19
2.5 Constantes 19
2.6 Expressões e operadores 20
2.7 Controlo de fluxo 24
2.7.1 Expressão if-else....... ........24
2.7.2 Expressão switch. 26
2.7.3 Expressões while e do-while... ....27
2.7.4 Expressão for ..28
2.7.5 Expressão foreach 29
2.7.6 Quebra de ciclos ....30
2.8 Tabelas 33
2.8.1 Tabelas simples 33
2.8.2 Tabelas rnultidimensionais.... .............38
2.8.3 Tabelas dentro de tabelas 40
PARTE I - A LINGUAGEM C#
3 - CONCEITOS DE ORIENTAÇÃO AOS OBJECTOS 45
3.1 Conceitos básicos 47
3.2 Encapsulamento de informação 47
3.3 Composição e herança 48
3.4 Polimorfismo 52
4 - PROGRAMAÇÃO ORIENTADA AOS OBJECTOS 57
4.1 O sistema de tipos do CLR 57
4.1.1 Referências ..57
© FCA - Editora de Informática xm
C#3.5 ^=^=^=^=^==^==^=^=^^==^==^^^
PARTE II - .NETESSENCIAL
8 - CLASSES BASE 249
8.1 A classe System.Object 249
8.1.1 Método ToStringO ........250
8.1.2 Comparação de objectos.......................................... 251
8.1.2.1 Método GetHashCodeQ 253
8.1.3 Método MemberwiseCloneQ 257
8.2 Cadeias de caracteres 259
8.2.1 Leitura da consola .............................................................260
8.2.2 Conversões de valores ....................................................................261
8.2.3 A classe System.String 262
8.2.4 A classe StringBuilder 265
8.2.5 Formatação de cadeias de caracteres... 266
8.2.5.1 Formatações definidas pelo programador 268
8.2.6 Expressões regulares 271
8.3 Colecções 276
8.3.1 A interface primordial: ICollection ...........276
8.3.2 Colecção List e ArrayList 278
8.3.3 Colecção LinkedList...... ..............280
8.3.4 Colecção BitArray 282
8.3.5 Colecção Dictionary e Hashtable 285
8.3.6 Colecção Hashset 288
8.3.7 Colecção SortedDictíonary .......290
8.3.8 Colecção SortedList ........................294
8.3.9 Colecção Queue. 297
8.3.10 Colecção Stack 298
8.3.11 Resumo das colecções........... 299
8.4 Ficheiros estreams 300
8.4.1 Gestão do sistema de ficheiros .............301
8.4.2 Leitura e escrita de ficheiros 307
8.4.2.1 Hierarquia à&streams 308
8.4.2.2 Classe Filestream 310
8.4.2.3 Ficheiros de texto......... ...............311
8.4.2.4 Ficheiros binários ...............................315
8.4.3 Serialização de objectos .....................................318
8.4.3.1 Serialização em formato binário 319
8.4.3.2 Serialização em formato XML................ 326
O leitor, ao ter pegado neste livro, ou talvez quando ouviu falar pela primeira vez em C#
e .NET, certamente que se perguntou: "Porquê aprender uma nova linguagem de
programação? Porquê aprender uma nova plataforma de desenvolvimento?". Pelo menos
nós fizemo-lo. No fundo, quem programa para ambientes Windows já possui uma
plataforma de desenvolvimento com imensas interfaces de programação que pode utilizar.
Ao mesmo tempo, quem programa na plataforma Java, também já dispõe de ambiente de
execução e de uma linguagem muito poderosa para criar aplicações. O que acontece é que
com a plataforma .NET e a linguagem C#, a Microsoft pretende dar um salto em frente na
forma como as aplicações são actualmente desenvolvidas. Neste momento a plataforma
.NET corre em sistemas operativos da família Windows, Lima, FreeBSD, MacOS, entre
outros.
O C# é uma linguagem com diversos objectivos em vista. Pretende ser mais simples do
que o C-H- e, ao mesmo tempo, poderosa, em termos das suas características. De facto, ao
criar o C#, os seus autores tinham como objectivo criar algo que combinasse a elevada
produtividade do Visual Basic com o poder de uma linguagem como o C++. Eis as
principais características desta nova linguagem, que achamos que são dignas de referência:
Uma nova linguagem, por muito poderosa que seja, não é verdadeiramente útil se não
tiver um grande número de bibliotecas de funções directamente disponíveis. O C# em si
não possui bibliotecas. No entanto, os programas escritos nesta linguagem executam
sobre a plataforma .NET, que possui um vasto conjunto de bibliotecas, assim como outras
funcionalidades que permitem uma grande versatilidade em C#.
l. Í A PLATAFORMA .NET
O que é então a plataforma .NET? Trata-se da infra-estrutura básica sobre a qual as
aplicações correm. Esta infra-estrutura é constituída por diversas partes, tal como é
ilustrado na figura 1.1.
Todas as aplicações escritas para a .NET correm dentro de uma máquina virtual chamada
Common Language Rimtime (CLR). O código que se encontra a executar aqui dentro
chama-se managed code e beneficia de várias características como:
alto nível para uma linguagem intermédia chamada MSIL (Microsoft Intennediate
Langiiagé). O CLR possui um compilador just-in~time que se encarrega de traduzir
o código intermédio para código nativo do processador, antes de o executar;
Windows/COM-f
11
Figura 1.1 —Arquitectura da plataforma .NET
O CLR encontra-se a executar por cima do sistema operativo, utilizando os seus serviços,
assim como os serviços COM+ disponibilizados pelo mesmo.
Um ponto interessante é que o .NET foi pensado, não apenas para aplicações que sejam
programadas em C#, mas também noutras linguagens. Certamente que o leitor se está a
questionar como é que tal é possível. NQfram&vork .NET, as linguagens de alto nível são
compiladas para uma linguagem intermédia chamada MSIL. E esse código que é
Este livro concentra-se na linguagem C# e nas classes base da plataforma .NET (a BCL -
Base Class Library). Pensamos que é preferível ter um livro que cubra, de forma
profunda, a linguagem em si e os fundamentos da plataforma, do que um livro que, por
tentar ser demasiado abrangente, fique demasiado esparso. As interfaces de programação
correspondentes a ADO.NET, ASP.NET e a Windows Forms são demasiado extensas e
merecem um tratamento mais profundo do que seria possível conseguir num livro desta
dimensão. Assim, os principais tópicos cobertos neste livro são:
" Hoje em dia, um dos principais problemas de programar em Java, pelo menos para aplicações servidor, é
que se está a utilizar uma infra-estrutura que não foi pensada de raiz para isso. Assim, em Java, não existe
um noção forte de processo, não é possível de forma directa, colocar código a correr baseado em quem o
corre e, mesmo quando isso acontece, não é de forma transparente.
4 © FCA - Editora de Informática
INTRODUÇÃO
Para ler este livro, não é necessário saber uma linguagem orientada aos objectos. No
entanto, assumimos que o leitor é um programador com alguma experiência numa outra
linguagem estruturada (ou minimamente estruturada), como C/C-H-, Java, Visual Basic
ou Delphi. O ritmo do livro é rápido, tendo como público-alvo, programadores de nível
intermédio ou experientes.
2. l PRIMEIRO PROGRAMA
Uma das melhores formas de começar a aprender uma nova linguagem é examinar e
executar exemplos escritos na mesma. Um dos programas mais simples que pode ser
escrito em qualquer linguagem é um programa cuja funcionalidade consiste apenas em
enviar uma pequena mensagem para o ecrã. A listagem seguinte ilustra esse programa,
escrito em C#.
using System;
class Primei reprograma
stafic void MainQ
{
Console.WriteLine("0 meu primeiro programa em C#!");
}
1 Durante a maior parte deste livro, iremos utilizar o compilador de linha de comandos para ilustrar os
exemplos, por forma a que o leitor tenha uma maior noção de todos os pormenores que estão a ocorrer no
sistema. No entanto, tipicamente, os programadores de C# desenvolverão aplicações em Visual Siiidio.NET
(VS.NET), o ambiente integrado de desenvolvimento da Microsoft.
© FCA - Editora de Informática 7
C#3.5
O programa começa com a expressão uslng System;. O que esta linha faz é importar
para o "espaço de nomes" (namespace) corrente todas as classes da biblioteca system. As
várias bibliotecas de programação disponíveis no sistema estão organizadas em espaços
de nomes, sendo necessário sempre que utilizamos uma certa funcionalidade de uma
biblioteca, resolvê-la no espaço de nomes presente. No caso particular deste programa,
iremos utilizar uma classe" que representa a consola (ecrã e teclado). Essa classe chama-
-se console, pertencendo ao espaço de nomes System. Ao escrevermos uslng System;
estamos a tomar visível no programa, todos os elementos presentes em system,
nomeadamente a classe consol e.
Um programa começa sempre a executar por uma função denominada Mal n C). Ao
programar, as várias sequências de instruções são organizadas em entidades chamadas
métodos (ou funções), estando essas funções dentro de classes. O método Mal n Q é o
nosso método principal (e único), onde o programa começa a executar. Os métodos
existem exclusivamente dentro de entidades chamadas classes, que encapsulam uma
determinada funcionalidade bem definida. Na verdade, uma classe é um conjunto de
dados e de métodos que actuam sobre esses dados. Um ponto importante que o leitor
poderá notar é que sempre que é definida uma classe, ou um método, a forma de indicar o
início e o fim da entidade em causa são as chavetas. Assim, sempre que é iniciado um
bloco, abrem-se chavetas { e sempre que termina um bloco fecham-se chavetas }.
Ao definir o método Mal n C), são indicados dois modificadores: void e static.
O modificador voi d indica que o método não irá devolver nenhum valor. Quando um
método é chamado, é como se estivesse a calcular o valor de uma função. Caso o método
não esteja de facto a realizar um cálculo, mas simplesmente um conjunto de operações,
então, o método não terá de resultar num valor, logo, não terá de retornar nada. É o caso
do nosso programa e em particular do método Mal n Q . O Mal n Q apenas imprime para o
ecrã as palavras "O meu primeiro programa em C#!", não leva parâmetros e não irá
retornar nenhum valor. Quanto ao modificador statl c, este será explicado mais tarde. O
ponto a saber é que Mal n C) deverá ter sempre o modificador statl c.
2 Mais à frente, iremos ver, cuidadosamente, o que é uma classe. Para já, convém ficar com a ideia de que se
trata de uma entidade que agrupa um certo conjunto de dados e funções.
8 © FCA - Editora de Informática
ELEMENTOS BÁSICOS
Uma questão importante é que, caso o programador não quisesse ter importado todos os
símbolos de system para o espaço de nomes corrente, seria ainda possível utilizar o
objecto console. Para isso, o programador deveria especificar o nome completo da
classe. No nosso caso em particular, isso seria feito da seguinte forma:
meQ
Isto eliminaria a necessidade da expressão usi ng System; neste programa, à custa de ter
de se colocar System em todas as linhas onde se utilizasse a classe Consol e. Ao longo
deste livro, iremos assumir que se faz sempre a importação do espaço de nomes System
(isto é, existe um using system; no início de todos os ficheiros).
" O programa começa sempre a executar no método Mai n C). Este método
deverá ser, regra geral, statl c voi d.
" A directiva using permite importar, para o espaço de nomes corrente, o
conjunto de símbolos definidos noutro espaço de nomes.
" É possível aceder a elementos de outro espaço de nomes sem utilizar a
directiva usi ng, bastando para isso, escrever o nome completo qualificado
do elemento a que se deseja aceder.
Vamos, então, ver um exemplo prático do que estamos a discutir. O programa seguinte
calcula as raízes quadradas dos múltiplos de 10, entre 10 e 100.
/*
* programa que calcula as raízes quadradas dos números entre
* 10 ^e 100, em intervalos de 10.
V
using System;
class RaizesQuad radas
{
static void MainQ
{
int numero;
numero = 10;
while (numero <= 100)
// Calcula o valor da raiz quadrada e
// envia-a para o ecrã
double raiz = Math.Sq Pt (numero) ;
Console. Writel_ine("A raiz de {0} é {!}", numero, raiz);
// Passa para o próximo número
numero = numero + 10;
}
ainda um outro tipo de comentários: os comentários de fim de linha. Sempre que numa
linha do código fonte surgem duas barras (isto é: //), o compilador ignorará tudo o que
surge após as mesmas, até ao fim da linha, considerando-o um comentário do
programador.
Em C#, é necessário declarar sempre uma variável antes de a utilizar. A forma geral de
declarar uma variável é:
'ti pòDaVà.ri ave]", hpmèpavãri áyeT £=.".vâlõrín_ici"a]ll" ZT_7_ _TT ~~..'".. - !."_" . ... .".:
Como se pode ver, é possível íhicializar uma variável assim que esta é declarada (por
exemplo: int numero = 10;). No entanto, isso não é obrigatório. Mas, uma variável tem
de ser sempre inicializada' antes de ser usada. Caso se tente utilizar uma variável antes de
esta estar inicializada, o compilador gera um erro. Por exemplo, no caso do nosso
programa, se não fosse feita a atribuição numero = 10 ; , o compilador iria gerar um erro,
pois a variável seria utilizada numa comparação, sern que esta já tivesse um valor
atribuído.
O bloco
'wrrile (numero <= 100) ....... " ............. " " - " ~•
Vejamos, agora, o que é executado dentro do ciclo. Em cada iteração (isto é, de cada vez
que o ciclo é executado), é criada uma nova variável de nome raiz, cujo tipo de dados é
double ("número real de precisão dupla"). Isto é, esta variável é capaz de armazenar
números reais. Ao escrever:
a variável raiz é criada e simultaneamente inicializada. O valor que ela irá conter será a
raiz quadrada da variável número. Para calcular a raiz quadrada, é utilizada a classe Math,
que encapsula diversas operações matemáticas vulgares, na qual é chamado o método
sqrtQ (Sqitare Roof). Este método recebe como parâmetro um número e retorna a raiz
do número que lhe foi passado.
A linha seguinte do código irá enviar para a consola o número corrente, assim como a
respectiva raiz quadrada:
'CP n sole V W ríte Ln.n ê CKTcsí?7 *d MTÍ Ql numero _,_ " r ai z) . .;_'
A última linha do ciclo é uma atribuição simples, que coloca na variável número o valor
corrente somado de 10.
Vamos, agora, examinar quais os tipos de dados elementares numéricos que existem na
linguagem. A tabela 2.1 resume os tipos existentes, assim como a principal informação
sobre os mesmos.
i NOME TOTAL DE BYTES \\A DE VALORES | SINAL?"] DESCRIÇÃO
f BYTE
i ! 0 a 255 | "Não ] Inteiro de 8 bits, sem sinal. i
SBYTE i Í , 11 -128 a 127 Sim j Í Inteiro de 8 bits, com sinal.
j SHORT Í 2 -32,768 a 32,767 |[ Sim J í Inteiro de 16 bits, com sinal.
j USHORT .2 1 0 a 65/535 - ] Não |i Inteiro de 16 bits, sem sinal.
r -2,147,483,648 Inteiro de 32 bits, com sinal; é
INT a Sim utilizado na maior parte dos
4
2,147,483,647 cálculos com inteiros.
(cont.)
NOME TOTAL DE BYTES | GAMA DE VALORES SINAL? DESCRIÇÃO
UINT T j 0 a 4,294,967,295 Não j Inteiro de 32 bfís, sem sinal.
í -9,223,372,036,854,775,808 Inteiro de 64 bfís, com sinal; é
LONG 8 |a Sim normalmente utilizado em
i 9,223,372,036,854,775,807 cálculos com inteiros grandes.
í Oa Inteiro de 64 bits, sem sinal.
ULONG 8 i 18,446,744,073,709,551,6' Não
; 15 í
j Í1.5X10"45 a ±3.4xl038 | Número real de 32 bits
FLOAT 4 Sim
t j (7 dígitos de precisão) (precisão simples).
!
( j Número real de 64 bits (precisão
! ±5.0xlO-324a±1.7xl0308
DOUBLE 8 Sim dupla); é normalmente utilizado
i (15-16 dígitos de precisão)
j em cálculos com números reais.
r"
1 Número real de 128 bits, que
i l.OxlO*28 a 7.9X1028
j utiliza internamente base 10; é
DECIMAL ! 16 i (aprox.) Sim
tipicamente utilizado em cálculos
! (28-29 dígitos de precisão)
i
i monetários.
No entanto, pode dar-se o caso de um literal poder corresponder a vários tipos de dados.
Por exemplo, ao escrever 10, este valor tanto pode representar um byte, um Int, um
uint ou outros. Sempre que isto acontece, o compilador escolhe o tipo de dados do valor,
seguindo a seguinte ordem: int-Hnnt-yiong-Hilong. No entanto, é possível especificar
directamente qual a interpretação que se quer que o literal tenha. Para isso, acrescenta-se
um dos seguintes sufixos ao número: u (de unsigned), ou 1 (de long), ou mesmo ambos -
ul (de unsigned long).
íuint ~ x ~ = I0LT;
long y = 121;
Ji _=_. 423 ul;_
A mesma discussão aplica-se aos valores reais (f l oat, double e decimal). Por omissão,
os literais de vírgula flutuante são doubl e. Qualquer número que tenha um ponto decimal
é visto como sendo um número real. Também é possível especificar um número em
formato científico.
idòubTé valor ""="10". 3; //'Um literal* real , simples" " "
yalor2 ^^I._3el5_;_ _ / / _ .Utilização dpi_ formato _ cientifico
No entanto, se uma variável for declarada corno f l oat e à mesma for atribuído um literal
real, simples, isso resulta num erro de compilação.
Tlõlírj^l^
Tal deve-se ao facto de se estar implicitamente a tentar converter um valor double em
f l oat. Uma vez que um f l oat não consegue representar todos os valores que um doubl e
consegue, poderia existir perda de precisão, pelo que o compilador gera um erro. Regra
geral, deve-se utilizar sempre doubl e para cálculos com números reais.
Os sufixos associados aos valores reais são: f (à&floaf)t d (de double} e m (de decimal}.
ífTõãt:" " vãTó?r^~2~orBf; ...... ~ ""'" " ' j
ldecjmal_val or2 . =..23543, 453543 nu____________________________._ ._. _.....!
Sempre que é necessário converter um tipo numérico num outro para realizar uma
operação, esta conversão é automaticamente realizada pelo compilador, desde que não
exista o risco de o valor final não ser representável na variável em causa, nem exista a
possibilidade de perda de precisão no cálculo:
Neste caso, o compilador irá fazer uma conversão implícita da variável a em doubl e para
conseguir realizar a soma. No entanto, sempre que não for possível realizar uma
Vamos, agora, supor que o programador sabe que o resultado irá ser sempre um inteiro e
que este inteiro será relativamente pequeno, sendo representável sempre numa variável do
tipo i nt. Neste caso, o programador poderá forçar a conversão, fazendo o que se chama
um cast:
IlxpJiclfa""".! ._"..". ~. '_., ~
Sempre que existe uma conversão explícita, no caso de valores inteiros, esta é feita por
truncatura. Por exemplo, o seguinte código resulta em que na variável destino fique o
valor 3.
;f"k>at orn-gem. = 3.764f";
int _destlnp..= ..Cl.nt) orisem; ._ //_ "destinp"_fica..çpm_p_ valor 3
~ Os tipos de dados numéricos mais utilizados são i nt, para valores inteiros,
ARETER e doubl e, para valores reais.
~ Os literais inteiros podem ser especificados em decimal ou hexadecimal,
Tipos de dados
numéricos neste caso começando por Ox.
~ Por omissão, um literal inteiro é considerado int. Existem sufixos que
permitem, explicitamente, indicar outros tipos de literais (l, u e ul).
" Por omissão, um literal real é considerado um double. É possível
escrevê-lo como um número com um ponto decimal ou em formato
científico. Existem sufixos que permitem, explicitamente, indicar outros
tipos de literais (f e m).
" Sempre que é necessário converter o tipo de uma variável num tipo de
dados mais abrangente, isso é feito implicitamente pelo compilador.
" Caso exista a hipótese de perda de precisão ou de o valor não ser
representável numa variável, é gerado um erro de compilação.
~ O programador pode sempre fazer uma conversão explícita para forçar a
conversão de um valor num certo tipo de dados.
~ As conversões explícitas são feitas por truncatura de valores.
i"f (fimDoprograma)
Console.WriteLine("Adeus, foi um prazer!");
Neste excerto, existe uma variável flmooprograma que é utilizada para controlar se uma
determinada frase é escrita ou não no ecrã. A expressão i f permite executar
condicionalmente um bloco de código, baseando-se numa expressão lógica (como seja uma
variável do tipo bool) colocada entre parêntesis.
Em C#a os caracteres são representados pelo tipo char (abreviatura de character). Este
.tipo de dados guarda os caracteres (letras, algarismos e símbolos) no formato Unicode,
que permite representar caracteres em qualquer língua. A tabela 2.3 resume a informação
sobre char.
3 A principal razão disto é o facto de um byte ser a unidade mínima endereçável pela maioria dos
processadores correntes. É certo que se poderia colocar oito variáveis destas num byie, mas então seriam
precisas várias operações, a nível do processador, para realizar uma operação sobre uma variável lógica, o
que seria muito dispendioso em termos de tempo de processamento.
16 © FCA - Editora de Informática
ELEMENTOS BÁSICOS
Um literal do tipo carácter é representado, entre plicas, por uma letra, uma sequência
Unicode (que começa por \u) ou por urna sequência de escape4. Eis alguns exemplos:
ichar" ~//"um" carácter" simples
T= '\U00411; // Um carácter representado em Unicode
"; _ // !Ma sequência de escape
É de notar que as sequências de escape podem ser utilizadas tanto em caracteres simples,
como no meio de cadeias de caracteres mais complexas. Assim, por exemplo, a instrução
consol e. wn" teLi ne("Antóni o: \tl2.3\naose: \tl4.3") resultaria na impressão de:
Àntohic;: ir. 5
;José: v 14.3
Neste caso \ introduziu uma tabulação horizontal e \ obrigou a uma mudança de linha.
4 Uma sequência de escape consiste num conjunto de caracteres que irá ser interpretado como um símbolo
especial, sendo esta sempre iniciada com urna barra invertida; \
© FCA - Editora de Informática 17
C#3.5
Uma string pode ser criada directamente em C#, colocando-a entre aspas:
stclngl7perguhta = "Qual "ò nome _dg pnmen rp rei de..Portuga].? ""j".' . . . ' . " _ .
Qualquer sequência de escape embutida na cadeia de caracteres é interpretada. A fim de
facilitar a escrita de cadeias que contêm barras invertidas (\ e outros caracteres
estranhos, também é possível especificar directamente strings que não devem ser
interpretadas. Isso é conseguido utilizando o símbolo @:
"string ~f rasei = @"\/"~c# Rui es \/"; ' " •
string frase2 = "\\/\# Rui es \\/"; „
•if (frasel == frase2} // Testa a igualdade das frases
, Console.WriteLine("Iguais"); // ... são iguais!
else
Consple..writeLineC"Diferentes"); . . . . . . . . . . .... .,..._ :
string || 20 ou mais |'• Tipo que permite representar uma cadeia de caracteres.
2.4 VARIÁVEIS
Quando se está a escrever um programa, é possível declarar uma variável em qualquer
parte do código. No entanto, não se pode declarar uma variável com o mesmo nome de
uma variável anteriormente definida:
int x?= 10;
if.fy-20)
i nt x =* 50 y; / / E r r o : o x já foi definido anteriorméfite-
Um outro ponto importante é que é obrigatório inicializar sempre uma variável antes de,a
utilizar. Caso isso não aconteça, o compilador também irá gerar um erro:
i "i nt- x,° : ''"'' '"" • ' ' . - ' ' ; '
if (x!"^ ÍQ) // Erro: x não foi inicializado -- "••"• :
• :GonspTe.WriteLineC"p_yalpr_de x. é dez!"); - . '
No entanto, mais tarde, iremos ver que para as variáveis associadas a uma classe (isto é,
as variáveis que não são variáveis locais a um método) existe um mecanismo de
inicialização com valores por omissão.
2.5 CONSTANTES
Existem também situações em que não se necessita de uma verdadeira variável mas sim
de um nome simbólico para um certo valor constante. Para isso, utiliza-se a palavra-chave
const.
PI" = "3 .1415926535; 77 Define "a* constante PI
Uma constante tem de ser sempre inicializada na altura da sua declaração e não é possível
mudar o seu valor.
Em C#, existem operadores unários, binários e, até mesmo, um operador ternário. Eis um
exemplo dos dois primeiros:
; k~~ -x; "~~ " ~ ~ //"õ operador - é neste "caso uriToperãdòr" unãno
iy.._= X.+..Y.;. . ._.//..O operador + é n_est_e_ caso urrf operador bi.nárj_q i _ -
Sempre que uma expressão é calculada, existe uma ordem de avaliação que é controlada
pela precedência dos operadores. A precedência é um género de "força relativa" que faz
com que certas expressões sejam avaliadas antes de outras. Vejamos o seguinte exemplo:
!£".= . ?l'~+."_3r *"T; " "."" "/ZJiCFIça com o y~alór'_T7 _ ' ~" 'f ".V_".~"T".'!1""7~V."~"7 ~~""~.~Í
Neste caso, a expressão é avaliada como sendo 2+C3*5) e não (2+3)*5, pois a
precedência do operador * é superior à correspondente do operador -K Sempre que for
necessário alterar a ordem de avaliação das expressões, utiliza-se parêntesis, que têm o
valor de precedência mais elevado:
.k _-. .C.2 +JÕ. * J>;._ ._. _ . . . _ _ / / k fira com o.vajor 25 __ _._
X-H-
j Post-increment, utiliza o valor da variável, incrementando-a de
| seguida.
[ Post-decrement, utiliza o valor da variável, decrementando-a
! de seguida.
(cont.)
i OPERAÇÕES
x & y
x A y 1
and B binário, realiza um e (and) entre os bits de duas
expressões.
xor binário, realiza um ou exclusivo (exclusive or} sobre os bits\e duas expre
LÓGICAS
BINÁRIAS
5 No caso das operações binárias and, or e xor, nós optamos por manter o nome inglês. Pensamos, desta
forma, tomar mais clara a distinção entre operações binárias de valores e operações lógicas sobre
expressões.
© FCA - Editora de Informática 21
C#3.5
(cont.)
- ,
x = y Atribui a uma variável o valor de uma expressão. 1
x *= y •
x /= y
x %= y
x += y
ATRIBUIÇÃO x -= y Realiza a operação especificada antes do sinal de igual entre o
x «= y valor na variável e a expressão y, 0 resultado é colocado
x »= y novamente na variável.
x &= y
x A= y
x 1= y
!
i
Tabela 2.6 —Tabela de operadores matemáticos e suas precedências
Na maior parte das vezes, o programador não necessita de ter esta tabela memorizada,
uma vez que a precedência de cada operador foi pensada para corresponder às noções de
senso comum e matemáticas habituais. De qualquer forma, recomenda-se que em caso de
dúvida, se utilize parêntesis para garantir que a expressão é a correcta.
Dos operadores referidos, existem alguns que merecem especial destaque. Os operadores
de incremento e de decremento são muito úteis, mas é necessário ter algum cuidado na
sua utilização. Sempre que o operador vem antes da variável (++x ou --x), a variável é
modificada antes de ser utilizada:
i h t f x ="22 \ ' " " " " """ " " ~;
^onsole.WriteLineC^O}", ++x) ; // 23 é Impresso no ecrã,
, ... . ._ _ // x passa a conter 23 \o caso de
depois modificada:
nht "x "=" 22; " " "" ' " ~ ":
;Console.WriteLine("{0}", x++); // 22 é impresso no ecrã, j
. _ _ / / x _pass_a. a conter 23 :
0 operador is permite verificar se uma certa variável é compatível com um certo tipo.
Embora este operador tenha uma funcionalidade limitada nos tipos de dados elementares,
é muito útil quando se está a programar com objectos, para verificar se um certo objecto é
compatível com uma certa classe em particular.
stririg hõmV=~IICãrTá'Tõnsêcàirf~ "" """' " " "i
1 f (nome is string) ;
^C-A. yari.áye]._nome_é_do.,tip_p _string") ;_ _ !
Os operadores <, <=, > e >= permitem testar a relação que existe entre os valores de duas
variáveis. O operador == é utilizado para testar igualdade e o operador != é utilizado para
testar desigualdade:
'int"x"= 10r " """ " "" " " " ' ~ " "~ " "
int y = 20;
if (x > y)
Console.Writel_ine("x maior que y " ) ;
else if (x < y)
.- Çonsgle^WríteLineC"x menor que y");
•éTsè if (x" == yj """ ' " " " " " " "~ - - - ,
i Console.WriteLlne("x é igual a y");
>else
Console.WríteLine.C!'Nunca, pode acontecer!");_
Para averiguar sobre o estado de duas condições, utiliza-se o "e lógico" (&&) e o "ou
lógico" (M). Para negar uma variável lógica, ou condição, utiliza-se o operador not (!).
;bool fimõésèmana = fruè';" ~'~ ' " """ "" • • •-
bool namorada = false; :
.bool tenisHoje = true; :
// Se ainda não é fim-de-semana
i f (IfimDeSemana)
Console.WriteLine("Nunca mais é fim-de-semana, que seca.");
// Se é fim-de-semana e temos namorada(o)...
if (fimDeSemana && namorada)
Console.WriteLine("Vamos passear com a cara metade");
.// Se existe um(a) namorada(o) ou há ténis hoje...
if (namorada |[ tenisHoje)
console.Write_Line("Hpje é um bom _dia!");
Uma vez que as expressões podem ser constituídas de subexpressões, os operadores
continuam a fazer sentido nestas:
-i f "C (Cx>3~00) && (y<=100)) i| (k>icr '&& í fim) )
Um operador que por vezes é útil, mas que, regra geral, deve ser utilizado com
moderação, é o ?:. O que este operador permite fazer é atribuir um valor resultado a uma
expressão, dependendo de uma condição. A condição é avaliada e, caso seja verdadeira,
resulta na primeira expressão após o "?". Caso seja falsa, resulta na expressão após o ":".
Eis um exemplo:
Trit x = 2'0; :
'•ínt y = (x > 10) ? 5 : 7; // caso x seja maior que 10,
- _ // y .fica com. 5 f __senãp f.i .ca com 7 _ ,
Este código é exactamente idêntico a:
int x = 20;
int y;
i f (x > 10)
i y « 5;
else
: y. = 7;
Finalmente, é de referir que é possível actualizar o valor de uma variável através de uma
operação directa sobre o seu valor. Por exemplo, se quisermos actualizar o valor de uma
variável k somando-lhe 10, basta escrever:
;!<;+=:
Praticamente todos os operadores matemáticos podem ser colocados antes do sinal de
igual, para se obter uma actualização da variável (exemplos: x-= 10 ; y*= 2 ; )
Neste caso, é escrito no ecrã se a é maior ou menor do que b, ficando a variável c com o
maior dos dois valores.
}
else
if Ca == 2) r
>
else
if Ca == 3)
else
Para lidar com esta situação, normalmente a solução mais aceitável é indentar
sucessivamente os if-el se em linhas seguintes. Por exemplo:
:iT'Xa'==l) * "" " "" " " " -----
lei se i f Ca == 2)
lelse if Ca == 3)
l..."" .- ~ .- - -- -- -----
Embora exista uma estrutura para lidar com comparações mutuamente exclusivas deste
género (a expressão swi tch), é importante ter esta noção, pois existem imensos casos
onde não é possível utilizar tais estruturas6.
6 Isto acontece porque na construção swi tch só se pode colocar valores enumeráveis (isto é, que se podem
contar: i nt} byte, etc.) ou sfrings. Existem muitas expressões que não caem nesta gama. Por exemplo, se
se desejar actuar de forma diferente, consoante o valor de uma variável esteja numa certa gama
(mínimo-máximo), não é possível utilizar um swi tch.
© FCA - Editora de Informática 25
C#3.5
Ao escrever-se um swl tch, é necessário colocar sempre entre parêntesis uma expressão a
ser calculada. Esta expressão tem de resultar num valor, que irá ser comparado com
diversas opções (finitas). Cada uma das opções é especificada, utilizando a palavra-chave
case, seguida do valor correspondente e de dois pontos. Em seguida, surge o código
correspondente a executar. Podem ser especificados diversos valores que correspondem
ao mesmo código, colocando diversos case seguidos. Após ter especificado o código que
se quer executar para uma determinada opção, é necessário colocar a palavra-chave
break que faz com que a expressão switch termine7.
case 1:
Console.WriteLine("Meda1ha de Ouro");
break;
case 2:
Console.Writel_ine("Medalha de prata");
break;
case 3:
Console.WriteLine("Meda1ha de Bronze");
break;
case 4:
case 5:
Console.writeLine("Dip"loma de ter ficado no top-5");
break;
! default:
í Console.Wn'tel_ine("Diploma de participação");
j break;
1} „_
Neste exemplo em particular, um corredor que chegue à meta é avaliado consoante a sua
posição de chegada. Para o primeiro, segundo e terceiro lugares, existem medalhas
específicas. O quarto e o quinto lugares levam à execução do mesmo código,
correspondendo a um "diploma de top-5".
Podemos ver que no fim existe uma palavra-chave default, que também é seguida de
código a executar. Esta palavra-chave indica o código que deve ser executado caso a
7 Na verdade, não é estritamente necessário que seja uma instrução break. Pode ser qualquer instrução que
corresponda a um salto explícito; break, return, throw, continue ou goto.
26 © FCA - Editora de Informática
ELEMENTOS BÁSJCOS
expressão não tenha sido igual a nenhum dos valores indicados. Neste exemplo, faz com
que seja emitido um diploma de participação para o atleta.
É ainda possível transferir o controlo de um bloco case para o outro. Para isso, utiliza-se a
expressão goto case-1abe7;, em que case-label representa o bloco case para onde se
quer saltar. Por exemplo, se quisermos que os atletas que recebem a medalha de ouro
também recebam um diploma de participação, escreve-se:
'case 05 '
corj^il^.Wr1téLlneC"MedaTha de ouro");
o; default;
,. . ,
Co'asbTe'.WrítéL-ine("Dip1oma de participação"); ' * * ' v j»*«
break; : *'•*•*-',
J „ " „ _ . __ _
Existem ainda alguns pontos extremamente relevantes para os programadores de C/C-H- e
de Java. Ao contrário dessas linguagens, em C#, não colocar um break antes do case
seguinte não leva a que o case seguinte seja executado. No entanto, isto resulta num erro
de compilação8. O controlo do fluxo de execução dentro de um switch tem de ser
explícito. Um outro ponto relevante é que em C#, pode-se colocar objectos do tipo
s t ri ng nas expressões a avaliar.
Neste exemplo, enquanto k não for igual a 100, o ciclo é executado fazendo com que seja
escrita uma linha no ecrã e o valor de k incrementado de 10 unidades.
O teste da condição é sempre executado antes da entrada no bloco de instruções.
Uma variante deste ciclo é a expressão do-while. Neste caso, o teste da condição é
sempre efectuado no fim do ciclo, sendo o ciclo sempre executado pelo menos uma vez.
Eis um exemplo:
8 É de notar que isto apenas se aplica em casos em que um case tenha alguma instrução. Quando isso não
acontece, os vários case são encarados como diversos casos que devem executar o mesmo código.
© FCA - Editora de Informática 27
C#3.5
nrrt:"ir~= "0; " ~ " ~'~" " " " " '"
Ido
• Console.Wr"iteLine("o valor de k é {0}", k);
: k = k + 10;
-} Wh.lle._Ck_<_lQPJ}j..
Muitas vezes, os ciclos w h i l e e do-while são associados a uma variável lógica, que é
controlada dentro do ciclo:
sboõTTimDõP"rõcjrãma~==~"fãlsê"; . . . . . . . . . . " " ..... • — - • - - - • •• -
Ido
i{
1 apresentaMenuC) ;
í i f ÇopcaoLidaDoUtllIzador == TERMINAR)
: fimDoPrograma = true;
Li: .whll.e._Qfi.niDoPro3rama);....._ _ ........... _______________________ _____
\ '•
l actualização ;
9 Nota: é possível inicialízar mais do que uma variável de controlo ou utilizar mais do que uma expressão de
actualização, separando as expressões por vírgula.
28 © FCA - Editora de informática
ELEMENTOS BÁSICOS
Também é de notar, que qualquer uma das três posições da expressão do ciclo for é
opcional, podendo ser deixada em branco. Por exemplo,
jfor .( .; i): --• -" - - - •- - -; • .:~-;..;:> :. i
Regra geral, quando se utiliza um ciclo for não se deve fazer a actualização da variável
de controlo dentro do ciclo. Qualquer programador, ao ler o código, espera que a variável
de controlo seja apenas actualizada na expressão do for. Se isso não acontecer é
preferível utilizar um ciclo whl l e.
Os ciclos foreach funcionam de forma algo semelhante aos ciclos for. Existe uma
estrutura que mantém um conjunto de dados e é necessário percorrer todos esses dados,
um a um. Um exemplo comum é percorrer todos os elementos de uma tabela.
Consideremos o seguinte exemplo:
10 Os programadores de C mais experientes poderão reconhecer esta construção, que se encontra presente em
muitos programas escritos ao longo dos anos. De facto, para escrever um ciclo infinito, esta era a forma
preferida dos programadores de C, para os quais o f o r C I í) sugeria a palavra forever.
© FCA - Editora de Informática 2.9
C#3.5
O ciclo foreach irá permitir percorrer todos os elementos da tabela, colocando o valor
corrente de iteração numa variável específica. Neste caso concreto, a variável é chamada
1. Assim, o resultado da execução deste código será:
TO" "" " " ' " "" "" " ' ~ " """' " ~:
|34
;i9 . _ . . . . . ._ „ __ _ . . _ _ . . ,.... :
A forma geral de um ciclo foreach é:
jforeach (tipo nomeVarfável\n estruturaDeDados) \e que es
As tabelas são um exemplo de variáveis que se podem utilizar com o ciclo foreach, mas
como iremos ver no capítulo 7, qualquer objecto que suporte a interface lEnumerable
também pode ser utilizado.
i f (existeErro)
{
fimDociclo = true;
. prpg.rama__yal _abo.rtar_! _"
30 © FCA - Editora de Informática
ELEMENTOS BÁSICOS
_
else . . .
Í i f (existeErro)
else
É de notar que se pode usar o break e o continue, tanto nos ciclos while e do-while,
como nos ciclos for.
11 A palavra goto vem da expressão inglesa go to, que significa "vai para".
© FCA - Editara de Informática 31
C#3.5
de ânimo leve, a não ser numa situação muito específica: quando é necessário sair
directamente de ciclos encadeados. Eis um exemplo:
A parte el se é opcional.
Caso se queira testar o valor de uma expressão, contra vários valores
enumeráveis possíveis, utiliza-se um switch.
A forma do swi tch é a seguinte:
switch (expressão)
case vai o ri: ... break;
case valor2: ... break;
default: break;
2.8 TABELAS
Suponhamos que é necessário armazenar um conjunto de elementos semelhantes numa
estrutura de dados. Por exemplo, é necessário armazenar a altura de um certo número de
pessoas a fim de no final calcular a sua média. Um array é uma tabela que permite
guardar um certo conjunto de variáveis, todas com o mesmo tipo.
Por exemplo;
'=" new'"
cria uma tabela capaz de armazenar 10 números reais: Para aceder a cada um dos
elementos, utilizam-se parêntesis rectos:
Colo'ca-na posição 3"~dà tábeTã ò valor" 1785 "" " r .-" ^
]*, = 1.85; ' » v^,
aT tu rãs' *- o o o 12.5 0 0
cão do
0 1 2 3 N.2 N-1 -*— Hp°rb
Ele mento
t t
Primeiro Último
Elemento Elemento
E de notar, que é possível declarar uma tabela sem que esta seja imediatamente
construída. A construção da tabela faz-se apenas quando existe um new ti pó [tamanho]:
;double[] alturas; //Declara uma tabela cara armazenar alturas
Jint total DePessoas; // Total de pessoas existentes
j// Lê totalDePessoas do utilizador
12 Provavelmente, isto surpreenderá os programadores de Visual Basic. No entanto, era C# existem outras
estruturas de dados, que examinaremos mais tarde, que suportam a semântica de uma tabela com a
capacidade de aumentar de tamanho.
34 © FCA - Editora de Informática
ELEMENTOS BÁSICOS
Ambas as fornias são equivalentes. O compilador determina qual é que tem de ser o
tamanho da tabela, cria a tabela e faz as atribuições necessárias para que os elementos
indicados fiquem na tabela.
Ao utilizar tabelas, um eixo muito comum é pensar que o último elemento de uma tabela é
N (o seu tamanho) e não N-l. Assim, se no exemplo anterior o programador tentasse
aceder ao elemento 10 (isto é: nomesDePessoas [10]), isso também seria um erro, pois o
último elemento existente é o 9.
/*
* programa que lê palavras do utilizador, uma por linha
* até à palavra "***fim***", e mostra quais as palavras
* únicas introduzidas.
*/
using system;
class PalavrasUnicas
fim = true;
else
// Verifica se a palavra já foi lida. Se sim,
// descarta-a, senão coloca-a na tabela. Caso a
// tabela esteja cheia, descarta a palavra,
// avisando o utilizador
bool encontrada = false;
for (int i=0; i<totalpaí avrasllnicas; i++)
Devemos, no entanto, salientar que, hoje em dia, a maior parte dos programas funciona utilizando janelas e
caixas de diálogo. Este esquema é apropriado para pequenos programas que se façam, com funcionalldades
bem definidas, que serão utilizados por pessoas proficientes.
36 © FCA - Editora de Informática
ELEMENTOS BÁSICOS
Quando se utiliza tabelas, uma operação que é necessário fazer frequentemente consiste
em copiar uma tabela para outro local. Por exemplo, o seguinte código copia urna tabela
(original) para o início de uma outra tabela (destino):
òm"ginaV= neWint[203; ...... .......................
int[] destino = new int[100];
Devido ao facto de esta operação ser tão frequente e de necessitar de estar optimizada,
nas bibliotecas do C#, existe uma classe Array, com o método copyO que pode ser
utilizado para efectuar este tipo de cópias. No caso do código anterior ficaria:
iri.t[] :o.n;gn rial "= new int[20] j " """" ~"~[
int[] .destino = new int[100]; ,
tábelaoestino, posiçãotfaTabelaDestino,
'
Para terminar a discussão das tabelas simples, vamos agora examinar a questão dos
parâmetros de linha de comandos, quando se chama um programa. Vamos supor que
temos um programa MostraParametros, que é corrido da seguinte forma:
C:\Liv"ro\exempTos>MòstraParatfietrbsVexè óTa ole olT" """ """" " '
Foram --passados 3 parâmetros:
:1: : Ma " " •
.2:
' ' " ole .
O ambiente de execução coloca na tabela parâmetros as sirings que foram passadas na linha
de comandos. A listagem seguinte mostra o código do programa MostraParametros.
/*
* programa que mostra os parâmetros de linha de comandos
* introduzidos pelo utilizador.
using System;
class MostraParametros
{
static void Main(string[] argumentos)
Console.WriteLine("Foram passados {0} parâmetros:",
argumentos.Length);
for (int i=0; i<argumentos.Length; Í-H-)
console.WriteLine("{0}: \ {!}", i, argumentos[i]);
Listagem 2,4 — Programa que lista os parâmetros de linha de comandos introduzidos pelo
utilizador (ExemploCap2_4.cs)
Declarar e criar uma tabela bidimensional é muito fácil. Por exemplo, a tabela anterior
poderia ser criada da seguinte fornia:
,"tnt[.; J 'tabela'=..;new"_1.nt [5,.4]_;;.__. "_._" JT.I/V",./". ".7_~"_'1_"_ "._1. V_'.'...'.'... ' -" " . ' >
Para aceder a cada una dos elementos, basta indicar quais os índices dos elementos,
separados por vírgulas, dentro de parêntesis rectos14:
tabel a [2,3] = 34; "" " //Actualiza o" valor do'elemento (2,3)
Para saber o número total de elementos presentes numa tabela, independentemente das
dimensões, faz-se nomeoaTabela.Length. Para saber quantas dimensões tem a tabela,
utiliza-se: nomeoaTabela. Rank. Finalmente, para se determinar quantos elementos é que
existem numa determinada dimensão, faz-se: nomeoaTabel a. GetLength (dimensão). Por
exemplo, o seguinte fragmento de código mostra o número total de elementos da
tabel a27, quantas dimensões a mesma tem e o número de elementos em cada dimensão:
Console. WritèLlne(irTòtal de elementos :' {0}" , "tábèTa27. Lenqth) ;
Console. WriteLineC"Número de dimensões: {0}", tabel a27. Rank) ;
for (int 1=0; 1.<tabe]a27 i Rank;_3_tt^___........ __
14 Os programadores de C/C-H- e Java deverão ter especial cuidado. Embora o C# se pareça muito com estas
linguagens, no caso das tabelas, a criação das mesmas e o acesso aos elementos não se faz colocando
diversos parêntesis rectos. Como veremos mais à frente, essa notação também existe mas é reservada para
o uso em "tabelas dentro de tabelas".
© FCA - Editora de Informática 39
C#3.5
"{IFTTi^taÇela^^
É tão fácil utilizar tabelas com diversas dimensÕes; como utilizar tabelas bidimensionais.
Por exemplo, o seguinte código cria uma tabela tridimensional, com dez elementos em
cada dimensão:
LLntlfjOábeTaBp;^ Tl!' . ... " " ./' J.T.1... l/_'."..7_."<
Para indicar o número de dimensões, basta simplesmente colocar o número de vírgulas
correspondentes entre os parêntesis rectos.
salários fc-
1 | 1 \ 1 1
1 1 1 \ 1 1
g i ifi
» g 3
y l 3 § S
B 8 ê
Para criar esta estrutura, temos de indicar que queremos criar uma tabela simples, que irá
conter sete elementos, que serão tabelas simples (isto é, com uma dimensão):
;// Cria umã~"tãbeTa de" sete' elementos que' i r a conter tabelas "si mpTes' ..... " ,
.Í'nt[][] sal arjos _=vnew ln.t£7] [].;__..........________, _.. . _____ ......... _____ ..... _, __ ______ i
Como podemos ver, o primeiro parêntesis recto indica o tipo de dados de topo (neste
caso, uma tabela unidimensional de sete elementos), indicando os seguintes o que se
encontra dentro dessa tabela15.
Após esta declaração, as subtabelas ainda não existem. Apenas a tabela de topo. Cada um
dos elementos da tabela tem de ser tratado como uma declaração de uma tabela simples.
Por exemplo, o seguinte código cria a tabela de índice O e inicializa-a com os valores
pertinentes.
sal a ri o,slcr] '= new nrrt[3];" ' ----- ---- - -... --,
Vejamos mais um exemplo. O seguinte código cria uma tabela bidimensional de 2x3,
com tabelas simples no seu interior:
.tab = new Tnt[2,3] [J; ~ ~ " "" ' " ". " ,
tabl ^ new 1nt[5];
tabi í O'X -;new int[3];
tab = new int[8] ;
tab = new 1nt[5] ;
tab = -new 1nt[2] ;
= new int£71;-_ ..
Este código poderia ser abreviado para:
n'nt"[,lE]""tW=
{ new ^'[5], new 1nt[3], aew IntQSl
1nt[2]> new
É de notar, que neste tipo de tabelas também se pode aplicar as expressões Length, Rank
e GetLengthQ. No entanto, estes não actuam recursivamente sobre as subtabelas. Isto é:
:_ _\t. {0}" ,
resulta em:
15 Os programadores de C/C-H- têm de ter extremo cuidado, pois a notação de tabelas nestas linguagens tem
xim significado completamente diferente para as mesmas expressões.
© FCA - Editora de Informática 41
C#3.5
" Para criar uma tabela multidimensional, basta colocar entre os parêntesis
rectos as dimensões a criar. Exemplo:
i n t [ , , ] tabela = new int[5,4,6];
Tabelas
multidimensionais ~ Os elementos são acedidos, especificando os seus índices dentro dos
e tabelas dentro parêntesis. Exemplo: tabel a[2,3,1] acede ao elemento de índice (2,3,1).
de outras tabelas ~ De forma semelhante às tabelas simples, pode-se criar uma tabela
multidimensional inicializando-a imediatamente.
~ nomeoaTabel a. Length obtém o número total de elementos na tabela.
" nomeDaTabel a. Rank obtém o número de dimensões da tabela.
nomeDaTabela.GetLength(dim) obtém o número de elementos na
dimensão dl m.
E possível criar tabelas com tabelas lá dentro. Para isso, especifíca-se as
dimensões da subtabela após os parêntesis da tabela de topo (exemplo:
"i nt [ , , ] [ , ] tabel a = new 1 nt [2,3,4] [, ]; cria uma tabela
tridimensional (2x3x4) de subtabelas bidimensionais).
Após a criação da tabela de topo, cada uma das subtabelas tem ainda de ser
criada (exemplo: tabel a [l, 1,1] = new int[2,3]; faz com que o
elemento (1,1,1) da tabela seja uma tabela de 2x3).
E possível utilizar as expressões Length, Rank e GetLengthCdim) nestas
tabelas, no entanto, estas contam o número de tabelas presentes e
dimensões e não os seus elementos reais.
Hoje em dia, praticamente todo o software está a ser escrito numa ou noutra linguagem
orientada aos objectos. As linguagens estruturadas como o C e o Pascal tiveram muito
sucesso nos anos 70 e 80. Desde os anos 90 que a programação orientada aos objectos -
OOP (Object-Oriented Programmmg) ganhou especial relevância, dominando a indústria
informática. Os exemplos mais familiares de linguagens orientadas aos objectos são o
C-H-, o Java, o Delphi e o SmallTalk.
Estruturas de Dados
constatar; mudar o nome de uma variável numa estrutura de dados pode implicar mudar o
seu nome em milhares de linhas de código onde esta é utilizada. Vejamos um outro
exemplo: se uma função pode alterar o valor de uma variável global, sem que o resto das
funções tenham "consciência" ou esperem essa alteração, isso poderá levar a graves erros
de funcionamento. Estes problemas são muito difíceis de retirar do código. Costuma-se
dizer que neste tipo de arquitectura existe um elevado acoplamento entre módulos.
A programação orientada aos objectos tenta aliviar alguns destes problemas. A ideia
principal é diminuir o acoplamento entre os diversos módulos. Para isso, cada uma das
estruturas de dados é encapsulada dentro de um objecto, que também possui funções que
actuam sobre essa estrutura de dados. Os dados não são directamente visíveis para o
exterior do objecto. Apenas a interface, isto é, as operações disponibilizadas no objecto, é
visível. Em OOP o programador pensa em termos de objectos que modelam o seu problema
e nas relações entre eles. As estruturas de dados específicas e a implementação das
operações sobre as mesmas devem ser apenas "detalhes" de implementação. A figura 3.2
ilustra esta ideia.
Objecto A Objecto B
Como é que se começa este processo? O programador começa por definir "classes". Uma
classe representa um tipo abstracto de dados - famílias de entidades. Por exemplo, um
empregado de uma empresa poderá corresponder a uma classe Empregado:
cláss Empregado - - - - - - -
1 prívãte string Nome; // Nome da pessoa
private int idade; // Idade da pessoa
// Construtor: inicializa os elementos Internos de um objecto
; p u b l i c Empregado(string nomeDaPessoa, int idadeoapessoa)
j^ome - nomeDaPessoa;
idade = idadeoapessoa;
• } '•',/'
// Mostra a informação sobre a pessoa :
p u b l i c void MostralnformacaoQ
console.WriteLine("{0} tem {1} anos", Nome, Idade);
} /.;:' ' . :
Para que serve o construtor? O construtor permite criar uma nova instância da classe. Isto
é, permite criar um novo objecto dessa classe. Por exemplo:
[Êmp^régádÕ^chefePrpjecto - ríew Empregado C" Luís si"]va~"~~34) T ..... ;
ichefep;rojecto.Mostrá±nforínàcao,O ; - _ _ ______ _______ ______ l
faz com que seja criada uma nova instância da classe (um objecto), que irá
guardar o nome e a idade do empregado no seu interior. Ao chamar
chefeProjecto.MostralnformaçãoO, o método é invocado naquele objecto em
particular - o empregado "Luís Silva". Em qualquer altura, podemos criar diversos
objectos da mesma classe, que estes possuem identidades distintas:
Emp~reg~ado erigenheirol_=~new Emp"regàclÕC"Mag"da,Diom'sfõ"7"25) ; ~"~~ • ~ ]
Empregado engenhei ro2 ^ njjffi Empregado("Cecí1ia Cardoso", 25); j
engenhei rol.Mostralnf6^oiã_ça'o,Q^r ^^^-, r - * & ,-.<„. \Q ;, i<
Uma vez que Nome é declarado como p ri vate, apenas elementos da sua própria classe
lhe conseguirão aceder. É certo que é possível declarar todos os elementos de uma classe
como sendo públicos, mas aí está-se a perder todas as vantagens de utilizar uma
linguagem orientada aos objectos, pois está-se a aumentar o acoplamento total da
aplicação.
Uma outra questão importante é o operador new. Este operador é utilizado sempre que se
está a criar uma instância de uma classe, isto é, um objecto. Este operador trata de
encontrar e reservar a memória necessária para conter o objecto e de chamar o construtor
do mesmo, finalmente, retornando uma referência para o objecto criado.
A linha com um quadrado numa das pontas indica uma relação de composição, estando o
losango do lado da classe que contém uma referência para a outra.
Uma possível solução para este problema seria criar uma nova classe que tivesse como
campos o nome, a idade e o número de acções que o patrão possui. No entanto, iríamos
também de ter de duplicar os métodos existentes, para além dos dados já presentes em
Empregado. Uma outra solução possível seria utilizar composição e colocar, dentro da
classe Patrão, uma instância de Empregado. No entanto, novamente, temos aqui o
problema de ter de duplicar os métodos de Empregado na classe patrão.
Quando surge este tipo de problemas, em que uma classe é uma especialização de uma
outra (patrão é um caso especial de Empregado: o patrão é um empregado da empresa),
estamos na presença de uma relação de herança. Isto é, existe uma classe que possui todos
os elementos que outra possui, mas também possui mais alguns, sejam estes métodos ou
dados. No nosso exemplo particular, dizemos que Patrão é uma classe derivada (ou
herdada) da classe Empregado (figura 3.4). A seta indica a relação de herança, indo da
classe derivada para a classe base.
Empregado
Ê um
Patrão
Também é vulgar chamar à classe Empregado classe base, uma vez que está a ser
utilizada como base de uma outra classe que se está a definir.
. } . . . . . . .
A primeira mudança aparente é na declaração da classe:
[class patrão : Empregado " " '" ' ~ " ' "" l'
o que isto quer dizer é que a classe Patrão deriva de Empregado, tendo todas as variáveis
e métodos presentes na classe base (Empregado), Note-se também a modificação no
construtor:
[puBTlc PatraoCstnng nomeDoPatrao, int idadeDoPatrao, int nAccoes)
| ._: ^baseCnojneDppatraq, idadepppatrap)
Como a classe é diferente, o construtor também tem de ser diferente. Após a declaração
do construtor, é indicado, após os dois pontos, a forma como a classe base tem de ser
construída. Isto é, antes de um "patrão ser um patrão, tem de ser um empregado". Assim,
a palavra-chave base representa o construtor da classe acima (Empregado). Neste caso, é
indicado que o objecto base Empregado deve ser inicializado com as variáveis
nomeoopatrao e idadeDoPatrao. Finalmente, no corpo do construtor propriamente dito,
é feita a inicialização da variável que faltava (NumeroAccoes).
É de salientar que na maioria das aplicações não existem muitas relações de herança. As
relações de herança são extremamente úteis quanto se está a desenvolver bibliotecas para
serem utilizadas (ou reutilizadas) por outros. Um eixo muito comum das pessoas que se
encontram a aprender OOP pela primeira vez é pensarem que a herança tem de,
forçosamente, ser utilizada na solução de todos os problemas. Isso não é verdade. Só se
deve utilizar herança em casos em que traga vantagens claras de reutilização ou, caso a
abstracção seja, de facto, algo que seja bem implementado em objectos concretos
derivados.
Uma das regras básicas para determinar se uma relação é de composição ou de herança é
perguntar se se deve dizer contém ou é um. Por exemplo: um automóvel contém um
motor, logo, deve existir uma relação de composição entre automóvel e motor. Não se
pode dizer que um automóvel é um motor. Da mesma forma, pode-se dizer que um
automóvel é um veículo. Assim, existe uma relação de herança entre estas duas entidades.
Não faz sentido dizer que um veículo contém um automóvel.
Sempre que o programador decidir ter uma classe base e várias classes derivadas, deverá
mover o máximo de funcionalidade para a classe base. Por exemplo, se existirem
variáveis com a mesma funcionalidade nas classes derivadas, estas deverão ser
substituídas por uma variável comum na classe base. O mesmo acontece com métodos
semelhantes. É típico existir uma classe base da qual derivam várias outras classes. Ao
conjunto de classes pertencentes à mesma árvore, chama-se hierarquia de classes. A
figura 3.5 ilustra parte de uma hierarquia de classes retirada da documentação da
plataforma .NET.
System.Ob]ect
Syslem.MarshalByRefObjecl
Syslem.lO.Stream
System.lO.FileStream System.Net.Sockets.NetworkSíream
3.4 POLIMORFISMO
Uma outra característica fundamental da programação orientada aos objectos é o
polimorfismo. Por polimorfismo, entende-se a capacidade de objectos diferentes se
comportarem de forma diferente, quando lhes é chamado o mesmo método. Vejamos um
caso concreto.
Nome = nomeDaPessoa;
}
public void MostraNomeQ
console.Writei_ine("{0}", Nome) ;
}
// Método preparado para ser alterado por classes derivadas
public virtual void MostraFuncaoQ
Console.WriteLine("Empregado");
}
// classe patrão, um caso especial de empregado
class Patrão : Empregado
public patrao(string nomeDoPatrao)
: base(nomeDopatrao)
{
Console.WriteLineC"Patrão") ;
}
// O programa principal
class Exemplo3_l
static void MainQ
// Uma pequena tabela dos trabalhadores da empresa
Empregado[] trabalhadores = new Empregado[]
new Empregado("zé Maria"),
new Empregado("António Carlos"),
new PatraoÇ"3osé António")
52 © FCA - Editora de Informática
CONCEITOS DE ORIENTAÇÃO AOS OBJECTOS
r,
// Mostra o nome e a função de todos os trabalhadores
for (int i=0; 1<trabalhadores.Length; I-H-)
trabalhadorés[i].MostraNomeC);
trabalhadores[1] .MostraFuncaoO ;
Console.WriteLineC) ;
No caso deste programa, temos uma classe base Empregado e uma classe derivada
Patrão. Existe, ainda, um método chamado MostraFuncaoQ que, por omissão, diz que
a pessoa é um "Empregado". No entanto, as classes derivadas devem poder modificar este
método para que reflictam a função da pessoa em questão. Assim, enquanto no caso de
um empregado simples o método mostra a palavra "Empregado", no caso do patrão
deverá mostrar "Patrão".
Agora, vem a parte mais interessante: ao escrever emp. MostraFuncaoO ; , o CLR guarda
a verdadeira identidade dos objectos que estão associados a cada referência. Assim,
embora estejamos a chamar o método MostraFuncaoO através de uma referência
Empregado, o sistema sabe que tem de chamar a verdadeira implementação desse método,
para o objecto em causa. Neste caso o resultado da execução seria então:
r ^sj,
Patrão
A isto chama-se polimorfismo. O sistema descobre, automaticamente, a verdadeira classe
de cada objecto e chama as implementações reais para os objectos em causa. Em C#,
© FCA - Editora de Informática 53
C#3.5
sempre que se quiser tirar partido desta funcionalidade, tem de se declarar o método base
como sendo virtual (chama-se a isto métodos virtuais). Sempre que numa classe
derivada se altera a implementação de um destes métodos, como é o caso da classe
patrão, tem de se marcar esse método com a palavra-chave override.
Convém alertar para o facto de que no caso de não se declararem os métodos como virtual
ou não se diga que os novos métodos são override, o comportamento do sistema é
chamar os métodos da classe que está a ser utilizada como referência. Isto é, no pequeno
exemplo de chefe, ao fazer emp.MostraFuncaoO ; , caso MostraFuncaoQ não fosse um
método vi rtual ou a nova implementação não fizesse o override da antiga, a chamada
resultaria em "Empregado" em vez de "Patrão". Isto é uma fonte comum de erros, pelo
que o programador deve estar atento a esta situação.
4.1. t REFERÊNCIAS
Sempre que se cria uma instância de uma classe (isto é, um objecto), este é criado
utilizando o operador new. Os objectos criados residem numa zona especial de memória
denominada por heap. Quando se declara e cria um objecto, o programador apenas fica
com uma referência para esse objecto. Por exemplo, ao escrever:
:ÈmpEegfida/;èlipl^Ln^^ L J ________. I ...j
emp representa uma referência para um objecto que reside no heap, não o "objecto real".
Isto contrasta directamente com a utilização de tipos elementares1. Por exemplo, ao
escrever:
"Alexandre Marques"
vai
1 O que nós designamos por valores elementares (1 nt, doubl e,...) são na verdade os chamados value types,
pois representam valores directamente. Os elementos criados no heap, como sejam as strings, as tabelas e
os objectos em geral, são chamados de reference types, pois são sempre acedidos por intermédio de uma
referência.
© FCA - Editara de Informática 57
C#3.5
emp2
Quando se faz emp2=empl, ambas as referências ficam a apontar para o mesmo objecto.
É de notar que isto não acontece com tipos elementares. Isto é, fazer:
"int vali =' 10; . . . . . . . .
;int vai2 = vali;
vali = 20;
console.w_riteLine("yall = {0} \n.val2 = {!}.".,__.val l,, vai2) ;
não resulta em que tanto vali como vai2 mostrem o valor 20. Quando se trata de
variáveis de tipos elementares, o nome de uma variável representa directamente um valor
associado a uma posição de memória fixa.
Tudo isto tem uma consequência muito importante. Em C#, podemos considerar que
todos os tipos de dados derivam implicitamente de uma classe única, chamada
system.object. Esta classe possui métodos úteis que são aplicáveis a qualquer objecto.
Como todos os objectos derivam de System, ob j ect, que também pode ser referido
abreviadamente pela palavra-chave object, é sempre possível utilizar esse tipo de
referência como "referência universal";
Empregado emç = new" EmpregadoC"ATexaridrè "Marques") ;
:pbject obj = emp; _ . _ . . . _ -
58 © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
Neste caso, obj irá continuar a ser uma referência para um objecto do tipo Empregado.
No entanto, enquanto se estiver a utilizar este tipo de referência, apenas os métodos
disponibilizados na classe system.object podem ser utilizados. Quando, eventualmente,
for necessário voltar a converter o objecto no seu tipo real, é sempre possível fazer uma
conversão explícita:
lÈmpregado empregado/=l^Émp_fegãdo) òbj; " "_ ~_
É de notar que caso obj não tenha uma referência para um objecto cujo tipo seja
realmente Empregado, isto resulta num erro de execução2.
Um ponto importante a reter é que é sempre possível converter um objecto de uma classe
derivada num objecto de uma classe base. Para isso, não é necessário qualquer conversão
explícita. De facto, como um objecto de uma classe derivada é também um objecto de
uma classe base, não faria muito sentido obrigar a que a conversão fosse explícita. No
entanto, os métodos que ficam disponíveis para o programador chamar directamente são
apenas os da classe que se está a utilizar para manter a referência.
Uma das razões pelas quais o uso de referências é tão importante é a gestão de memória.
Em C#, a criação de objectos é feita explicitamente pelo programador, utilizando o
operador new. No entanto, ao contrário de outras linguagens de programação, o
programador não tem de se preocupar em destruir explicitamente os objectos. Sempre que
o garbage collector detecta que um objecto já não está a ser utilizado, ele liberta a
memória ocupada por este. Como é que isto acontece? Vejamos o seguinte exemplo:
:Èmpregado empi"= new Èmpregàdõ"("À"y;
Empregado emp2 = new Empregado("B");
Sempre que se quiser utilizar uma referência que não aponte para nenhum objecto,
utiliza-se a palavra-chave nul l. Por exemplo, em:
object ' obj =" huTT; " " ~ " "" "" " " " "
.Empregado.. emp_=_.nul"I;,
obj representa uma referência que pode apontar para qualquer objecto, mas que neste
momento, não aponta para nenhum, emp representa uma referência para objectos do tipo
Empregado, mas que de momento, também não referencia nenhum.
2 Os erros de execução são também conhecidos pelo nome de excepções. Este tópico será abordado no
próximo capítulo.
© FCA - Editora de Informática 59
C#3.5
Uma questão importante é que sempre que existe um método, todas as variáveis criadas no
âmbito desse método são criadas numa zona de memória chamada stack, desaparecendo
após a execução do método. Assim, se for criado um objecto no heap que apenas é
referenciado por uma variável local de um método, esse objecto, eventualmente, será
destruído pelo garbage collector, após a execução do método. Por exemplo, em:
ivòTcT ~xpto~Q~~ " " * "" ' ' " " ~" '""" !
| Empregado emp = new Empregado(...); i
4.1.2 BOXING/UNBOXING
Na secção anterior, referimos que todos os tipos de dados podem ser tratados como
objectos derivados de System.object. Mas o que acontece com os tipos elementares
(int, d o u b l e , etc.)?
3 Estritamente falando, o que existe é uma estrutura. A diferença entre classe e estrutura será tratada mais à
frente neste capítulo.
GO © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
É sempre possível fazer o unboxing de uma referência. Para isso, basta fazer uma
conversão explícita para o tipo primitivo original:
int vaT2%= (inty òHL"JZI/'_7rJI'7~~"'ir~Zr'*Z~"T~._l//...TfrL7 :• ~".y.:".."7M.l!
O processo de boxingl unboxing é ilustrado na figura 4.3.
Um ponto importante a não esquecer é que quando existe boxing e unboxing, os valores
são copiados. Isto é, não é possível criar uma referência para um inteiro e modificar o seu
valor através da referência, e vice-versa. O valor original é sempre o mesmo e separado
do que foi copiado no processo de boxing. Por exemplo, o código seguinte:
int vali = 10; ~7! Yaju~ê type "rsfmp"TeS"
;object vai2 = vali; .•// ;Èoxi hg .
•vali = 2 0 ; // vali modificado, vai2 fica
;Console.WriteLineX"Valor de vali: {0}"3. vali);
Lçonsole ...writeL.i ne_C!Val gx. de_y al_2i_í 011' . A _ yal 2)_;_
resulta na impressão de:
writeLi neO e declarado como sendo object 4 . Neste método, é então chamado o método
TostringO sobre essa referência, obtendo uma cadeia de caracteres que representa o
objecto em causa. Se estivermos a falar de, por exemplo, console.wn"teLnneC"{0}",
3) ;, é feito o boxing automático do número 3, pois na chamada do método, é necessária
uma referência para este.
Mais tarde, iremos ver que não é exactamente assim. O segundo parâmetro é declarado como sendo uma
tabela de object. No entanto, para a discussão em causa, pode-se encarar o segundo argumento como
sendo um object simples.
62 © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
; X = XÍ ; ;
y = yi ; ,
. }
Esta classe define um ponto e, ao mesmo tempo, permite calcular a distância a outros
pontos. Usando esta classe, o código seguinte é válido:
Ponto pi = new ponto C I O . O , 70.0); '
Ponto p2 = new ponto(14.O, 12.0);
Console.WriteLineC"d(pl,p2) = {0}".,. pl.Distancia(p2)); .
Note-se que o método DistanciaQ é declarado como public. Isto quer dizer, que
qualquer outra classe para além do ponto pode utilizar este método. Isto é, o método é
visível para elementos fora da classe. Neste caso, os campos x e y também foram
declarados como públicos, o que quer dizer que também se pode fazer:
pi.x = 120.0; ' ""
pi,y = 3.2.0; . . ... .
Por omissão, todos os campos (e métodos) que não levam um modificador têm visibilidade
pri vate. Isto quer dizer que apenas são visíveis dentro da própria classe. Se x e y fossem
declarados da seguinte forma:
:class ponto
: private double x;
private double y;
A tabela 4.2 sumaria os modificadores que urn membro pode ter quando é declarado.
MODIFICADOR : DESCRIÇÃO
public 0 membro é completamente visível para fora da classe onde está definido, i
Um campo ou método declarado como p u b l i c pode ser acedido por qualquer entidade,
independentemente de onde este tenha sido declarado. Normalmente, deve-se utilizar o
nível mais restritivo de protecção possível (prívate). Apenas os membros que realmente
foram pensados para ser visíveis e utilizados por outros programadores devem ser
públicos. Por exemplo, é muito má ideia declarar variáveis membro como publ i c (como
fizemos na classe ponto). Isso, regra geral, viola os princípios básicos da programação
orientada aos objectos, perdendo-se as vantagens de utilizar esse paradigma. O problema
é que se existir uma variável declarada como public, quaisquer entidades fora da classe
podem aceder-lhe, sem controlo da classe. Isso aumenta o acoplamento entre os módulos
e faz com que seja muito fácil perder o controlo de que entidades estão a modificar o quê.
Os membros protected só podem ser acedidos por membros da própria classe ou por
classes derivadas desta. Normalmente, declara-se um membro como protected se a classe
está a ser pensada explicitamente para ser derivada por outra classe.
O modificador p ri vate (o que é utilizado caso não se indique nenhum modificador), faz
com que o membro seja visível apenas na própria classe onde está declarado. Normalmente,
os dados e os métodos auxiliares de uma classe devem ser declarados com este
modificador.
Estes são os três níveis principais de acesso que normalmente são utilizados. Para além
destes, existem ainda rnais dois: internai e protected internai, que são utilizados
para especificar protecção a nível de módulos.
Suponhamos, agora, que o programador quer declarar um membro que seja visível por
todas as entidades dentro do módulo corrente, mas não mais. Isto é, um p u b l i c para o
assembly corrente. Neste caso, utiliza-se o modificador i nternal.
Imaginemos, agora, que o programador deseja declarar um membro que seja protected
(isto é, apenas acessível na própria classe e em classes derivadas), mas apenas visível
dentro do assembly corrente. Neste caso, utiliza-se o modificador protected i nternal.
Quando se quer ter uma classe que se possa utilizar a partir de outro assembly, declara-se
essa classe como publ i c. Por exemplo, se declararmos a classe ponto da seguinte forma:
[púBlíc class"Pohtb "" ~' ~ ~ .;"•-,
f^private double x; . ..é-\
private double-y; J í ; '* •
Distancia(Ponto p)
return' Cy~p.,yO*Cy-p.~y)D ;
'T. f -is. - • . . - ' • • .
Teste.exe AssemblyPonto.dll
Nesta figura, podemos ver que o ficheiro Ponto. es é compilado para o assembly de nome
Assembl yponto. dl l. Como a classe ponto foi declarada como publ i c} pode ser utilizada
em outros assemblies. O ficheiro Teste.cs, que resulta no executável Teste.exe faz uso
desta classe. Caso a classe fosse declarada como 1 nternal (o que equivale a declará-la sem
modificador), então, não seria possível utilizá-la fora do assembly Assembl yponto. dl 1.
Todos os tipos de dados (classes) que sejara pensados para ser utilizados fora de um
módulo devem ser declarados publ i c.
4.2.3 CONSTANTES
No capítulo 2, vimos que se pode declarar uma constante, usando a palavra-chave corist.
O mesmo se aplica a valores declarados no âmbito de uma classe. Imaginemos que
estamos a definir uma classe que permite efectuar operações matemáticas comuns6. Entre
outras coisas, queremos também definir algumas constantes, como o PI e o E (número de
Napier), de forma a que possam ser utilizadas tanto nessa classe, como em qualquer
programa que necessite dessas constantes. Para isso, basta declarar as constantes dentro
da classe:
publ i c d ass Matemàti ca
public const double PI = 3.1415926535; •
publlc const double E = 2.7182818284;
Quaisquer métodos da classe Mátemàti ca podem utilizar estas constantes, assim como
qualquer outra classe externa. Por exemplo:
using System; " " ..... ""
cias s Teste
public static void MainO :
Se uma variável de uma classe for declarada como readonly, esta funciona como uma
constante, só podendo ser inicializada uma vez. O único local onde é possível a sua
inicialização é no construtor da classe ou, então, na sua declaração directa.
Vejamos um pequeno exemplo. Imaginemos que temos uma pequena classe que
representa uma impressora. Uma das coisas que é necessário guardar nesta classe é a sua
resolução máxima (MAX_RES), em dpi (dots per incJi). Se o programador declarasse
MAX_RES como constante, então a classe não se conseguiria adaptar aos vários tipos de
impressora, com diferentes resoluções, que estão ligadas a diferentes computadores. Ao
mesmo tempo, declarar MAX_RES como uma variável simples não faz muito sentido, pois
para um computador ligado a uma impressora ern particular, esta é constante. A solução
consiste em declarar MAX_RES como sendo um campo readonly, sendo este inicializado
no construtor da classe:
Após a classe ser instanciada, o valor de MAX_RES nunca pode ser modificado. Quando
esta classe fosse utilizada, o valor da constante poderia ser inicializado, por exemplo, a
partir do driver da impressora. Neste exemplo, apresentamos um valor fixo:
[impressora" Taiseríèf: = new ImprèssoraC60CO ; • - • • • - • — ™ •--
^ working[ n ) ;._________________________________•
Os únicos locais onde é possível inicializar um campo readonly é no construtor e
directamente na sua declaração.
ARFTER ~ Para declarar uma constante numa classe, utiliza-se a palavra-chave const.
" O valor das constantes é fixo e determinado em tempo de compilação.
Constantes e ~ As constantes podem ser declaradas, utilizando diversos níveis de acesso.
campos ~ Para aceder a uma constante, utiliza-se Nomeoacl asse. CONSTANTE.
readonly
~ Os elementos readonly representam constantes determinadas em tempo de
execução.
" Os elementos readonly só podem ser inicializados no construtor da classe
ou na sua declaração.
No entanto, por vezes é necessário que todas as classes partilhem determinada informação.
Por exemplo, suponhamos que, no caso dos empregados, queremos que todos eles
possuam um campo chamado Di rector, que deverá ser igual para todos, representando o
nome do director da empresa. Queremos também que sempre que modifiquemos esse
campo, a alteração seja visível em todas as instâncias da classe. Para isso, utiliza-se um
campo estático declarado, usando a palavra-chave static. Vejamos o exemplo da
listagem 4.1.
r
* Programa que Ilustra a utilização de membros estáticos.
*/
uslng System;
class Empregado
// Nome do empregado
p n" vate strlng Nome;
// Nome do director da empresa
private static string Director;
public Empregado(string nomeDaPessoa)
Nome = nomeDaPessoa;
}
public void MostraQ
Console.Writel_ine("N9me: {0}", Nome);
Console,WriteLine("Director: {0}", Director);
console.WriteLineQ ;
}
public static void AlteraDirectorCstring novooirector)
Director = novooirector;
}
class ExemploCap4_l
public static void MainQ
Empregado empl = new Empregado("Pedro Nunes");
Empregado emp2 = new Empregado("António Bernardes");
Empregado.Alteraoi rectorC"Cari os Fernando");
empl.MostraÇ) ;
emp2.MostraQ ;
Empregado.AlteraDi rector("3osé António");
empl.MostraQ ;
empZ.MostraQ ;
Neste exemplo, Di recto r é declarado como static, o que implica que é partilhado por
todas as instâncias (objectos) da classe Empregado. Para alterar o valor de Director,
existe um método, também este estático, que actualiza o novo valor. Sempre que um
método é declarado como sendo estático, não é necessário (nem sequer possível)
utilizá-lo usando uma referência. É sempre necessário utilizar o nome da classe:
Empregado.Alteraoi rectorC). Se executarmos este exemplo, vemos que o resultado é:
'NómèT Pedro "Nunes
;Director: Carlos Fernando
i
iNome: António Bernardes
Director: Carlos Fernando
iNome: Pedro Nunes ;
'Director: José António i
iNome: António Berrardes ;
tDirector,:__Dpsé António .. ._ ... , . . . . . _ „ :
Daqui, pode ver-se que as duas instâncias de Empregado estão a partilhar a variável
Di rector.
Uma questão relevante quando se utiliza métodos estáticos é que estes não podem aceder
a variáveis não estáticas. De facto, basta pensar um pouco para ver que isso não faria
sentido. Vejamos porquê.
Ao fazer, por exemplo, Empregado. AlteraDi rectorQ , não existe nenhum objecto em
particular sobre o qual estamos a actuar. Estamos a trabalhar com a classe como um todo.
Se dentro deste método tentássemos fazer uma atribuição a Nome, isso resultaria num erro
de compilação, uma vez que um método estático não está associado a nenhum objecto em
particular.
Regra geral, deve-se utilizar membros estáticos o menos possível. Uma fonte de erros
muito comum ern pessoas que estão a começar a aprender OOP é declararem todos (ou
quase todos) os métodos e as variáveis como static e muitas vezes como public. De
facto, o que isso faz é reduzir a linguagem orientada aos objectos a uma forma de
programação estruturada. Como todos os elementos são declarados como static, isso é
equivalente a dizer que só existe uma instância de cada estrutura de dados. Como todos os
elementos são declarados como public, isso quer dizer que todos os elementos são
acessíveis de qualquer parte. Esta forma de programação deve ser evitada a todo o custo.
Normalmente, o problema ali public static começa da seguinte forma. A classe principal
do programa necessita de ter um método MainQ estático. Isto deve-se ao facto de o
programa necessitar de ter um único ponto de entrada bem definido: o CLR necessita de
fazer uma chamada semelhante a classeprincipal .Man n C) para executar o programa.
Um programador pouco experiente pode ver-se tentado a declarar uma variável na classe
principal e usá-la:
class Teste
int Total;
public static void MainQ
total = 10;
}
Este código não compila. A variável Total é uma variável de instância, logo, necessita de
ser acedida através de um objecto. No método Mal n C), não estamos em presença de
nenhum objecto. Neste momento, o programador passa Total a static e o programa já
compila:
class Teste .. , . , . . . . -.-
l static
. int
- Total;
public static void MainQ
Total =10;
}
Para evitar este tipo de problemas, é aconselhável que a classe principal do programa, que
possui o método MainQ, seja colocada à parte. Dentro do M a i n O , devem ser declarados
e usados os objectos necessários, não noutro local. Alternativamente, pode-se criar uma
instância da classe principal no método Mai n(), sendo esta posteriormente utilizada. Algo
semelhante a:
4.3 CONSTRUTORES
Como vimos anteriormente, um construtor permite inicializar os campos de um objecto.
Um construtor é declarado como se de um método se tratasse, mas sem valor de retorno.
O construtor de uma classe tem de ter, obrigatoriamente, o mesmo nome que a classe
onde está definido e é sempre o primeiro bloco de código a ser executado, antes de se
utilizar os métodos de instância da classe.
Uma classe possui sempre um construtor por omissão, que não leva parâmetros.
Consideremos a seguinte classe:
cTãss~Êmp'regã"do"" ....... "" ~" " '" "'
{ í
p ri vate stnng Nome; i
private int Idade; '
public void MostraQ j
console. WriteLine("{0} tem {1} anos", Nome, Idade); :
Apesar de não ter sido declarado explicitamente um construtor, a classe, mesmo assim,
possui um construtor público por omissão. Isto é, é possível fazer:
LPÚcêWdJ?.~patrᣠ~'_'_ _ ~ _ _i
Neste caso em particular, Nome será uma referência a nul l e idade terá o valor 0.
public EmprègádõCs^trTng^nòmèDaPéssõá)
Nome = nomeDaPessoa;
public Empregado(string nomeoapessoa, int idadeoapessoa)
Nome = nomeDaPessoa;
Idade = idadeDaPessoa;
J ..
: public void MostraQ i
Console.wn"teLine("{0} tem {1} anos", Nome, Idade);
; l
Neste caso, passa a ser possível criar objectos, utilizando dois construtores diferentes. Um
em que apenas temos de especificar o nome da pessoa, outro em que temos de especificar
o nome e a idade:
;Élmprégàdo~pT = ~hew "Errip"r"ég"ádb"C"aoaquim Ariíorvíõ11")";
:Empregado p2 =__n_ew_EmpregadoC"poaqui_m António",__4jOj. . . . _ . . . . . . i
É de notar, que não é possível construir objectos de mais nenhuma forma, apenas utilizando
estes dois construtores. Mas o que acontece ao construtor por omissão? Em C# (assim
como noutras linguagens), logo que se declara explicitamente um construtor, deixa de ser
possível utilizar o construtor por omissão. O raciocínio subjacente é o seguinte: a partir
do momento em que se declara um (ou mais construtores), o programador está a dar uma
especificação clara da forma como quer que os objectos sejam construídos. Assim, a não
ser que o programador declare explicitamente que quer um construtor sem parâmetros, o
construtor por omissão deixa de poder ser utilizado7. Isto é, já não é necessário haver um
construtor para o caso de o programador "omitir" a existência de um.
Anteriormente, foi referido que uma variável tem sempre de ser inicializada antes de ser
utilizada. Qual será então o resultado de:
iÈmpregãdõ "p =' nèw" EmpregadoÇ"J"oaqUim" António") ; " ' "
ip.MpstraQ; . . _ . . . _ .._ . „ . . . . . Í
O construtor que apenas leva uma string não inicializa o valor de Idade. Então, será que
isto não viola a regra que diz que uma variável tem sempre de ser inicializada antes de ser
utilizada?
A tabela seguinte sumaria as inicializaçoes por omissão que ocorrem nas variáveis de
instância.
TIPO DE VARIÁVEL VALOR
Numérico
(byte,sbyte,short,ushort int.uint, 0 do tipo correspondente
1 ong , ul ong , f 1 oat , doubl .decimal)
e
Lógico false
(bool)
Carácter '\0'
_(char)
Referências null
(incluindo ob j ect, string e tabelas)
Tabela 4.3 — Valores de inicialização por omissão
7 Em certas circunstâncias, é necessário fazer com que não seja possível ao programador final construir
objectos de uma classe. Para isso, basta declarar um construtor sem parâmetros com o nível de protecção
private.
8 Por esta altura, deve ser claro que uma tabela não é, nada mais, nada menos, do que um objecto que
beneficia de uma sintaxe especial. A mesma coisa acontece com as cadeias de caracteres.
74 © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
A inicialização será:
Empregado incrito' .= Tièw Empr-e^ado()..{" Nome = "Maria", .Idade =.19 >; / _ •
Os inicializadores são úteis para definir valores de propriedades ou campos públicos no
momento da criação de um objecto. No entanto, este mecanismo não deve ser confundido
com o construtor da classe, que tem uma funcionalidade diferente. A atribuição dos
valores especificados no inicializador é sempre realizada após o objecto já estar criado.
Consideremos, ainda, a classe Empregado. Tanto no construtor que leva como parâmetros
o nome e a idade da pessoa, como no que leva apenas o nome, é necessário inicializar o
valor de Nome. Suponhamos, agora, que essa inicialização implicava várias operações ou
que existem vários construtores onde seria necessário repetir o código de inicialização.
Este tipo de situação pode levar a vários problemas, à medida que se tenta manter a
consistência entre os diversos construtores, uma vez que começa a existir muito código
repetido.
Neste exemplo, podemos ver que para utilizar um construtor a partir de outro, basta
colocar dois pontos e utilizar a palavra-chave this, como se de uma chamada a um
método se tratasse. No entanto, essa chamada de método é dirigida a um construtor que
possua a mesma assinatura de parâmetros que lhe é passada. Neste caso, existe um
construtor que leva como parâmetro uma string (o nome da pessoa), sendo esse código
executado antes do corpo do construtor corrente.
Se olharmos para a classe Empregado, podemos ver que é algo desagradável termos de
utilizar nomes de variáveis diferentes nos parâmetros dos construtores, relativamente às
variáveis de instância. Ao longo do livro, isso tem sido feito porque se declarássemos um
parâmetro do construtor com o mesmo nome de uma variável de instância, esta seria
"escondida". Isto é, se existir uma variável definida como parâmetro de um método ou
como parâmetro de um construtor, caso exista uma variável de instância com o mesmo
nome, esta deixa de ser visível:
•ciass"Empregado " ~" ~ ' ----- -_.
:{
private string Nome;
Seria muito mais interessante se pudéssemos utilizar o mesmo nome sem problemas. De
facto, isto é possível utilizando a palavra-chave thi s:
clà~ss* Empregado " " "
{ !
private string Nome; ;
Como thi s representa o objecto corrente, thi s. Nome representa a variável Nome do
objecto corrente. Ao fazer this.Nome=Nome;, a variável de instância fica com o valor da
variável passada como parâmetro de entrada.
Embora esta funcionalidade seja útil neste tipo de situações, as recomendações presentes
na documentação MSDN (Microsoft Developers Networfc), para atribuir nomes às
variáveis e aos parâmetros de métodos, indicam que se deve de utilizar nomes de
variáveis de instância começados por maiúscula e nomes de parâmetros começados por
minúscula, a fim de minimizar este tipo de conflitos9.
Uma outra situação em que o thi s é muito útil é quando é necessário verificar se estamos
em presença do mesmo objecto (isto é, se duas referências representam o mesmo
objecto). Para isso, basta fazer uma comparação simples. Por exemplo, consideremos o
método igual C), que verifica se um empregado é o mesmo que um outro, passado como
parâmetro:
IcTãss" Empregado ~~
Este método começa por verificar se a referência para este objecto é a que é passada
como parâmetro (outro). Caso seja, a pessoa é necessariamente a mesma. Caso não seja,
pode ser a mesma pessoa, mas que está armazenada num outro objecto. Neste caso, é
necessário comparar os nomes das pessoas armazenadas em Empregado 10 . Esta situação
pode surgir, por exemplo, na sequência do seguinte código:
rEmpfègãrdõ""pl~^ riéw ÊtnpregadóTM5oãqúini António"11); ;
jEmpregado p2 = new Empregado(";joaquim António"); í
h"f (pi.igual(p2)) l
; Console.writei_ine("Mesmo empregado") ; i
•else i
: ÇonsoJlê. WrlteLin_e("Di'.f erentes empregadcts1') ; _ __ _ j
Neste caso} existe um empregado pi com uma pessoa armazenada (Joaquim António) e
existe um outro, p2, referente à mesma pessoa, mas armazenado noutro objecto (este
objecto poderia, por exemplo, ter sido lido do utilizador através do teclado). Obviamente,
que as referências irão apontar para objectos diferentes. Isto é:
resulta em f ai se. No entanto, eles ainda representam a mesma pessoa. Daí a comparação
dos nomes. É de notar que se fizéssemos:
ji¥~"CpI. igual CpD)
Isto resultaria imediatamente em. true, devido à comparação das referências. No caso do
método igual(), resolvemos comparar primeiro as referências, em vez de comparar
directamente os nomes, pois esta última operação é mais exigente em termos de recursos.
Vejamos, então, como é que na classe Empregado se pode fazer a inicialização do campo
estático Director:
• cláss Empregado ~ ~ " ' ~
{
p ri vate static .string Director;
O método EqualsQ é herdado de System. Ob j e et, sendo modificado em classes derivadas. Este método
permite comparar dois objectos de um certo tipo. Mais tarde, iremos ver corno é que o operador = pode
ser modificado, por forma a representar uma comparação entre tipos, especificada pelo programador. No
caso do tipo string, isso já foi feito nas bibliotecas da plataforma .NET.
78 © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
Não é fácil determinar por que ordem os construtores estáticos das classes irão ser
chamados. Regra geral, é complicado descobrir onde é que uma classe é pela
primeira vez referenciada. Assim, o código implementado nestes construtores não
deve depender de ordens específicas de inicialização;
Uma vez que este tipo de construtores é chamado automaticamente pelo CLR e
nunca pelo programador, não faz sentido este tipo de construtores possuir um
modificador de protecção de acesso. Isto é, não faz sentido declará-los como
publlc, protected, p ri vate, etc. De facto, é ilegal fazê-lo, resultando num erro
de compilação.
É de notar, que uma classe pode conter um construtor estático e um construtor normal,
sem parâmetros. Enquanto o construtor estático é invocado apenas uma vez, o construtor
normal é chamado sempre que é criado um novo objecto. Isto é ilustrado no seguinte
programa:
/*
* Programa que Ilustra a utilização de construtores estáticos.
*/
uslng System;
class ClasseSimples
statlc ClasseSlmples C)
Console.writeLine("Construtor estático chamado!");
}
publlc ClasseSlmples C)
Console.Wr1teLlne("Construtor de Instância chamado!"D;
Neste programa, existe uma classe, ClasseSimples, que possui um construtor estático, e
um construtor normal, sem parâmetros. No programa principal, cria-se três objectos do
tipo Cl assesi mpl es. O resultado da execução é:
jPFóg"rámã""a"êxêcutãr! ~ " ~'~ '~ ~ " ~ "
iConstrutor estático chamado!
jConstrutor de instância chamado!
|Construtor de instância chamado!
[Construtor,de instância chamado!
Como podemos ver, o construtor estático foi automaticamente executado, sendo corrido
apenas uma vez. Também se pode observar que o construtor estático foi chamado após o
programa ter começado a executar e da primeira vez que classesimples é utilizada.
Finalmente, sempre que se criou uma instância de classe classesimples, o construtor
ordinário de instância foi chamado.
4.3.5.1
Sempre que um campo é declarado como static readonly, isto é, uma constante que é
determinada em tempo de execução, comum a todos os objectos da classe, a sua
inicialização tem de ser efectuada num construtor estático. A razão é simples de perceber:
um campo readonly tem de ser inicializado antes de ser utilizado. Essa inicialização é
feita no construtor. Se o campo também é stati c, então a sua inicialização tem de
acontecer antes de a classe ser utilizada pela primeira vez. Isso corresponde precisamente
à utilização de um construtor estático. O seguinte exemplo ilustra este tipo de utilização:
|77~cl asse""qlíê^rêpTêisêhf á ~o 'ecr^~'dõ~~cõmputãaõ? ~~~ l
idass Ecrã ;
K !
public static readonly SIZE_X; // Tamanho máximo do ecrã XX i
public static readonly SI2E_Y; // Tamanho máximo do ecrã YY |
static Teste()
SI2E_X = 1024; // Esta inicialização é dependente do hardware
SIZE_Y = 768;
í private static'string Director - "<d asco n he.cn de» "j ' ^ J 4/.
s •••
í L ' *v* \- , Jt rt , f r j
Neste exemplo, Nome fica automaticamente inicializado com uma cadeia de caracteres
vazia e a idade da pessoa com o valor 0. Este tipo de inicialização é equivalente a realizar
uma inicialização no construtor da classe. No entanto, quando um objecto está a ser
criado, este tipo de inicializações é efectuado antes de o construtor relevante ser
executado. No caso da variável Director, a inicialização apresentada corresponde à
utilização do construtor estático.
Apesar de útil, em geral, não é recomendável que se faça este tipo de inicializações,
devendo as inicializações serem feitas explicitamente nos construtores. De um ponto de
vísta de engenharia de sofovaret quando se faz a inicialização dos campos de uma classe,
essa inicialização deve ser feita de uma forma completa num único local. Isto, para evitar
que existam variáveis que fiquem esquecidas ou cujos valores sejam "pseudo-inicializados"
incorrectamente em diversos locais.
XptoCint valor)
x = valor;
>
Os métodos, tal como os outros membros de uma classe, podem levar um modificador
que indica a sua visibilidade para o exterior da classe (private, protected, p u b l l c ,
internai ou protected Internai).
É correcto declarar variáveis locais aos métodos com o mesmo nome das variáveis de
instância. Neste caso, as variáveis de instância ficam "escondidas", passando as variáveis
locais a ter prioridade. Para conseguir aceder às variáveis de instância, é necessário
utilizar a palavra-chave thi s:
i class Teste
private int X;
pubile TesteO
í X =-20;
int X = 10;
// Mostra o valor" dá variável local "(107""
. Console.WnteLijie("{Q}..11, X)j _. . .
// MÓ st rã ~ò vai õ r "da "variável de TnstâncTa'~(2Cr)
console,.wrlteL.i.n.e(II{OJ:"I...this.ixl; . --
4.4.2 OVERLOADÍNG
Quando se programa em C#, algo muito comum consiste em declarar vários métodos que
possuem o mesmo nome. A este processo dá-se o nome de overloading. Quando isto
acontece, os métodos são distinguidos pela sua assinatura, isto é, pelos tipos dos parâmetros
que lhes são passados.
i .... .„._„ ._ . . __ i
Nesta classe, existem dois métodos Max C). O primeiro leva como parâmetros dois
inteiros, retornando um inteiro. Este método permite calcular o maior valor de dois
inteiros que lhe são passados. O segundo método permite fazer a mesma coisa, só que
utilizando uma tabela de inteiros11, O facto de ambos terem o mesmo nome não é
problemático. Para ser possível fazer o overload de métodos, o único requisito é que estes
aceitem tipos de dados diferentes como parâmetros.
Por exemplo, consideremos a já nossa familiar classe Ponto, assim como os métodos
Distancia C):
Na implementação do segundo método, deveria existir uma verificação de que a tabela não possui tamanho
0. Tal verificação não é feita porque pensamos que o código, para efeitos de ilustração, fica mais claro
desta forma.
84 © FCA - Editara de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
Neste exemplo, são definidos dois métodos DistanciaQ. O primeiro possui como
parâmetro um ponto, do qual se irá calcular a distância ao corrente. O segundo método
utiliza simplesmente as coordenadas do segundo ponto. Uma vez que se trata de métodos
que trabalham sobre instâncias, é necessário chamá-los sempre sobre um objecto:
fpbrito a~="hewTõnt"õ~Ct7~ ZJ; 77"cri~á~dòis" pontos: "á,"b j
jponto b = new ponto(2, 5); j
! i
Como se pode ver, é possível a coexistência de ambos os métodos, lado a lado, sendo
distinguidos pelos parâmetros de entrada.
Uma funcionalidade muito útil é o facto de ser sempre possível chamar uma versão
diferente do mesmo método. Por exemplo, no caso deste método estático, estamos a
utilizar a versão de instância do método para calcular a distância entre os dois pontos:
return pl.Distancia(p2) ;.
Claro que para utilizar o método DistanciaQ anteriormente criado, é necessário utilizar
a classe como um todo, uma vez que o método é estático:
"d'3T_= ~Pgnto_ .'
Apesar do overloading de métodos ser muito útil, muitas vezes, os programadores caem
na tentação de defínir diversos métodos, exactamente corn a mesma funcionalidade, sem
trazerem grande valor acrescentado à classe. O método Distancia C) apresentado
constitui um bom exemplo. É útil exercer alguma contenção e tentar definir diferentes
formas do mesmo método, quando realmente isso simplifica bastante o trabalho de quem
os ira utilizar.
Por exemplo, imaginemos que na classe Ponto, queremos implementar um método que
calcula a distância do ponto à origem do referencial. Em vez de retornar um valor,
poderíamos ser tentados a escrever o seguinte código:
;pub~lic cláss ponto " " ..... " " ~ ...... "
linguagens, incluindo Pascal e C++, é possível passar as variáveis como sendo "elas
próprias" (isto é, por referência).
Neste caso, o código compila e executa, mas obtém-se como resultado O, em vez de 10,
como se poderia estar à espera. Tal como foi dito, ao chamar um método, o valor da
variável que se encontra como parâmetro de entrada é copiado. Isto é, a variável não é
usada como uma "referência" para a variável original.
Uma questão interessante é que as referências para objectos também são passadas por
cópia. No entanto, é possível utilizar a cópia da referência para modificar o valor dos
objectos propriamente ditos. Por exemplo, é inteiramente possível criar urn método que
modifica um objecto externo ao mesmo. O método seguinte modifica o objecto ponto,
que lhe é passado como parâmetro, de forma a torná-lo no ponto simétrico ao corrente:
public class ponto
Ao fazer-se:
:Ponto a ="rfêw~ pontoCIO". 07" TO". 07 f
[Ponto b = new ponto(0.0, 0.0);
!a.ColocaSimetrico(b);
i_b -.MostrjiQJ .
O resultado da execução será:
Ou seja, apesar de apenas ser passada uma cópia da referência para dentro do método,
ainda é possível modificar o objecto apontado pela mesma.
o compilador irá gerar um erro, dizendo que min e max estão a ser utilizadas sem serem
inicializadas. De facto, o compilador não tem nenhuma forma óbvia de saber que as
variáveis estão a ser utilizadas para sofrerem uma atribuição dentro do método
EncontraMinMaxO- Do ponto de vista do compilador, isto é uma utilização de variáveis
não inicializadas, logo, um erro.
Uma solução simples consiste em inicializar m i n e max com um valor qualquer (por
exemplo: 0), antes de as utilizar. No entanto, existe uma solução melhor. Sempre que
existam variáveis passadas como parâmetro, cujo objectivo é funcionar como parâmetro
de saída de um método (isto é, que irão necessariamente sofrer uma atribuição dentro de
um método), então, utiliza-se a palavra-chave out. Todas as variáveis passadas como
referência, usando a palavra-chave out, não necessitam de estai* previamente inicializadas.
No entanto, o compilador verifica se essas variáveis sofrem realmente uma atribuição
dentro do método em causa. Caso essa atribuição não aconteça, é gerado um erro de
compilação.
>.-- -.
Regra geral, quando se utiliza variáveis de referência, cujo objectivo é serem utilizadas
somente como parâmetros de saída, estas devem ser marcadas como out e não como ref .
maioria das vantagens que decorrem da existência de herança múltipla, mas sem os seus
problemas associados.
Vamos, agora, examinar, com mais detalhe, os mecanismos associados à herança de classes.
4.5. l OVERftiDfNGSWIPLES
Tal como vimos anteriormente, para criar uma classe derivada de outra, basta indicar, na
sua declaração, qual a classe de onde esta deriva. Vamos, então, reexaminar
cuidadosamente o exemplo das classes Patrao-Empregado, incluindo novas
funcionalidades. Neste exemplo, tínhamos uma classe Patrão que derivava de
Empregado:
Iclass Empregado"
<}
iclass Patrão : Empregado
_.. j í
Existem dois pontos importantes neste exemplo. O primeiro ponto diz respeito à
implementação do método. Neste exemplo, estamos a redefinir o método
MostrainformacaoQ. No entanto, uma boa parte da sua funcionalidade já está definida
na classe Empregado. Para chamar um método que se encontra directamente acima na
hierarquia de derivação, utiliza-se a palavra-chave base. Isto é, ao escrever:
é chamado o método da classe Empregado, que irá mostrar a informação referente a essa
parte do objecto corrente. O resto da implementação de MostrainformacaoQ da classe
Patrão garante que também é mostrada a informação sobre o número de acções que o
patrão possui.
Isto quer dizer, que está a ser feita uma nova implementação de um método que já foi
declarado numa classe acima, na hierarquia de derivação. Na verdade, é possível declarar
o método sem utilizar esta palavra-chave, mas isso leva a um aviso de possível erro por
parte do compilador. Como veremos mais tarde, isso serve para garantir que o programador
está consciente de que se encontra a implementar uma nova versão de um método, que
esconde a anterior.
surge:
que é o que estamos à espera. No entanto, o que acontece se fizermos uma conversão para
uma classe base? Isto é:
ÍÊmpreg.ãTo~firnp~^"~bi"gBÕss; ' " "j
Isto é, o método chamado foi o da classe base e não o da classe derivada. Dependendo da
anterior experiência de programação do leitor, isto poderá ser o que é esperado ou não.
Por um lado, estamos a utilizar uma referência do tipo de uma classe base, logo, o método
que deve ser chamado é o da classe base. Por outro, o tipo de dados real do objecto é o da
classe derivada, logo, talvez o método que devesse ser chamado fosse o da classe
derivada.
Regra geral, ao desenhar uma classe, o programador deve pensar claramente se ele
próprio ou outros programadores irão criar classes derivadas dessa classe. Caso existam
cenários em que isso acontece, é importante ter em conta quais são os métodos que irão
ser overrided. Esses métodos deverão ser virtuais.
4.5.2 POUMORFISMO
Como dissemos anteriormente, os métodos que são pensados explicitamente para serem
alterados em classes derivadas devem ser declarados como virtual. Nas classes
derivadas, deve ser feito o override do método. No exemplo anterior, obtemos:
classf prftshao : Empregado
r ; . '. ^, JU~-
j } '"
O segundo programador utiliza a sua classe B, e o respectivo método F(), sem problemas
de maior. No entanto, algum tempo depois, por coincidência, o primeiro programador
resolve adicionar um método F() à classe A. Dependendo da situação, isso poderá levar a
graves problemas, uma vez que a implementação de um método não tem nada a ver com a
implementação do outro. Isto é, os programadores não estavam a pensar em termos de
override do método F C).
Para evitai- este tipo de problemas, em particular neste tipo de situações, a semântica do C#
faz com que os métodos chamados sejam estaticamente aqueles que estavam definidos. Isto
é, são invocações não virtuais, simples, definidas em tempo de compilação. A
implementação B. FQ esconde a implementação A. FQ.
Para além disso, da próxima vez que o segundo programador compilar a sua aplicação,
que inclui a classe B, irá deparar-se com um aviso por parte do compilador. O compilador
irá queixar-se, dizendo que o método FQ não está declarado utilizando a palavra-chave
new. Se repararmos, nesta situação, estamos na presença de um override, mas o
programador não está explicitamente a dizer que a implementação de FQ presente na
classe B é uma nova implementação deste método. Caso o programador tivesse declarado
F() como vi rtual em A3 também surgiria um aviso. Neste caso, o compilador diria que o
novo método não estava declarado nem como sendo new, nem como sendo ovem' de.
Esta última parte é algo complicada, mas bastante importante. Consideremos as seguintes
classes:
'class A " "~ " "" " " " :
class B : A
{
public override void F()
Console.WriteLine("B.FO") ;
}
'class C : B
public new virtual void F Q !
Console.WriteLine("C.FO");
}
class D : C
' public override void F C)
; console.Writel_ine("D.FO"); ;
i >
Existe uma classe A que declara um método virtual FQ. Existe também uma classe B, que
faz o override desse método. Entretanto, existe uma outra classe, c, que diz que FQ
constitui uma nova implementação (new), sendo este um método virtual. Finalmente,
existe uma classe D que faz o override de F Q.
Sempre que num método se coloca a palavra-chave new, para todos os efeitos, é corno se
esse método tivesse um nome completamente diferente de um método com a rnesma
assinatura que se encontre numa classe acima na hierarquia. Isto é, neste caso, é como se
os métodos F() das classe A e B fossem completamente separados dos que surgem em c e
em D.
Assim, ao fazer-se:
[D o b j* D = new D Q ;"
ÍA refA = objo;
é perfeitamente natural que o resultado desta chamada seja "B. FQ". Temos um objecto
da classe D. Entretanto, este é convertido para uma referência de uma classe base A, que
também possui um método F C) . Dado que este método está declarado como sendo virtual,
ao chamar F() usando esta referência, a chamada será feita no ponto mais próximo da
classe D possível. Dado que em c se diz que este método é uma nova implementação,
completamente independente da que estava em B, a classe mais próxima do tipo real de D
será B, fazendo com que o método seja aí chamado.
Já no caso da chamada:
irefç.FQj
isto resultará em "D. FQ". O princípio é o mesmo. Em c, F() é declarado como sendo um
método virtual. Assim, ao utilizar uma referência do tipo c para o objecto da classe D, ao
chamar F(), irá ser determinado o verdadeiro tipo do objecto, sendo a chamada feita o
mais próximo possível dessa classe. Neste caso, isto corresponde ao método FQ da classe
D em si.
Embora este exemplo seja algo complicado, ilustra um ponto muito importante e que
voltamos a realçar: sempre que se coloca a palavra-chave new num método, isso
corresponde a uma nova implementação desse método, que é completamente
independente do que foi definido em classes acima, na hierarquia de derivação.
Caso se queira alterar um método para constituir uma nova definição de um método
acima, na hierarquia de derivação, coloca-se a palavra-chave o vê r ri de.
4.5.4 CUVSSESSEUVDAS
Importa referir, que existem classes seladas, assim como métodos selados. Se um
programador decidir que uma determinada classe não poderá ser utilizada para derivação,
deve declarar a classe como seal ed:
iseãTécT d ass Fí naT
K
Neste caso, qualquer tentativa para utilizar a classe Final como uma classe base resulta
num erro de compilação:
[cTãsse"" Teste T"Fi'tiãl 77" Erro" dê""còrnpi~l"acãb ......... ~;
k i
A mesma situação aplica-se a métodos. Se existe um método em particular de que o
programador deseja fazer, uma única vez, override, então, declara o método como
sealed:
ícTass Base ....... ....... ~~ * " ..... ";
l{
: public virtual void FQ
i ...
j }
Note-se que não faz sentido, colocar directamente um método como selado. Isto é, o
equivalente a:
jcTãss Teste ~ • --— -- - !
Vamos entender porquê. Caso se queira um método que não possa ser modificado em
classes derivadas, basta não declarar o método como sendo vi rtual. Se um método não
for declarado como vi rtual, é automaticamente um método "selado". Pelo contrário, ao
declarar um método como vi rtual, é porque se deseja que o seu comportamento possa
ser modificado em classes derivadas (ou seja, que possa ser feito o seu override}. Assim,
só faz sentido marcar um método como seal ed, numa classe derivada.
Muitas vezes, ao declarar uma classe que irá ser derivada, é comum existirem métodos
que terão forçosamente de existir, mas que não é possível, na classe base, especificar uma
implementação. Essa implementação será apenas definida nas classes derivadas. A este
tipo de métodos chama-se métodos abstractos. Qualquer classe que contenha um método
abstracto chama-se uma classe abstracta.
Um facto que é dado como certo é que cada pessoa da empresa irá ter necessariamente
um ordenado. No entanto, a forma como esse ordenado é calculado para cada um dos
tipos de empregado difere completamente. Assim, faz sentido existir um método
calculaordenadoQ na classe Empregado, mas cuja implementação apenas possa ser
especificada nas classes derivadas:
ãbsfract""cTass Empregado " ..... ~ "" "j
private string Nome;
private int idade;
this.Nome - nomeDaPessoa;
this. Idade = idadeDaPessoa;
~ : baseCnònieDaPessdãV^ldadéDãPessõã)
thls.OrdenadoMinlmo = ordenadoMinimo;
}
public override decimal CalculaOrdenadoQ
return 2*OrdenadoMinimo;
.
Por exemplo, no caso de operário, este recebe sempre duas vezes o ordenado mínimo
corrente do país. Note-se que o método calculaOrdenadoQ foi declarado com a
palavra-chave override. Isto, porque quando se tem um método abstracto, que é
implementado numa classe derivada, esse método é, por definição, virtual.
Um ponto muito importante é que, uma vez declarada uma classe como abstracta, não é
possível criar instâncias dela. Isto é, ao fazer:
•Empregado emp. ="~new EmpregadoíC"çarTos Manuel",_" 23Jj~' _.~~.~' " . . . _." . ... ,
o compilador irá gerar um erro. Ao instanciar uma classe, essa classe tem de ser sempre
uma classe concreta. Isto é, não é possível escrever o seguinte código:
decimal salário = emp.CaTcuTaõrdenadoO ; " \\ . . .
Como GalculaOrdenadoC) ainda não foi definido, este tipo de operação não faz sentido.
No entanto, é perfeitamente legítimo ter um objecto concreto, de uma classe derivada, e
convertê-lo para uma classe base, chamando métodos definidos nas classes derivadas.
Como os métodos abstractos são virtuais, o CLR encarrega-se de encontrar o método
correcto a chamar:
: patrão' donoDaEmpresã' = riew ~Patr~áo("Mànuel Marques""," 61J;"
Empregado emp = donoDaEmpresã; ;
Um exemplo típico é quando se quer utilizar uma biblioteca legada, pré-.NET, que se
encontra numa certa DLL13. Neste caso, é comum criar uma classe que encapsula a DLL,
definindo os métodos nela presentes.
1 DLL significa "Dynamfc Link Libraiy". Consiste num ficheiro com a extensão DLL que contém um
conjunto de rotinas que podem ser utilizadas em diversos programas.
too © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
Neste contexto, assim como na maior parte das vezes} o modificador extern é utilizado
em conjunto com o atributo Dl l impo rt (o uso de atributos irá ser examinado em detalhe
no capítulo 6). Este atributo permite especificar em que DLL está implementado o
método especificado.
iclass Teste :
• [DlllmportC"ModuloAux11iar.dll")]
i publlc extern static vold MetodoExternoQ ;
4.5.7 INTERFACES
Muitas vezes, o programador é confrontado com o complicado problema de decidir de
que classe herdar. Há muitas situações em que seria útil poder herdar de mais do que uma
classe. Por exemplo, imaginemos que temos uma classe base que representa leitores de
CD, assim como uma outra classe base que representa leitores de cassetes. De que classe
deverá herdar uma classe que represente uma aparelhagem? Talvez neste caso, fizesse
mais sentido utilizar composição. No entanto, ao utilizar composição, perdemos a
capacidade de converter um objecto numa classe base e de o utilizar independentemente
das suas especificidades.
Para resolver este tipo de problemas, em C# existe o conceito de interface. Uma interface
permite obter quase a totalidade dos benefícios da herança múltipla, mas sem os seus
problemas. Uma interface especifica um conjunto de métodos que têm de ser
implementados por uma classe. Uma classe pode implementar uma ou mais interface^
bastando, para isso, indicar quais as interfaces implementadas, tendo os respectivos
métodos definidos. As interfaces são extremamente importantes e amplamente utilizadas,
tanto na plataforma .NET, como na programação do dia-a-dia.
Vejamos então um exemplo. Suponhamos que temos uma classe CD, que representa um
CD de música. Hoje em dia, muitos CD começam a trazer, para além das faixas de
música, um ou mais pequenos filmes no seu interior. Assim, ao declararmos uma classe
CD, vamos colocar no seu interior uma string que representa a faixa de áudio e uma
st n" ng que representa a faixa de vídeo:
.cTass CD" ~ "
! private string FaixaAudio;
• private string FaixaVideo;
}._. __ _ _ _ . . .
Dependendo do tipo de leitor que uma pessoa tem, o mesmo será capaz de tocar o áudio
ou o áudio e vídeo. Para explicitarmos a capacidade de tocar um CD, independentemente
da forma como o faz, podemos declarar uma interface, que represente essa capacidade.
No nosso caso, chamaremos a essa interface ÍLeitorCD 14 :
interface "ILèTtorCD - . . . . . . . -
1 void TocaCD(CD cdATocar);
Consideremos, agora, duas classes que representam entidades capazes de tocar CD: um
computador (computador) e uma aparelhagem (Aparelhagem). Para especificar que uma
classe implementa um certa interface, basta fazer uma declaração, tal como se de uma
herança se tratasse, e implementar o método correspondente:
iÇlass^Computadpr : iLeitprCD ~ ~~~~~~~~~~~. _ _ '. l
l public void TocaCD(CD cdATocar)
T .._. . _ _ _ . . . .. . .
Neste caso, o computador é capaz de tocar tanto o áudio como o vídeo de um CD. No
caso da aparelhagem, esta apenas consegue tocar o áudio:
class"ÂpareThagem : "ÍLeitorCD - .. . .
A partir deste momento, podemos utilizar os objectos da classe Aparei hagem e da classe
computador para tocar objectos CD, sem necessitar de conhecer os detalhes de
implementação das mesmas. É possível fazer operações como:
CD tóp20 = new CD("<20 melhores~cãnções>", "<video~rãdical>");
leitorAlvo = stereo;
le1torAlvo.TocaCDCtp.p20); _.__
Para o programador menos experiente, pode não ser aparente qual é a grande vantagem de
se poder utilizar interfaces comparando com utilizar a mesma classe base. Mas, vejamos:
não faria sentido declarar iLeitorco como sendo uma classe, da qual Aparelhagem e
Computador herdariam. De facto, um computador é muito mais do que um leitor de CD,
apesar de também possuir um. Para além disso, também podemos querer que Computador
implemente interfaces como: iLeitorovo, lExecutaProgramas, lAcessointernet e
assim sucessivamente. Por outro lado, a classe Aparei hagem poderá também implementai-
as interfaces como iLeitorCassetes e iRadio. Utilizando herança simples, tal não é
possível de fazer. Usando interfaces basta fazer:
cTass Computador : " ' iXêvtorDVb 7 lExècutâPrõgramás ," lAcessòlnterríét
/*
* Exemplo que Ilustra a utilização de Interfaces
*/
using system;
// classe que representa um CD
ri^ c c CD
class rn
this.FaixaAudio = faixaAudio;
this.Faixavideo = faixavideo;
\c strlng AudloQ
return FaixaAUdio;
class Exemplo4_3
static void Main(string[] args)
CD top20 = new CD("<20 best songs>" , "<video radical>");
Computador pç = new ComputadorQ ;
Aparelhagem stereo = new Aparei hagemQ ;
ILeitorCD leitorAlvo;
Console. WriteLine("Qual o dispositivo a usar? " +
"(pc/stereo) ") ;
string dispositivo = Console. ReadLine() ;
if (dispositivo == "pç")
leitorAlvo = çc;
else if (dispositivo == "stereo")
leitorAlvo = stereo;
else
leitorAlvo = null ;
i f (leitorAlvo í= null) ____ _ __ _ __ __
leitorAlvo.TocaCD(top20);
else
Console.Writel_1ne("D-isposifivo desconhecido");
Uma outra questão importante é que uma interface pode também herdar de uma outra
interface. Nesse caso, a classe que implementar a interface derivada tem de implementar
todos os métodos anteriormente definidos. Por exemplo, consideremos a interface
iGravadorCDRW. Qualquer gravador de CD "RW", também é capaz de ler CD. Portanto,
faz sentido escrever:
i vo;i
11 ,^
Neste caso, qualquer classe que implemente IGravadorCDRW, terá de implementar os
métodos TocaCDO e GravaCDQ.
Tal como as classes, uma interface pode herdar de mais do que uma interface, bastando
para isso, especificá-las separadas por vírgulas.
15 Para os programadores de C++ isto será familiar. Uma interface ern C-H- corresponde à criação directa de
classes puramente abstractas, com todos os métodos virtuais.
© FCA - Editora de Informática 1 O5
C#3.5
Para fazer a conversão inversa, é necessário uma conversão explícita, pois uma referência
para um deteraiinado Empregado pode não corresponder a um Patrão.
p~àtrao_"o'Pàtrap.l=" CPatrap) empjf"/.";". Z .!".'".'.""""".".'."1~""..._" J._7L_~". """..."..
Isto só deverá ser feito se o programador tiver a certeza de que a conversão é possível. De
facto, caso a conversão não seja possível, o CLR irá gerar uma excepção, o que
corresponde a um erro de tempo de execução.
No contexto de conversão entre tipos, existem três operadores muito importantes: is, as e
typeof.
4.6.1 OPERADOR is
O operador is permite testar se um determinado objecto pode ser convertido para um
determinado tipo de dados. Por exemplo:
patrão bigBpss = new Patrão C"MãrfuéT Marquês" , 617; .
•Empregado* emp = bigBoss; ' ;
|if Temp is „ . ..
eCrò _empr_egjiâpL
fará com que seja escrito no ecrã que "o empregado é na verdade um patrão.". Ou
seja, o operador is é útil quando é necessário testar a compatibilidade entre tipos.
Continuando o exemplo anterior, se escrevermos:
ílf"Cemp i-5 strihcp "" " ~ ~"~ "
' conso1e'.WriteLine("lmpossnvel! o empregado é uma string!");
;else ,
Con£role.WriteLine("Tudo ^em^, nada..de estranho. ").;_.
surgirá "Tudo bem, nada de estranho.".
4.6.2 OPERADORAS
Muitas vezes, quer-se fazer algo mais do que simplesmente testar a compatibilidade de
um objecto com um certo tipo. É útil poder converter directamente uma referência para
um objecto numa referência de outro tipo, caso estas sejam compatíveis. Para isso,
utiliza-se o operador as. Este operador converte uma referência para um objecto numa
referência para outro tipo, caso seja possível, ou deixa a referência com o valor nuTl,
caso os tipos não sejam compatíveis. Por exemplo:
.Patrão. bigBoss = new pãtraoÇ""MariueT Marques" ,"6^J]~"" -•• - -
iEmpregado emp = bigBoss;
(patrão opatrao = emp as Patrão; i
:if (oPatrao != null)
Console. WriteLine("emp era do tipo patrão11); '..- • •
else
l Conso1e.._WriteLineC"emp nãp..era_dg tipo Patrão"!;. _ .__ j- .
Este operador é muito útil, quando num método de uma classe base, é necessário converter
o objecto corrente para a real classe derivada16. É de notar, que utilizar o operador as é
semelhante a utilizar o i s, com uma comparação e uma conversão explícita. Isto é,
"^^
é equivalente a:
r-jf ~Çpef r Ã~n's~T
refB = (TipoB) refA;
O operador typeof é uma peça basilar neste processo, permitindo obter uma referência
para um objecto que representa o tipo de dados que lhe é passado como parâmetro.
Vejamos um pequeno exemplo:
Ao executar esta linha, ficamos com um objecto - 1 nfostri ng - que contém informação
sobre a classe string. A partir deste momento, podemos mostrar diversa informação
sobre essa classe. Por exemplo, ao executar:
eC11!^^
!çp_as_ol_eíWrlteLlne_Cl'É interface; '
o resultado é:
PÉ" Cl asse": Truè ......... '
•!É.. Interface :__F.al_se.
O que nos permite concluir que estamos em presença de uma classe e não de uma
interface.
1 Embora exista esta possibilidade, e seja útil em alguns casos, é necessário usar de alguma prudência.
Tipicamente, se numa classe base se testa o verdadeiro tipo do objecto, de acordo com as classes derivadas
existentes, possivelmente está-se a simular polimorfismo com comparações, o que na maioria dos casos
não constitui boa programação orientada aos objectos.
1 OS © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
' _ " console.WitèLIn.eÇmethbd^ | " . ' " " . " . ' " " " ." "".."J.."/"." ..'. " i
iríamos ver todos os métodos existentes na classe s t ri ng17.
É também possível obter o tipo associado a um determinado objecto. Para isso, utiliza-se
o método GetType C), herdado da classe System. ob j ect. Por exemplo, é possível fazer:
>patrao trigBoss = new patrão("Manuel Marques", 61);
rrype fipopatrao =_bjg.Bpss...GetType_Q..; _.._
É importante realçar que a informação presente em ti popatrao diz respeito à classe em
si e não ao objecto. Estamos a tratar de tipos de dados, isto é, classes. Não estamos a
tratar de instâncias dessas classes.
Conversão entre
~ As conversões entre referências para classes abaixo (mais específicas) na
tipos hierarquia de derivação têm de ser sempre explícitas (casf).
- O operador is permite testar se um certo objecto é de um certo tipo.
Exemplo:
nf (emp is Empregado)
4.7 ESTRUTURAS
Nas últimas secções, temos estado a examinar os chamados tipos por referência
(reference types]. Sempre que se cria um elemento deste tipo de dados, o elemento existe
no heap, existindo um overhead significativo na sua criação e também na sua libertação.
17 Note-se que a classe Methodlnfo se encontra definida no espaço de nomes System.Reflection, pelo
que é necessário fazer a sua importação.
© FCA - Editora de Informática l O9
C#3.5
Por vezes, um programador quer, na verdade, definir apenas uma estrutura de dados e não
verdadeiramente uma classe. Por exemplo, um ponto talvez possa ser visto melhor como
uma estrutura de dados simples, contendo uma posição "x" e uma posição "y", do que
como urn tipo de dados abstracto completo, com os mais variados métodos e com uma
interface que esconde completamente.a existência do seu núcleo: os valores de "x" e "y".
Uma estrutura utiliza-se exactamente como uma classe normal. Isto é, para criar um
ponto, basta fazer:
Uma questão crítica a perceber quando se discutem estruturas é que as estruturas não são
objectos. Por exemplo, as estruturas não suportam herança. Uma outra questão bastante
importante é que quando se está a passar uma estrutura como argumento de um método, a
não ser que se utilize a palavra-chave ref, a estrutura é efectivamente copiada por valor.
Isto é, ao fazer:
[Ponto ~pT= "néw "ponto O ; """ " ' ~~"
Ip.x = 10;
IP-y = 20;
;ecra.pesenhaPpntqCp)j ___._.,_ _, _ _ _
quando se chama o método oesenhapontoQ, é feita uma cópia de p para ser utilizada
dentro deste método. Ao contrário do que acontece com as classes, nas estruturas, é
passado realmente o valor da estrutura e não apenas uma referência para o objecto em
causa.
Já referimos que as estruturas não suportam herança em geral. Isto é, não é possível
derivar de uma estrutura e uma estrutura não pode derivar de nada. A única forma de
herança possível diz respeito aos métodos herdados implicitamente de System.object.
Neste caso, é possível, por exemplo, fazer o override do método Tostri ng O e de outros.
Um outro ponto importante a reter é que, no caso das estruturas, não é possível esconder
o construtor por omissão, sem parâmetros. Isto é, a partir do momento em que se declara
uma estrutura, pode-se utilizar sempre este construtor. O construtor sem parâmetros
inicializa todos os campos de uma estrutura com os seus valores por omissão. Não é
possível modificar este construtor. Obviamente, é possível definir outros. No entanto, este
construtor implícito está sempre presente:
istpuct- ponto" ~
pubTic int x; *
publàc iht y;
x, int y)
thig./^ x;
As estruturas devem ser utilizadas com algum cuidado e apenas em casos que se
justifique. Tipicamente, são utilizadas quando se quer agrupar um pequeno conjunto de
dados, que deve realmente ser visto apenas como isso: dados. Caso se esteja em presença
de dados de alguma dimensão com diversas operações associadas, provavelmente estã-se
na presença de uma classe. Aliás, tipicamente será esse o caso.
4.8 ENUMERAÇÕES
Um tipo de dados valor que ainda não examinámos corresponde às enumerações. As
enumerações representam constantes simbólicas de um certo tipo concreto. Em vez de o
programador definir um "inteiro constante", pode utilizar uma enumeração. Embora
internamente as enumerações continuem a ser inteiros, isto pennite que exista type-safeness e
que as constantes sejam agrupadas. Vejamos um pequeno exemplo:
public"èhurri Éstadocivil ~" ""'"" "" !
i SOLTEIRO,
: CASADO,
1 DIVORCIADO,
i VIUVO
Neste caso, definimos um tipo de dados virtual chamado Estadocivil, que pode ter
como valores SOLTEIRO, CASADO, DIVORCIADO ou viuvo. Internamente, ao declararmos
uma variável do tipo Estadocivil, estamos, na verdade, a declarar um inteiro que pode
assumir um dos valores definidos. Também internamente, a cada um dos valores
possíveis é automaticamente atribuído um valor fixo, começando em 0. Por exemplo,
SOLTEIRO é internamente O, viuvo é internamente 3.
Para utilizar a enumeração, basta declarar uma variável desse tipo e utilizá-la
normalmente. Por exemplo:
Estadocivil estadoPèssoà'" " " " " "
; estadoPessoa = Estadocivil.SOLTEIRO; i
switch (estadopessoa)
case Estadocivil.SOLTEIRO:
Console.WriteLine("Solteiro!");
break;
case Estadocivil.CASADO: '
Console.WriteLine("Casado!");
break;
case Estadocivil.DIVORCIADO:
Console.Wri teLi ne("Di vorci ado");
break; _
Isto, apesar de ser lícito definir quais os valores que, internamente, cada elemento da
enumeração deve ter:
publiç, erwm Estadoci vil ~- " ........... :
í " *
SOLJEIRO =1,
'CASADO*. „• = 2,
DlWfS&IADO = 3, ;
VIUVO = 4 - - i
> . * ...... _ .............. . . : - . .
caso, eventualmente, seja necessário extrair o valor correspondente ao elemento, é
necessário realizar uma conversão explícita:
= Cint) Estadoci vil .SOLTEIRO; ..... = •'"'
. valorsolteiro) ; _ ....... //Resulta em 1.
Quando o código é compilado, todas as definições parciais são agrupadas numa classe
única, absolutamente normal. É importante realçar que todas as definições parciais têm de
estar disponíveis quando o código é compilado. Não é possível adicionar campos ou
métodos a classes que já se encontram compiladas para código IL, acrescentando mais
informação a uma classe já existente18.
18 De facto, se tal fosse possível, seria uma grave falha a nfvel dos mecanismos de segurança da plataforma.
© FCA - Editora de Informática 115
C#3.5
4. l O ESPAÇOS DE NOMES
No início deste livro, referimos que quando se escreve:
se está a importar para o espaço de nomes corrente (o espaço de nomes global) os tipos de
dados e elementos definidos em xpto. Vamos ver com mais cuidado o que isso quer
dizer.
Um espaço de nomes permite encapsular um conjunto de definições para que estas não
colidam. Por exemplo, imaginemos que um programador define uma classe útil. Se
existir um outro programador que defina uma classe com esse nome e o código de ambos
116 © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS
Sempre que alguém necessita de utilizar o código desta biblioteca, ou faz a importação de
tudo o que está dentro dela utilizando a directiva using ou, então, utiliza o nome
completo da classe em questão. Por exemplo, suponhamos que temos:
namespace csharpCurs"oCòmpTeto. Teste
public class Pessoa
Caso alguém queira utilizar a classe pessoa tem duas hipóteses: a) importa todas as
classes presentes nesse espaço de nomes escrevendo:
using csharpcursoconipleto.Teste;
no início do seu código ou b*) utiliza o nome completo da classe:
CShãrpCursbcomp"le"to.Testè."Péssoa p" = ~
new CSharpCursoÇompleto.Teste. Pessoa.Q;_._ _
Normalmente, esta última solução é utilizada apenas quando existe um conflito entre duas
classes de bibliotecas diferentes com o mesmo nome.
namespace B
// Código
J ___. . . . .... _. ____ . . . . .
No entanto, isso é exactamente equivalente a definir um nome composto:
inamespace A . B
' -.//.código ._ _ __ _ . .. , _. ....
4.1O.1 AUASES
A palavra-chave using também permite definir abreviaturas para classes e espaços de
nomes. Para isso, faz-se:
Uma excepção representa uma situação anormal que tem de ser tratada em algum ponto
do código. A partir do momento em que é activada, aborta a execução normal do
programa até existir forma de a tratar ou, em último caso, terminar a execução de todo o
programa.
De seguida, iremos ver estas questões em mais pormenor, examinando como é que podem
ser tratadas as excepções, como é que podem ser lançadas (isto é, como é que num certo
ponto do código se notifica o restante programa de que algo está errado) e como é que
podem ser definidas novas excepções.
5. l UM PRIMEIRO EXEMPLO
Vejamos a listagem 5.1. Este programa copia um ficheiro origem para um ficheiro
destino. Os nomes dos ficheiros são passados na linha de comandos.
/*
* Programa que ilustra o conceito de excepções.
* Este programa copia um ficheiro origem para um ficheiro
* destino.
*
* Primeira versão, ainda sem o tratamento de excepções.
*/
using System;
using System.IO;
class copia
static void MainÇstringÇ] args)
origem.CloseQ;
destino.Glose Q ;
Listagem 5.1 — Programa que copia um ficheiro origem para um novo ficheiro destino
(ExempIoCap5_l.cs)
O programa começa por abrir um ficheiro origem para leitura, usando a classe
FileStream, e um ficheiro destino para escrita. Caso o ficheiro destino não exista, é
criado e caso exista, é truncado. O construtor da classe leva como parâmetros o nome do
ficheiro e o modo como este é aberto.
Em seguida, enquanto for possível ler dados do ficheiro de entrada, estes dados são
copiados para o ficheiro de saída. Vejamos esta fase em pormenor. O ciclo principal deste
programa é:
j7/~CbpTã~o"fichei rd ó~ri'gériT"parã "o"Tichei "rõ" cTestiTTo "" ~ ;
ido
í bytesLidos = origem.Read(buffer, O, BUF_SIZE); '
destino.write(buffer, O, bytesLidos); '
""e^CbytesLidps > _ ( £ ) ; _ ^ _ ^ _ _ - _ „ •
No entanto, enquanto se está a copiar os dados, existem imensas coisas que podem correr
mal; entre outras coisas, o disco pode encher, pode haver um erro de leitura ou mesmo
um erro de escrita. Em linguagens como o C} este tipo de situações é tratado, verificando
em todos os pontos do código, quais os valores de retorno das funções chamadas.
Tipicamente, os valores de retorno indicam se houve ou não erro, sendo necessário testar
esses valores contra códigos comuns.
origem. CloseC) ;
destino. Cl oseQ ;
i origem.CloseC);
! destino.Close();
li" " ' "" " '" l
A ideia é que caso ocorra um erro (neste caso uma lOExcepti on), a rotina onde o mesmo
aconteceu lança uma excepção. Uma excepção não é nada mais nada menos do que um
objecto que contém informação sobre o erro que ocorreu1. Quando é lançada uma
excepção, a execução normal do programa é alterada, sendo ignoradas todas as instruções
seguintes do programa. A execução continua no bloco catch mais próximo (envolvente)
cuja classe declarada corresponde à excepção lançada. O código dentro do bloco catch é
responsável por tentar recuperar da situação de erro ou, em casos extremos, abortar o
programa.
Mais concretamente, suponhamos que havia um problema ao escrever buffer para disco:
o disco encontra-se cheio. Neste caso, ao invocar destino.wri te Q, irá ser lançada uma
excepção do tipo lOException. Do ponto de vista do programador, isto corresponde a um
objecto concreto desta classe, que é visível no bloco catch. Quando a excepção é
lançada, a execução normal do programa termina e o controlo é passado para o bloco
catch:
try
i // Copia o ficheiro origem para o ficheiro destino
1 do
1 Um objecto que representa uma excepção é uma instância de System.Exception ou de uma classe
derivada desta.
© FCA - Editora de Informática 123
C#3.5
Neste caso simples, o bloco catch trata simplesmente de dizer que houve um problema
na cópia, mostrar os detalhes do problema e de fechar os ficheiros em causa. Em
particular, dentro do bloco catch é visível um objecto que representa a excepção (aqui
representado pela referência erro). Numa situação real, possivelmente, em vez de se
abortar tudo, no bloco catch estaria código que mostraria ao utilizador a causa de erro e
lhe daria oportunidade de responder se quereria tentar novamente a operação ou não. Para
isso, bastaria envolver todo o bloco try-catch num ciclo, testando uma variável lógica
cujo valor dependia da resposta do utilizador.
Vejamos agora um outro pormenor. Neste programa, uma outra altura onde pode ocorrer
um eixo é quando são criados os objectos que representam os ficheiros. Os ficheiros em si
são abertos nessa altura. Por exemplo, se o ficheiro de entrada não existir, é lançada uma
Existe aqui um pormenor muito importante. Os blocos catch têm de ser especificados do
mais específico para o mais abrangente. Por exemplo, FileNotFoundException é uma
classe derivada de lOException. A primeira representa uma situação particular do caso
genérico que é uma lOException. Assim, o seu bloco catch tem forçosamente de
aparecer primeiro. Trata-se de um erro de compilação se isso não acontecer.
Também é possível colocar um bloco catch sem especificar o tipo de excepção que se
está a apanhar. Nesse caso, o bloco apanha toda e qualquer excepção que ocorra.
Continuando o exemplo anterior:
! catch iFiTeNõtFoundÉxceptTón " erfõj ~ ......... .......... "~ ......... ..... " "i
' ^^ ............ j
f"" ........ "~ errò."FfléNàiiiéjr ~ " "* " """" ~ .................. ' •
$ u (lOException
jcatch , erro) ;
j
Console. WriteLine("ocorreu um erro na cópia do fi chei ro!\n") ; ;
1 Console. Writel_ine("Detalhes: " + erro.Message) ;
|L________________________........_________________________.....____________.„_ .„_......._ _ ____________.....j
[catch
j{ __ .....// _Este bloco
_ _............ ... apanha
-- qualquer excepção
.......... - ...... l
_„ ......
i Console. writel_ine("ocorreu um erro indeterminado no sistema!");
il___________________________. . . . ._........_ ....... ..................._ .... _ . . _ _ _ ...... _______ _____ .í
Regra geral, não é muito boa ideia usar este tipo de construção, uma vez que este bloco
apanha qualquer erro, seja este qual for: falta de memória, erros internos do sistema e
assim sucessivamente.
O leitor mais atento, provavelmente, já reparou que caso não sejam introduzidos
argumentos de linha de comandos, ao fazer-se:
g" TTomèõrigem = ãrgs[0];......~ ""// Nome "do ficheiro de origem " —• --
jstring NomeDestino = args[lj; // Nome do ficheiro de destino . !
no início do programa, estamos em presença de uma situação de erro. Caso não existam
parâmetros, o tamanho da tabela args é 0. No entanto, nestas linhas, estamos a aceder à
posição O e l dessa tabela.
Será que isto quer dizer que se deve colocar um bloco try-catch em volta destas linhas,
ou globalmente, em torno de todo o programa? A resposta é um claro não. (Supomos que
o leitor está agora bastante confuso!).
using System;
using System.IO;
class copia
static void Main(string[] args)
if (args.Length != 2)
Console.Wri teLi ne("Argumentos i nváli dos");
Console.WriteLineCcoçia <ficheiro original> " +
"<fi cheiro destino>"j;
Environment.Exit(O); // Termina o programa
try
// Abre os ficheiros de origem e destino
origem = new FileStream(Nomeorigem, FileMode.Open);
destino = new FileStreamCNomeoestino, FileMode.Create);
// Define um buffer de cópia
const int BUF_SIZE = 8*1024;
byte[] buffer = new byte[BUF_S!ZE];
int bytesLidos = 0;
TT (origem != nul I)
origem.closeC);
catch
{
try
if (origem != null)
destino.closeQ;
}
catch
{
}
}
Listagem 5.2— Programa que copia um ficheiro origem para um novo ficheiro destino, com
tratamento de excepções (ExemploCap5_2.cs)
O bloco final!y deste programa pode parecer bastante surpreendente. O fecho dos
ficheiros está protegido por blocos try-catch individuais e, simultaneamente, para cada
ficheiro, é verificado se a referência correspondente não se encontra a nul l. Na verdade,
tais protecções são essenciais. Por um lado, quando um ficheiro é fechado, pode acontecer
um erro, não sendo possível o seu fecho. Em muitas circunstâncias, a abordagem mais
simples consiste em simplesmente continuar, mesmo não se conseguindo fechar o mesmo.
Nestes casos, abortar o programa talvez seja excessivo - provavelmente dever-se-ia ter
colocado uma mensagem de erro a alterar o utilizador -, por uma questão de clareza,
optamos por deixar o código mais conciso. Ao mesmo tempo, neste exemplo, é
necessário que a protecção do fecho dos vários ficheiros seja feita individualmente. Caso
exista um problema a fechar o primeiro ficheiro, é essencial tentar fechar-se o segundo.
Isso só se consegue utilizando dois blocos try-catch distintos. Finalmente, antes de se
tentar o fecho dos ficheiros, é necessário verificar se as referências correspondentes não
se encontram a nul l. Tal acontece quando não é originalmente possível abrir um ficheiro
(i.e. o construtor da classe Filestreatn lançou uma excepção, estando neste momento o
bloco f i n ai l y a ser executado).
Neste programa, optámos por tratar uma situação excepcional em particular (o utilizador
introduzir o nome de um ficheiro que não existe), apresentando uma mensagem de erro
em particular, tratando todas as outras situações como um erro genérico que impede a
continuação do programa. Note-se que onde anteriormente havíamos utilizado
lOException, passamos a utilizar Exception. Tal foi feito porque existem alguns erros
relacionados com ficheiros, que não são apanhados por lOException. Por exemplo, caso
o ficheiro destino já exista e seja apenas de leitura, o CLR irá lançar uma
System.Unautho ri zedAccessExcepti on.
O ponto mais importante a reter deste exemplo é que, embora a utilização de excepções
seja aparentemente simples, existem normalmente formas de execução do programa que,
caso o programador não seja extremamente cauteloso, podem levar à terminação indevida
da execução ou, mesmo, à continuação da execução com dados incorrectos.
catch
// Trata qualquer tipo de excepção
final l y
// Código executado incondicionalmente
Existe um bloco try dentro do qual podem ocorrer excepções. Em seguida, existem um
ou mais blocos catch que tratam as excepções correspondentes aos tipos declarados.
Pode ainda existir um bloco catch que apanhe todos os tipos de excepção2. No final,
pode existir também um bloco final l y cujo código é sempre executado. A definição das
excepções tem de ser sempre feita da mais específica para a mais genérica, isto é, as
classes mais derivadas devem aparecer sempre primeiro do que as correspondentes
classes base.
Existem ainda dois aspectos importantes que é necessário examinar. Primeiro, um bloco
try-catch pode possuir no seu interior outros blocos try-catch. Esses blocos,
normalmente, servem para tratar erros que podem surgir ao tentar recuperar-se do erro
original. Caso uma excepção esteja a ser propagada num bloco try-catch interior e não
exista um bloco catch correspondente, o examinar dos blocos catch passa para o bloco
try exterior:
2 É de referir que este tipo de catch tern o mesmo efeito que um catch (Exception e) { ... },
também capaz de apanhar qualquer tipo de excepção. Esta última forma tem a vantagem de possuir uma
variável com informação sobre a excepção.
© FCA - Editora de Informática 1 29
C#3.5
try
try
F C) lança uma excepção do tipo
*$$$*- ^~~~-\ •••
TypeBException. 0 CLR
começa por examinar o bloco 7j
catch mais próximo.
c^c-rj ÇrypeAException a) í^\ '" ^, -i1
A excepção vai sendo propagada ao longo
dos diversos blocos catch até encontrar
um capaz de a tratar. É de notar que todos
"•fffíSny U^ os blocos f i nal 1 y intermédios são
executados, mas apenas o catch correcto
;1
é corrido.
Ga^bf^^peBException b)
\]
£/ i
Apenas o primeiro bloco catch capaz de tratar a excepção é executado. Caso não seja
possível encontrar nenhum, a excepção é propagada até ao nível de topo do programa,
fazendo com que o mesmo seja terminado pelo sistema operativo. Um ponto importante é
que todos os blocos f i nal l y intermédios são executados. Tipicamente, nestes blocos,
encontra-se código que trata de libertar recursos pedidos para a execução do segmento de
código em causa.
Para ilustrar este mecanismo, imaginemos que numa aplicação, existe uma chamada a urn
método FQ, que por sua vez ira chamar um método G C). Se em G Q ocorrer um erro que
leve a que uma excepção seja lançada, esta irá sendo propagada ao longo do stack da
aplicação até que seja encontrado um bloco catch correspondente. Para ilustrar este
ponto, consideremos o seguinte exemplo:
!/*
Classe de teste que mostra a propagação de excepções.
.. . . „ . . . . . _ .
catch (Exception e)
// Tratamento da 5. Bloco catch
// excepção encontrado, É feito o
tratamento da excepção.
Neste caso, o método xQ possui um bloco try-catch capaz de tratar qualquer excepção.
Nesse bloco, é chamado o método F() que por sua vez chama G C). Em G C) ocorre um
problema, sendo lançada uma excepção. Dado que G Q não possui nenhum bloco catch, a
excepção é propagada ao longo do stack, para a função que a chamou (isto é, o método
FQ). Dado que F() também não possui um bloco catch adequado, a excepção é
novamente propagada ao longo do stack, sendo finalmente tratada no bloco catch do
método X C).
Novamente aqui, à medida que todas as excepções são propagadas ao longo do stack,
todas as variáveis declaradas nos blocos e métodos intermédios são destruídas. Todos os
blocos final l y intermédios de eventuais blocos try que estejam a envolver as chamadas
dos métodos são executados.
Nesta altura, os leitores que programam na linguagem Java devem estar a pensar que algo
está seriamente errado neste livro. A questão é que em Java é obrigatório declarar quais
as excepções que cada método pode lançar. Tal obriga o programador a pensar
explicitamente nas situações de erro que podem acontecer e, na opinião dos autores, é
algo muito positivo. Em C++ tal declaração é opcional, mas possível. Em C#, tal não é
necessário, nem sequer possível. Quando consultámos alguns representantes da Microsoft
sobre o assunto, a justificação que nos foi dada foi de que foram realizados estudos que
Uma excepção consiste numa instância de system. Exception ou de uma classe derivada
desta. O programador pode criar uma excepção directamente usando esta classe, mas,
mais correctamente, deve definir novas classes que representam excepções que podem
ocorrer no seu código. As excepções definidas pelo programador devem, regra geral,
derivar de system.Appl1cationException. Esta é a classe base reservada para excepções
gerais de um programa.
A classe Exception possui diversos construtores que podem ser utilizados. No entanto, o
mais importante é o que possui uma cadeia de caracteres como parâmetro. Essa cadeia de
caracteres representa uma mensagem de erro, podendo a mesma ser acedida através do
campo Message:
'ÈxceptToTi Idadeínvallda = new Except1on("ldade Inválida");
Para lançar uma excepção, basta fazer um throw do objecto correspondente. Isto é, usando
a excepção definida anteriormente, bastaria fazer algo semelhante a:
"if (Idade < OJ //Condição de erro "
;._ tjirpvy Jdadelnyallda; _ //^Lançamento, da excepção, _ :
Vejamos, então, um exemplo concreto. Consideremos a classe Empregado que temos vindo
a utilizar. Suponhamos que existe um método MudaidadeQ que leva corno parâmetro a
nova idade da pessoa. Caso a nova idade seja menor do que zero, isso constitui um erro,
devendo ser lançada uma excepção.
Mais tarde, este argumento levantou algumas discussões muito interessantes corn engenheiros da Microsoft
sobre robustez de código uy produtividade, sem que nenhuma das partes tenha conseguido convencer a
outra da superioridade do seu ponto de vista.
132 © pCA - Editora de Informática
EXCEPÇÕES
Para implementar esta situação, começa-se por definir uma nova excepção -
idadeinvalidaException 4 , que deriva de system.ApplicationException. Quando
ocorre este tipo de erros, é útil guardar dentro da excepção, a idade que lhe foi passada.
Assim, a implementação desta excepção fica:
p u b l i c class IdãdelnvalidaÊxception : System.ApplicationÈxceptibn
// A Idade Inválida que causou a excepção
p n" vate int idade;
public idadelnvalidaExceptionCint idade)
: base("ldade Inválida: " + idade)
this.ldade = idade;
}
public int obtemldadeQ
return idade;
; >
A classe deriva de system.ApplicationException e guarda no seu interior a idade
inválida que causou a excepção. O construtor leva como parâmetro essa idade e guarda na
classe base, como mensagem de erro, a frase "Idade Inválida", acrescentada da idade em
causa. O programador pode obter essa idade através do método obtemldadeQ.
i f (novaldade < 0)
throw new idadelnvalidaException(novaldade);
Idade = novaldade;
}
Note-se que a linha que muda a idade só é executada caso a idade seja válida. Caso não o
seja, a excepção é lançada, abortando a restante execução do método.
try
4 Tipicamente as excepções devem ser definidas com a palavra "Exception" no final do seu nome.
© FCA - Editora de Informática 133
C#3.5
icatch (idadelnvalidaException e)
i Console. WriteLine("A Idade Introduzida é inválida: {0}", [
e.obtemldadeQ) ; !
;}.________.._ ..... ............ ______ ......... ... .... ..... „._ ... ... _________ '
Neste caso, é mostrada uma simples mensagem de erro, mas num programa real, seria
possível, por exemplo, pedir ao utilizador para introduzir novamente a idade.
Por vezes, também é útil propagar a excepção por mais do que um bloco catch. Por
exemplo, suponhamos que é necessário mostrar uma mensagem de erro específica devido
ao facto de a idade ser inválida, mas que ainda é necessário abortar o programa, estando o
código correspondente a essa fase, no bloco catch global. Nesse caso, é necessário
propagar a excepção após a execução do primeiro bloco. Para isso, basta fazer um throw
simples, sem argumentos:
ffry "" ........ "'"" ......... ' • • - - • • — ..... - -- • ••- .......... - - -- ••
i{ :
• emp.Mudaldade(novaldade) ;
icatch (idadelnvalidaException e) j
l Console.Writei_ine("A idade introduzida é inválida: {0}",
j e.obtemldadeO); :
| throw; // A excepção volta a ser lançada
•catch
j{
| // o programa é abortado aqui
! Environment.ExitCO) ;
i}___________________.... „ _ _ . . _ . ...... .. .... ...... ... ..... . .. _.______________________ '
Como o leitor já deve ter notado, a utilização de excepções interfere de forma muito
poderosa no fluxo de execução normal de um programa. Acima de tudo, as excepções
devem ser utilizadas com cuidado e quando se justifique. Por exemplo, é possível utilizar
excepções para controlar a iteração ao longo de uma tabela:
ÍT rTt [] ~ tabel ã = new int[100]; ...... " - - - ........ - - --.. - ...... - ..... _ - - -
; int i = 0;
l while Ctrue)
í tabela [T] =
!>
,catch (indexoutOfRangeException
; // Fim da iteração
Neste caso em particular, o programador estaria a tentar evitar o overhead da comparação com o fim da
tabela, em cada ciclo de iteração. Embora isso pareça fazer sentido do ponto de vísla de performance,
lançar e apanhar excepções são actividades bastante pesadas, pelo que, provavelmente, neste caso, a
abordagem não funcionará tão bem como esperado.
© FCA - Editora de Informática 135
C#3.5
representam problemas como falta de memória, acessos fora dos índices de uma tabela e
similares.
Na tabela 5.1, são apresentadas algumas propriedades comuns a todas as excepções, que
são herdadas de Excepfion.
PROPRIEDADE DESCRIÇÃO
HelpLlnk Um /////rpara um ficheiro contendo mais informação sobre a excepção.
InnerException Nome da excepção que originalmente deu origem à excepção corrente.
" O bloco try contém o código que pode lançar excepções; os blocos catch
tratam as excepções do tipo de excepção que foi lançado; o bloco f i nal 1 y é
sempre executado.
~ Os blocos catch têm de ser colocados da excepção mais específica (classe
mais derivada), para a mais geral (classe mais próxima da base).
~ Caso não exista nenhum bloco que apanhe a excepção no nível corrente, a
excepção é propagada para o próximo bloco try-catch envolvente, mesmo
que isso implique voltar ao método (ou métodos) que chamaram a função
corrente.
" Para lançar uma excepção, é necessário criar um objecto que directa ou
indirectamente derive de system.Exception e utilizar a palavra-chave
throw para o lançar.
" Caso seja necessário voltar a lançar a excepção, estando dentro de um bloco
catch, utiliza-se a palavra-chave throw sem argumentos.
" Regra geral, as excepções das aplicações devem ser derivadas de
System.Appli cati onExcepti on.
" A propriedade Exception.stackTrace é extremamente útil para efeitos de
debugging, permitindo ver as chamadas que levaram à ocorrência da
excepção.
exemplo, se uma variável representar um saldo de uma conta, nenhum utilizador ficará
contente ao descobrir que ao depositar um certo montante, a sua conta fica com saldo
negativo7.
Para obrigar a que seja gerada uma excepção caso exista um overflow ou um underflow
numa variável, coloca-se o código em causa num bloco checked:
jlíshòrt VãTor~=~~"f>553"5;"" " "" " * ' "" --->•-- - .
jchecked
! ++valor; ]
j}
=.{PJ",
Neste exemplo, irá ser gerada uma System. overf l owException quando valor é
incrementado. O programador é livre de apanhar esta excepção e de a tratar ou de deixar
que a mesma termine o programa.
Como dissemos, o comportamento por omissão do compilador é gerar código que não faz
a emissão de excepções quando os limites das variáveis são excedidos. No entanto, é
possível obrigar o compilador a gerar tais excepções para todo o programa, utilizando a
opção de compilação /checked.
j unchecked
K
i ++valor;
Para terminar, falta referir que se pode utilizar as palavras-chave checked e unchecked
em expressões, utilizando-se, nesse caso, obrigatoriamente parêntesis. Por exemplo, as
seguintes expressões são válidas:
itotal '= checked Cval ò r+1) ; ................. .............. "
itotal = _unchecked__(--valgrj);.
No caso dos tipos elementares, com sinal, sempre que a capacidade da variável é excedida, o valor torna-se
negativo, "dando a volta" para o fím da escala. Isto deve-se ao facto de o bit mais significativo de uma
variável representar o sinal da mesma.
138 © FCA - Editora de Informática
• ' k - . -,-= 'Í
EXCEPÇÕES
ARETER " Para obrigar ao lançamento de excepções em caso de violações dos limites
numéricos das variáveis, utiliza-se blocos checked:
checked
Excepções de
Aritem ética
1 Na plataforma .NET, os componentes são tipicamente encapsulados em asseniblies. Por sua vez,
tipicamente, os assemblies correspondem a uma DLL bem definida.
© FCA - Editora de Informática 1 41
C#3.5
Como se pode ver, existe um componente (botão) seleccionado. À direita, podemos ver as
propriedades do botão. Tudo o que foi necessário para criar esta aplicação foi arrastar o
componente Button da barra de ferramentas à esquerda, para a janela de trabalho e
configurar as suas propriedades. Também foi arrastada uma Caixa de Texto (TextBox).
Note-se que ao configurar a propriedade Text para a palavra "Aceitar", o botão mostra
esse texto no seu desenho.
O componente "botão" também é capaz de lançar eventos. Por exemplo, quando alguém
carrega no botão, pode ser interessante mudar o texto que se encontra na caixa de texto.
Para conseguir este efeito em ambientes de desenvolvimento visuais, basta carregar no
evento associado ao botão, sendo automaticamente criado um método que será chamado
quando o botão é carregado. Tal é ilustrado na figura 6.2.
Neste caso, o que acontece é que o componente "Botão" é capaz de lançar vários eventos
(isto é, notificações). Um desses eventos chama-se Click e acontece quando alguém
carrega no botão. Ao associarmos esse evento com um certo pedaço de código, o pedaço
de código é corrido sempre que aconteça esse evento.
Neste capítulo, iremos abordar, do ponto de vista de programação, a forma como são
construídas as propriedades, os eventos e os atributos. Embora estas três funcional]dades
da linguagem sejam muito úteis quando se está a programar utilizando componentes,
também é possível utilizá-las quando se faz desenvolvimento "tradicional" de código. São
mesmo muito úteis, pois simplificam muitas tarefas de programação, mesmo quando não
se usam ambientes de desenvolvimento visuais.
6. 1 PROPRIEDADES
Vamos voltar ao nosso exemplo da classe Empregado. Um empregado tem diversas
características, nomeadamente o seu nome e a sua idade. Vejamos o esqueleto da classe
que o implementa:
.public clãss "Empregado " ..... .- .._.... ._.
{
p ri vate string Nome;
p n" vate int idade;
public EmpregadoCstring nome, int idade)
this.Nome = nome;
i f Cidade < 0)
throw new idadelnvalidaExceptionCnovaldade) ;
'. this. Idade = idade; :
, }
Como discutimos no capítulo 4, não é boa ideia ter campos da classe declarados como
públicos. Isto é, Nome e idade não devem ser públicos. No entanto, é muito útil poder
modificar o nome e a idade de um empregado directamente. Vamos concentrar-nos na
idade. Uma solução simples consiste em adicionar um método para obter o valor da idade
e outro para a modificar:
public d ass Empregado " " ....... " .........
p ri vate string Nome;
p ri vate int idade;
Do ponto de vista de quem usa a classe, para obter a idade ou para a modificar, basta
utilizar o método correspondente:
Empregado emp = new Émpregãdo("Antõnio Manuel", 19);
iemp. Idade = emp.Tdade^+ .T;. "_"1/.""J"" '„".""."!.""_ ~'' "l ".T J.7 ..V". /"I ... ~. . . ~
E exactamente este tipo de funcionalidade que as propriedades nos permitem ter: tratar
campos privados, como se de públicos se tratasse, mas na verdade tendo métodos a
encapsularem o seu acesso.
Uma propriedade é composta por um método ou por um par de métodos2 que permite
expor um valor como se fosse um campo público. No caso de Nome, ficaria:
:pub~lic class Empregado
get é chamado sempre que alguém tenta obter o valor da propriedade. Neste caso, get
tem um comportamento muito simples: retoma a idade do empregado (ou seja, o campo
IdadeEmpregado).
O método set é chamado sempre que alguém tenta alterar o valor da propriedade, set
possui sempre uma variável implícita - value - que representa o novo valor da
propriedade. Neste caso, o método set verifica se a idade é inválida. Se for, lança uma
excepção. Caso contrário, modifica o valor da variável interna onde é armazenada a idade
do empregado: IdadeEmpregado.
2 Estritamente falando, não se trata de métodos mas funcionam como tal. Nós adoptaremos o nome de
"método" por se tratar de uma descrição com a qual é fácil relacionarmo-nos.
A nomenclatura oficial para este tipo de métodos é accessor methods, existindo o geí accessor e o set
accessor.
© FCA - Editora de Informática 1 4S
C#3.5
Um outro ponto importante relativamente ao get e ao set é que não é necessário declarar
ambos. Por exemplo, se apenas declararmos o get, trata-se de uma propriedade apenas de
leitura (read only). Caso declaremos apenas o set, trata-se de uma propriedade apenas de
escrita (write only}. É bastante comum existir este tipo de propriedades.
Vale ainda a pena referir que, tal como os métodos, pode-se declarar uma propriedade
como estática, ficando associada à classe como um todo, ou como virtual, sendo possível
alterar o seu comportamento em classes derivadas. Também é possível declarar uma
propriedade como sendo abstracta. Nesse caso, é necessário indicar quais os métodos
get/set suportados:
.public abstract int MyProp
{ :
get; // get suportado
set; // set suportado
Quando se tem uma classe que, conceptualmente, pode ser tratada como uma tabela,
então é possível definir uma propriedade que trata um objecto da classe como se de uma
tabela se tratasse.
Por exemplo, suponhamos que temos uma classe cujo único objectivo é armazenar
informação sobre os empregados de um departamento. Chamemos a esta classe
ListaEmpregados. Neste caso, gostaríamos de utilizar os objectos desta classe da
seguinte forma:
s et
{
Lista[index] = value;
: } ;
Seria de esperar que fosse possível passar a uma propriedade indexada mais do que um parâmetro, uma vez
que as tabelas multidimensionais exigem que lhes sejam passadas todas as coordenadas do elemento a
aceder.
return emp;
^eturn null;
set
{
for '(Int 1=0: 1<Lista.Length; Í-H-)
{ , - -
*-1f Çirista[i] .Nome == ríome)
L_4sta[1] ='value;
Note-se que embora a variável que é utilizada para índice seja do tipo string, o que se
está a colocar e a retirar dos objectos da classe ainda são empregados, isto é, do tipo
Empregado. Assim, no set, o que se faz é encontrar na tabela o empregado com o mesmo
nome do que é passado como índice e actualizar o objecto Empregado correspondente.
Caso não exista uma pessoa com o mesmo nome, não se faz nada4.
4 A este tipo de estrutura de dados chama-se uma tabela associativa e, tipicamente, é implementada como
hashiable. Normalmente, quando um elemento não se encontra na tabela, é acrescentado à tabela, ao
contrário do que aqui acontece, em que simplesmente é ignorado.
© FCA - Editora de Informática l 49
C#3.5
6.2 EVENTOS
O sistema de eventos é baseado em dois conceitos básicos: produtores de eventos e
consumidores de eventos. Os consumidores de eventos correspondem a um certo
conjunto de objectos que registam o seu interesse com um objecto produtor, em receber
notificações sempre que algo relevante acontece no produtor. Após a fase de registo,
sempre que existe o lançamento de um evento, existe um pedaço de código que é
executado em cada um dos objectos consumidores. A informação sobre o evento é um
objecto que é passado como parâmetro a esse código. A figura 6.3 ilustra o conceito de
produtor/consumidor de eventos.
/• "\o "DestinoA"
Consumidor da
/• N
/
S
^
eventos de "Origem"
/•
J
\o "DestlnoB"
Objecto "Origem"
Produtor de Consumidor de
eventos eventos de "Origem"
v. / ^ J
\
f \o •DestlnoC'
Consumidor de
eventos de "Origem"
^ J
6.2.1 DELEGATES
O conceito de delegate é bastante simples. Trata-se de uma referência para um método.
Isto é, é possível criar uma referência para um certo método de um objecto, sendo o
mesmo chamado quando se usa essa referência. Vejamos um exemplo simples.
int total = 0;
foreach (int vai i n valores) :
total+= vai;
Console.Writel_ine("Média = {0}", (doubl e) total/vai ores. Length) ;
Ou seja, definimos o equivalente a uma classe, cujas instâncias são funções que levam
como parâmetro uma tabela e não retornam nenhum valor.
Para criar uma instância de Função, utiliza-se a mesma notação que para objectos:
;NhcM7T7=~n~Mw7F^ ]~" "~.....7"'._77 7LII" "77." 7"7"L.L~ri-7~~".'IIJ
Ou seja, criamos uma instância de Função, chamada f que representa uma referência para
o método Matematica.MaxQ. Neste momento, podemos usar f como se fosse
Matemati ca . Max Q :
TntU valores = { 1 2 / 3 2 , 34; 43 , "73 }; ........... ..... "" :
Ou seja, em todos estes casos f funciona como sendo uma referência para um objecto que
representa um método. A listagem 6.1 mostra o código completo deste exemplo.
/*
* Programa que ilustra o conceito de delegate
*/
using System;
public class Matemática
public static void Max(int[] valores)
int max = vai ores [0] ;
foreach (int vai in valores)
i f (vai > max)
max = vai ;
Note-se também que não existe nenhum impedimento a que o delegate retorne um valor.
Isto é, se declararmos:
•delégate int_fúncaò"(int[]
é perfeitamente possível criar uma instância do delegate que aponte para um método que
retorne um valor e obter esse valor no final da invocação:
;íntY"retqrnq L =__f(ya_Tqres)j IL .' _. ^ .T_ - ____ .".._..'_ ........ ... . ........ . .
No entanto, existe um ponto subtil a ter em conta. Se declararmos o delegate como
retomando um valor (neste caso um inteiro), não irá ser possível criar uma instância deste
que aponte para um método que retorne voi d ou outro tipo de dados. Isto é, com a nova
definição do delegate, o seguinte código:
iFuncao f = nevy Função (Matemati ca ._Max)_;
não compila, uma vez que Matemati ca . Max () está declarado como retornando voi d.
6.2.2 MULTÍCASTDELEGATES
Um dos pontos mais importantes dos delegates, e no qual se baseia o sistema de eventos,
é que é possível chamar mais do que um método usando o mesmo delegate. Isto é, é
possível escrever o seguinte código:
Função f; ~ " " '
.f = new Função(Matematica.Max); ;
;f+= new Função(Matemática.Mi n);
Lf+= new FunçãoCMatematica^Mediai; _ . _ ._ _ . ___ . ..._. ... !
Isto é, colocar o delegate a apontar para diversos métodos simultaneamente. Ao chamar:
j f(valores);/7 "" " "'/'.!_.. . "/. 7. ~ * _ . ._. _".'."_" ~. T"" 77".Y_ 7 - ~ 7.777"'..". J' " 7 l
os três métodos são executados em ordem, resultando em:
;Max = 43" " "" ~" '"" " " "" "~ " "~ !
Min = 12
iMédia _=_?8_,8 _ __ „ .. „ .-
Isto representa um mecanismo de chamada de métodos extremamente poderoso. A este
tipo de delegates chama-se multicast delegates. Recordando a figura 6.3, é agora possível
começar a ver os contornos que um sistema de eventos deverá ter.
Note-se que, tal como se pode utilizar o operador 4-= para acrescentar uma referência para
um método a um objecto delegate^ pode-se utilizar o operador -= para a retirar. A notação
é exactamente idêntica.
Até agora, temos estado a examinar o uso de delegates com métodos estáticos. No
entanto, tal não é de forma alguma um requisito. O objecto de um delegate armazena não
só referências para um conjunto de métodos a invocar, mas também os objectos
associados.
L .pub11c_yojd_ Distancia(Ppntp p) _ _ __ _ _ :
154 © FCA - Editora de Informática
PROGRAMAÇÃO BASEADA EM COMPONENTES
Consideremos, agora, que temos dois pontos, pi e p2. É possível definir um miilticast
delegate utilizando-o para calcular a distância de ambos os pontos a um terceiro ponto -
P 3:
;class Teste
; static void Main(string[] args)
! Ponto pi = new Ponto(10, 10);
i Ponto p2 = new Ponto(20, 30);
OperacaoSobrePontos dist;
dist = new operacaosobrepontos(pl.Distancia);
i dist+= new OperacaoSobrePontos(p2.Distancia);
Ponto p3 = new Ponto(50, 50);
dist(p3);
J _ _ _. ... . .... _. . ..
Vejamos em mais detalhe o que acontece. Ao fazer
dist = new ppe.rac~aõsó&f.e^^ L_ .„ _ . . .
o objecto do delegate armazena uma referência para o método a chamar e para o objecto
correspondente. Ao adicionar a segunda referência:
:dist+=. n e w Opera7caoSobF£poh^ _~ _ _ . ... ... ..
Quando é feito:
;ponto p3 = new "PontoCS'0, 50) ;
Nas duas últimas secções, vimos como é possível definir uma "referência para uma
função", chamando-a a partir de outro ponto do código. É muito comum as APIs da
plataforma .NET terem métodos em que o programador necessita de lhes passar como
referência um método com uma assinatura específica, sendo a mesma definida através de
© FCA - Editora de Informática 1 5S
C#3.5
6.2J3.1 MÉTODOSANÓNIMOSUSANDOZ?£LaS47E5
Urn programador poderia, então, utilizar o método Map para, por exemplo, rapidamente
imprimir listas de números. Para tal, usaria o método imprimeQ, assim definido:
; static int; imprime (int valor) " • • • • - - ....... - -•- - - — -:
; Consol e . Wri teLi ne(val o r) ; :
return valor; í
Chamando:
'Trit [J" valores = {l, 2, 3, 4,' 5 };
-,., valores);
surge:
No entanto, também poderia definir um método Quadrado Q que, quando aplicado a uma
lista, resulta numa nova lista contendo os quadrados dos números nela presentes. Ou seja,
definindo:
static int Quadrádò(iht valor)
return valpr*valorj , _
Este tipo de operação é normalmente chamada map pois, dada uma função e uma lista, resulta no raapear
da lista, usando a íunção que lhe foi passada.
156 © pCA - Editora de Informática
PROGRAMAÇÃO BASEADA EM COMPONENTES
e executando:
nnt[] valores = { l, 2, 3, 4, 5 }"; "
"intG quadradas = MapCquadrado, valores);
Map(imprime,quadrados) ;
surge:
l 4"9 16 25 •-" ' " "
Na verdade, neste exemplo, até é possível combinar as duas operações:
= { " l , 2, 3, "4"j" 5 }; ~" " " "••" .
;M_apCXnipraQie,- Map<Quadradpj valores)); _ _ . _ _ . . . - - _ . _
No entanto, para o programador utilizar as funções imprimeO e quadradoQ teve de
definir os métodos como entidades da classe quando, neste caso particular, seria
perfeitamente legítimo defini-los directamente no local onde são utilizados. Para criar
estes métodos anónimos, basta utilizar a palavra-chave delegate, embutindo
directamente o código do método, no local onde anteriormente o mesmo era referenciado.
Por exemplo:
ririt[] quadrados "=" _M_ãp"©uàdrãdõ,. "valores);
Escrevendo o código desta forma, deixa de ser necessário declarar o método Quadrado C)
à parte. Neste caso, diz-se que é um método anónimo, pois a palavra delegate está a
servir como marcador de definição, não sendo necessário atribuir um nome formal ao
método.
Um aspecto muito interessante dos métodos anónimos é que podem referenciar variáveis
externas ao mesmo. Por exemplo, o seguinte código, que soma todos os valores de uma
lista, actualizando a variável total } é válido:
.rntO vàl<pres = -T 17 ~2~; 3 T "í» "5" "í I
int total^ 0;
MapCde1egate(int valor)
total += valor;
return valor;
! J: i 'Calores); ____
© FCA - Editora de Informática 157
C#3.5
6 O capítulo de tópicos avançados descreve com detalhe em que consiste a inferência de tipos. Para já,
apenas é necessário saber que o compilador é capaz de tirar conclusões sobre os tipos de dados que devem
ser utilizados, sem que o programador os tenha de declarar explicitamente.
© FCA - Editora de Informática
PROGRAMAÇÃO BASEADA EM COMPONENTES
As expressões lambda têm a vantagem de fornecer uma sintaxe mais directa e compacta,
sendo consideradas peças importantes na arquitectura da linguagem LINQ. Elas podem
ser compiladas como código ou dados, o que permite que sejam processadas em tempo de
execução por optimizadores, tradutores e avaliadores. Expressões lambda são similares
aos delegates (uma referência para um método) e devem aderir a uma assinatura de
método definida por um tipo delegate. Contudo, a palavra-chave delegate não é usada
para introduzir a construção. Em vez disso, há um novo operador (=>) que informa o
compilador que esta não é uma expressão normal.
Permitem que não sejam explicitamente indicados tipos de dados nos parâmetros de
entrada, tirando o compilador, conclusões sobre os tipos de dados mais apropriados
a usar;
O corpo de uma expressão lambda pode ser uma expressão ou uma declaração em
bloco.
Para ilustrar estes pontos, considere-se o seguinte código, escrito usando métodos
anónimos:
(delegate int "OpCint; a, int b); " // operação" Matemática " " •
Existe ainda um ponto digno de atenção. Tal como acontece com os métodos anónimos,
pode-se referenciar numa expressão lambda, variáveis externas à expressão.
Relembremos o exemplo da secção anterior, que soma os valores presentes numa tabela:
;int[Fvalõres"=T17 2, 3, 47 5 ' } ; ............. ~ }
;int total = 0 ;
i
;Map(delegate(int valor) ;
{
total += valor; :
return valor; :
Sempre que existe uma notificação a ser lançada no código (isto é, sempre que é
necessário lançar o evento), é chamada a variável que representa o evento definido.
Vamos ver uni exemplo concreto disto. Suponhamos que temos uma classe que
representa um computador (computador). Esta classe irá publicar um evento que
representa um login por parte de um utilizador (onLogin). Ou seja, sempre que um
utilizador entra na máquina, é lançado um evento de entrada no sistema. Quaisquer outros
objectos podem registar o seu interesse em receber este evento.
Esta classe define um delegate que leva como parâmetros o objecto que se encontra a
produzir o evento e os argumentos do evento. Neste caso, estamos a supor que não
existem argumentos, logo, a classe usada é System.EventArgs. A classe computador
também publica um evento chamado OnLogin, sendo esta a variável que outros objectos
utilizam para registar o seu interesse neste evento.
Vejamos, agora, quando é que é lançado o evento. Supondo que existe um método
Login C) que é invocado quando um utilizador entra no sistema, bastará chamar o
delegate OnLogi n nesse método. Isto é:
clàss Computador" " '
public delegate void EyentoLogin(object produtor, EventArgs args);
! public event EventoLogin OnLogin;
public void Login(string userName, string password)
Note-se que ao chamar o evento onLogin, é passado como origem do evento, o objecto
corrente (this). Também é criado um objecto de System. EventArgs que indica que o
evento não possui informação adicional.
Qualquer classe que queira tratar o evento de login terá de implementar um método que
corresponda à definição de computador. EventoLogi n. Por exemplo, suponhamos que a
classe Log, que regista as enteadas no sistema, está interessada em receber este tipo de
eventos. Para isso, esta irá implementar o método EntradasistemaQ:
("cTàss Log
public void EntradaSistema(object produtor, EventArgs args) j
Console. Writei_ine("Entrou um utilizador no sistema"); ;
: } !
Neste exemplo, seria muito mais interessante se, ao ser lançado o evento de login, este
contivesse informação sobre o nome de utilizador que enteou no sistema. De facto, esta
informação é extremamente relevante para as partes interessadas em receber este tipo de
evento. Isso pode ser conseguido derivando uma classe de system. EventArgs colocando
essa informação na mesma:
íclass LòginEventArgs f systèm/EventArgs ~~
public string User { get; set; };
public LoginEventArgs(string username) j
i. t
: this.User = username;
;} _. . . _.. .... _ ._ . _ ._..
A listagem 6.2 mostra o código completo deste exemplo, que faz uso da classe
LòginEventArgs.
© FCA - Editora de Informática
C#3.5
/*
* programa que ilustra a produção e consumo de eventos,
*/
using System;
class LoginEventArgs : System.EventArgs
public string User { get; set; };
public LoginEventArgs(string username)
this.User = username;
}
class Computador
public delegate yoid
EventoLoginCobject produtor, LoginEventArgs args);
}
class Log
public void EntradaSistema(object produtor, LoginEventArgs args)
Console.WriteLine("o utilizador <{0}> entrou no sistema.",
args.username);
}
class Exemplocap6_2
static void Main(string[] args)
Log log = new LogQ;
computador computador = new ComputadorQ;
computador. OnLogi n-*-=
new computador.EventoLoginClog.EntradaSistema);
computador.LoginC"pmarques", "secret");
computador.Login("nernani", "topsecret");
} ^^
Listagem 6.2 — Programa que ilustra o conceito de eventos (ExemploCap6_2.cs)
~ Para lançar o evento, a classe que o produz deverá, nos locais apropriados,
chamar a variável de instância associada ao evento. Por exemplo:
OnTipoEvento(this, new system.EventArgsQ) ;
Apesar de estar fora do âmbito deste livro, não resistimos a apresentar um pequeno
exemplo, utilizando Windaws Forms. O que fizemos foi criar um novo projecto utilizando
o VísualStudio.NET, sendo o tipo de projecto Windows Application. Nesse projecto,
acrescentámos uma Texteox e um Button. Finalmente, carregámos no botão que
colocámos noform, o que levou o VisiialStudio a acrescentar código para tratar o evento
cl i ck do botão. O aspecto da aplicação é mostrado na figura seguinte.
L MucOnutOin
p* J HiucOd
No método que o VisiiolSiiidio criou para tratar o evento Oncllck, fizemos algo muito
simples: mudámos o texto que se encontra na caixa de texto:
p ri vate voi d"EJuttonl_cl 1 ck(object sender, System. EventÃrgs e) " ~' " :
; textBoxl.Text = "It W o r k s ! " ;
•>_.. _ ...... . ...... . .;
Se o leitor criar este projecto e repetir esta experiência, irá notar que no código gerado
automaticamente pelo VisualStiidio se encontram as seguintes linhas7:
'partiai clàss Fbrml ~ "" " " "" " :
Note que se trata de uma "classe parcial", estando o código definido em dois ficheiros diferentes.
1 G6 © FCA - Editora de Informática
PROGRAMAÇÃO BASEADA EM COMPONENTES
6.3 ATRIBUTOS
Os atributos são um poderoso mecanismo que permite realizar o que se chama
"programação declarativa". No entanto, um programador da plataforma .NET irá utilizar
mais frequentemente atributos predefmidos do que, propriamente, definir os seus atributos.
Vejamos, então, no que consiste a "programação declarativa". A ideia básica a reter é que
os atributos não resultam em código que o computador irá executar. Em vez disso, são
pequenas anotações que permitem a outras ferramentas descobrir o tipo de ambiente em
que o código deverá correr. Os atributos constituem metainformação referente ao código
existente.
Suponhamos, ainda, que foram escritos muitos programas utilizando esta classe e, um día
mais tarde, o programador decide acrescentar um método que calcula o máximo entre os
valores de uma tabela passada como argumento. Este deverá ser o método preferido para
cálculo de máximos.
© FCA - Editora de Informática 167
C#3.5
Para conseguir isto, o programador marca o método antigo com o atributo obsol ete:
public "clãss"Matemática " " "
[ nWso]ete(Mytilize7o método" Max Cl nt"[] valores) "}] " " _ ;_.7.71777' 77777 77 Í
public static int MaxCint x, int y, int z)
i f (x>y) :
return (x>z) ? x : z;
else
return (y>z) ? y : z;
Ou seja, neste caso, o atributo obsol ete é utilizado para o compilador saber que existe
uma classe ou um método que não deverá ser utilizado, avisando o programador para o
facto.
Assim, como existe o atributo obsol ete, existem muitos outros. No seguinte código:
úsing" System;
.using System.Diagnosties;
usi ng System.Runtime.InteropServi cês;
class Teste
p rivate int estatistica; :
Existem dois pontos subtis neste exemplo. Em primeiro lugar, é possível marcar um
método ou um elemento em geral com mais do que um atributo. Neste caso, Antigo C)
possui simultaneamente dois atributos. O segundo ponto é que caso um atributo não
possua parâmetros, ou estes sejam opcionais, pode-se indicar simplesmente o nome do
atributo. É o caso do atributo obsolete:
'[Obsolete]
ipublic extern.static vp1d_Ant_1goQ ;
O ponto importante é que quando se cria uma classe, os atributos ficam associados à
classe em causa. Outras ferramentas ou outras classes podem consultar quais os atributos
existentes e agir em conformidade. Tipicamente, isto é feito, utilizando o mecanismo de
reflexão mencionado da plataforma .NET.
Embora nos exemplos que demos, os atributos sejam dirigidos ao compilador, na maior
parte das vezes, os atributos são utilizados pelo ambiente de execução para configurar um
conjunto de elementos relevantes, para executar o código em causa. Por exemplo, no caso
de se estar a utilizar COM+, as propriedades transaccionais e de segurança dos
componentes são especificadas usando atributos.
A razão pela qual nós incluímos o uso de atributos dentro da programação baseada em
componentes é simples. Os componentes representam entidades binárias de sofhvare bem
definidas. Na maior parte das vezes, os componentes correm dentro de servidores
aplicacionais ou dentro de outros ambientes que gerem o seu ciclo de vida e de execução.
Assim, é perfeitamente natural existir um mecanismo que permita ao programador,
especificar quais é que são os requisitos que espera do ambiente de execução dos
componentes. Os atributos representam esse mecanismo.
No entanto, existem situações em que não basta indicar o atributo antes ou era que isso
nem é sequer possível. Por exemplo, se um atributo se referir a um valor de retorno, o
atributo deve ser indicado antes do método em causa. No entanto, é necessário
distingui-lo de um atributo que se aplique ao método (a situação "normal"). Para isso,
especifica-se a que é que o atributo em causa se está a aplicar:
IcTass Empregado' " ~" " " " " ~
: [return: ForrnatoNlB]
i public string ContaBancarlaQ :
• > ""
Uma outra situação em que é necessário indicar a que é que se refere um atributo é
quando este é aplicado a um. assembfy. Neste caso, o atributo pode ser indicado após as
cláusulas usi ng mas antes de qualquer código:
10 Mais à frente, veremos como isso pode ser feito. Basicamente, envolve derivar uma nova classe de
system.Att ri bute.
1 "7O © FCA - Editara de Informática
PROGRAMAÇÃO BASEADA EM COMPONENTES
: usihg system";
'[assembly: CLSCompliantCtrue}]
;dass Test
_ £1*
• • • i/"Sfe§s**f
;}..
A tabela 6.1 contém as palavras-chave que se pode usar para evitar ambiguidades na
definição de atributos.
Um ponto importante a recordar é que certos atributos apenas podem ser aplicados uma
vez a um elemento, enquanto outros podem ser aplicados mais do que uma vez. Por
exemplo, é possível aplicar várias vezes o mesmo atributo de segurança a um certo
método, mas utilizando diferentes parâmetros para distingir diferentes privilégios de
utilizadores. No entanto, não faria sentido aplicar duas vezes o atributo CLSCompliant
dentro de um assembly, uma vez que se trata de uma propriedade binária: ou é ou não é.
Vejamos, então, como definir um atributo simples. Suponhamos que queremos definir um
atributo para indicar quem é que foi o autor (ou autores) de um determinado método,
classe ou mesmo assembly:
rAutor("Paulo
[Autor CJIHernani pedroso")]
'clãss csharp-CursõCompTeto *• \
f
T-
• , • * *
„-*..' „
' *
T- ~«-
Este atributo pode ser utilizado, por exemplo, para, dinamicamente, descobrir todos os
autores que, de uma forma ou de outra, possibilitaram a escrita de um certo programa.
get
return NomeAutor;
Dentro do atributo, é guardado o nome do autor, por forma a que essa informação possa
ser retirada mais tarde.
A parte mais interessante desta classe é que também possui um atributo a marcá-la:
LAttrfbuteUsageCAttrfbuteTargets.Afl ,
Al l owMul ti pi e=t rue ,
. . Inhented=false)]
public class AutorAttribute System. Attribute
J.
AttributeTargets.All indica que o atributo é aplicável a todos os elementos da
linguagem de programação. Neste primeiro campo, é válido fazer combinações "OU" dos
elementos apresentados na tabela seguinte.
~
CAMPOS DE ATTR1BUTETARGETS
Constructor
Delegate
Enum
Event
Fiel d
interface
"MêtfiõcT
Module
CAMPOS DE ATTRIBUTETARGETS
l) .Property
Returnvalue
í[ Struct
Isto é, se, por exemplo, se quiser especificar que um certo atributo apenas é aplicável a
classes e a interfaces, tal é conseguido com:
: [AttrtbuteUsageCAttributéTargets.cTasslAttributétargets.Interface,
AliowMulti pie=true,
Inherited=false3]
Ou seja, não é complicado definir um novo atributo. Basta criar uma classe contendo a
informação relevante para o atributo (os construtores dessa classe indicam a forma como
o atributo pode ser utilizado) e definir em que locais é que o atributo pode ser utilizado
pelo programador, assim como as suas características.
Um pormenor que talvez tenha surpreendido o leitor é que o atributo que define o próprio
atributo (isto é, AttributeUsage) utiliza uma sintaxe algo especial:
TAttrlbutéÚsageCAttrnbuteTargets.All,
AllowMulti pie=true,
í . : ; ^:znherited=false)] . _.__ __ .... . . . . . . .
Nomeadamente, o colocar AllowMultiple=true e inherited=false parece algo
estranho. Se o leitor consultar a documentação desta classe, irá concluir que esta só
possui um construtor e este apenas leva um parâmetro: Att n" buteTargets.
Para declarar um elemento opcional num atributo, basta criar uma propriedade pública,
passando a ser possível utilizá-la da forma semelhante à apresentada acima. Primeiro,
surgem os campos de um dado construtor do atributo em causa e, em seguida, surgem as
propriedades opcionais da classe, separadas por vírgulas. Por exemplo, para adicionar um
campo opcional Emai 1 ao atributo Auto r, faz-se:
[ÃttrfbutéUsageCAttrfbutèTargéts".An, " "" ~~
: AllowMulti pie=true, :
: lnherited=false)] :
Ipublic class AutorAttribute : System.Attríbute ]
l get
j return NomeAutor; ;
1 > :
puFIic strfng ÊmaiV" " ~~ ~~ "" " ~
get
return EmailAutor;
set
{
this.EmailAutor = value;
..}.... ...._. _..
j
O operador typeof permite descobrir informação sobre a classe csharp_cursocompl eto,
estando o método GetcustomAttributesQ a ser utilizado para obter informação sobre
os atributos definidos pelo programador11.
/*
* programa que ilustra o uso de atributos.
*/
using system;
[Attri buteUsageCAttri buteTargets.All,
AllowMulti pie=true,
inherited=false)]
public class AutorAttribute : System.Attribute
{
private string NomeAutor;
private string EmailAutor;
public AutorAttributeCstring nome)
this.NomeAutor = nome;
this.EmailAutor - "<desconhecido>";
11 A variante do método usada no exemplo leva corno parâmetro uma variável lógica que indica se devem ou
não ser incluídos atributos herdados de classes acima. Neste caso, estamos a especificar que sim.
© FCA - Editora de Informática 175
C#3.5
get
return NomeAutor;
s et
thls. EtnailAutor = value;
}
publlc class ExamploCap6_3
publlc statlc vold MainC)
System . Ref l ectl on . Memberlnf o 1 nf o ;
Para terminar a descrição da linguagem C#, falta-nos cobrir alguns tópicos mais
avançados. Esses tópicos são abordados ao longo das secções deste capítulo, assim como
outros tópicos isolados que não teriam muito cabimento num dos capítulos anteriores.
É de referir, que os tipos anónimos são tipos de referência que derivam directamente da
classe object. Em termos do CLR (Cominou Langitage Runtime), um tipo anónimo não é
diferente de qualquer outro tipo de referência.
Uma expressão de consulta começa com a cláusula f rom e termina com uma cláusula
select ou group. A cláusula inicial f rom pode, opcionalmente, ser seguida por várias
cláusulas f rom, let e where. Cada cláusula f rom introduz uma ou mais variáveis de
iteração. Cada l et calcula um valor e introduz um identificador que representa esse
valor. Cada cláusula where é um filtro que exclui itens do resultado. A cláusula select
ou group pode ser precedida por urna cláusula orderby que especifica a ordem do
resultado. Por fim, a cláusula 1 nto pode ser usada para "ligar" consultas, tratando os
resultados de uma consulta como geradora de uma consulta posterior.
Tabela 7.1 — Palavras-chave que podem ser usadas numa expressão de consulta
Neste caso, serão impressos os inteiros de O a 5 sem que o seu tipo tivesse de ser
declarado.
O declarador tem que incluir um inicializador, logo, a variável local tem de ser
declarada e inicializada na mesma expressão;
A expressão de inicialização não pode fazer referência à sua própria variável. Isto
é, as variáveis declaradas não podem ser usadas na sua própria inicialização;
y ar aTurios = new[]
"Pedro Martins" ,'UÍ,
'• "Ana Cristina",- 16.j
"João Carvalho", 14
A primeira expressão origina um erro de compilação porque o inicializador não pode ser
um objecto ou uma colecção. É necessário introduzir a expressão new criando uma nova
instância do objecto. A segunda expressão é errada devido a não ser possível converter
implicitamente i nt em stri ng ou vice-versa.
<} .............. ". ..:_; .. . ............... _____ : - .'.;, • ....- . . . . _ ........ _--\e tipo de
/*
* Programa que ilustra inferência de tipos em expressões de consulta,
V
using System;
usi ng System.Collections.Generi c;
using system.Text;
using System.Linq;
class Aluno
public int id { get; set; }
public string Nome { get; set; }
public string Apelido { get; set; }
public int Idade { get; set; }
class ExemploCap7_l
return alunos;
PesquisarAluno(l);
PesquisarAluno(2);
Neste exemplo, declara-se urna classe Aluno que irá permitir armazenar instâncias de
pessoas tendo como campos: um identificador; o primeiro nome da pessoa; o último
nome da pessoa; e a sua idade. O método carregaAlunosQ retorna uma lista de alunos
que, neste caso, é definida estaticamente. Finalmente, o método PesquisaAlunoQ
permite imprimir um determinado aluno, identificando-o por id. A parte interessante
encontra-se exactamente neste método. Neste método, é definida uma expressão LINQ
que permite pesquisar e filtrar o aluno em que estamos interessados. Vejamos como:
var IHTiriôQuéry =~ ' " '«
from aluno In alunos :
where aluno.ld — i d
select_new { al.uno.Nome,, ai uno,. Apelido, aluno. Idade }; _ /
A expressão selecciona todos os elementos presentes em alunos (from aluno i n
alunos), colocando o resultado numa variável implícita aluno. De seguida, esses alunos
são filtrados por identificador, restando apenas os que tenham um valor idêntico ao que
foi passado como parâmetro do método (where ai uno. id==i d). Finalmente, é criado um
novo tipo de dados anónimo, que irá conter o nome, o apelido e a idade das pessoas em
causa(select new { a l u n o . N o m e , a l u n o . A p e l i d o , aluno.idade }).
Como nota final, para os leitores familiarizados com outras linguagens que suportam
inferência automática de tipos, é de referir que a palavra-chave var, não significa
"varianf. A técnica utilizada para inferência de tipos não é a mesma que é utilizada em
linguagens de scripting (VBScript, Perl) ou nos tipos de dados variant (COM), onde
urna variável pode conter diferentes tipos de valores, durante a sua vida útil no programa.
A palavra-chave var apenas tem a função de instruir o compilador que determine e
atribua o tipo mais adequado à variável definida durante a inicialização, sendo esta
atribuição estática.
var a l u n o s M a n a =
class ExemploCap7_2
{
static void Main(string[] args)
TabelaDinamica tab = new TabelaDinamicaQ ;
; //" "Rêtõ r na~~unTÍ Êri úmérãtò r~ q úêT "p è rrni ta pêrcor ré r à" cõTecçãõ
i lEnumerator GetEnumeratorQ;
Ou seja, qualquer classe que a implemente necessita de ter um método que retorne um
lEnumerator. Por sua vez, lEnumerator também é uma interface, sendo os objectos
desta utilizados para iterar ao longo da colecção em causa. lEnumerator está declarado
da seguinte forma:
jptíbTi" c "í nterfáce "lEnumerator" ~" " " " ~ "" "~" '
// Retorna o objecto corrente apontado pelo enumerador
object Current { get; } i
// Avança para o próximo objecto, retornando true. Caso não ;
// seja possível, retorna false ;
bool MoveNextQ; ;
// Coloca o enumerador no inicio da colecção l
void ResetQ ; l
._.,_. _ ...... .. . -_..._ _.._ .. , . _.,....
Isto é, para TabelaDinamica suportar lEnumerable tem de retornar um objecto que
implemente lEnumerator. Este objecto possui uma referência para o objecto original de
TabelaDinamica tendo métodos para o colocar a apontar para o primeiro objecto -
ResetQ; para avançar para o próximo objecto - MoveNextQ; e uma propriedade para
obter o valor correntemente apontado - Current.
Vamos, então, implementar uma classe Tabel aoi nami caEnumerator que permita
enumerar os objectos presentes numa TabelaDinamica. Tipicamente, chama-se a este
tipo de objectos enumeradores1. Para um enumerador funcionar correctamente, terá de
guardar duas informações: d) qual a tabela dinâmica a que se refere; b) qual o elemento
presentemente apontado. Para saber qual a tabela a que se refere, basta guardar uma
referência para a tabela em causa. Para referenciar o elemento apontado, e dado que cada
elemento possui sempre um índice associado, basta guardar um inteiro. Assim, a
implementação desta classe será:
Na edição anterior deste livro, chamámos itemdores a este tipo de objectos, visto ser esta a sua habitual
designação em português e, na verdade, ser consistente com a nomenclatura utilizada noutras linguagens de
programação. No entanto, a versão 2.0 da linguagem C# introduz um novo conceito chamado iteraíors
(iteradores). Assim, optámos por alterar a designação para "enumeradores", mantendo portanto a
consistência com a plataforma .NET.
19O © FCA - Editora de Informática
TÓPICOS AVANÇADOS
• public TãbelãbinamicáÊhum"e^
this.Tabela = tabela;
RésetQ;
return true;
get
if (Elemento<0 || Elemento>=Tabela.NúmeroElementos) ;
throw new invalidoperationExceptionQ ;
return Tabela.obtemElemento(Elemento);
} :
Existem alguns pormenores a ter em atenção nesta implementação. Em primeiro lugar,
quando um enumerador é construído, ou é feito o seu reset, deve ficar a apontar para
"antes" do primeiro elemento. Isto é, ainda não deverá referenciar um elemento válido.
Só após a primeira operação de MoveNextQ, é que deverá apontar para o primeiro
elemento. Isto deve-se ao facto de os enumeradores serem tipicamente utilizados com o
teste de fim de ciclo à cabeça. O código seguinte é bastante usual:
tabelaDinamicaÈriumeràtòr it = new TabelãbTnãmicáEhumératòr(tab)í;
while (it.MoveNextQ)
.. .console.WriteLineC{P}"j..ÂtíÇurrent) ; -
Outro requisito dos enumeradores é que a operação MoveNextQ retome true, caso tenha
sido bern sucedida a avançar para o próximo elemento. Caso já tenha chegado ao último
elemento, deverá retornar f ai se. Daí, o teste no início de MoveNextQ:
public bool MoveNextQ
; if (Elemento == Tabela. NúmeroElementos-1)
return false;
++El emento;
return true;
y
© FCA - Editora de Informática
C#3.5
não complicar o código. A plataforma .NET requer que seja lançada uma
invalIdoperatlonExceptlon, caso se tente utilizar o enumerador após a colecção ter
sido modificada. Isso pode ser conseguido de diversas formas, sendo uma delas, colocar
um "número de série" de alteração na classe da colecção original e guardar esse número
de série no enumerador quando este é criado. Sempre que existe um MoveNextQ, esse
número de série pode ser verificado.
Para concluir esta implementação, falta então colocar a classe Tabel aDinami ca a
implementar a interface system.Collections.iEnumerable:
•class TabéTãDTnanrica : System.ColTectlohs.lEnúhíerable' "~ "" •
/*
* Programa que Implementa uma tabela dinâmica simples,
* e o suporte para enumeradores da mesma.
*/
uslng System;
using System.Collections;
return Tabela[posicao] ;
}
/* Número total de elementos na tabela */
public int NúmeroElementos
get
return Total Elementos;
class ExemploCap7_3
7.4.2 ITERADORES
Como acabamos de ver, implementar um enumerador pode ser uma tarefa não só
relativamente trabalhosa como também, em alguns casos, complicada. Um tipo de dados
enumerável tem de implementar a interface lEnumerabl e e, por conseguinte, o método
GetEnumeratorC). Simultaneamente, este método tem de retornar um objecto que
implemente a interface lEnumerator e os métodos: MoveNextO e ResetQ e a
propriedade current. Com a introdução da versão 2.0 da plataforma .NET, todo este
processo pode ser simplificado pela utilização de métodos Iteradores.
• System.Collecti ons.lenumerator;
System.Collecti ons.lenumerable.
Sempre que yield return é chamado, é retornado o valor indicado. Quando o método
em causa for novamente executado, o mesmo começa a executar, não do início do código
mas a partir do local do último yield. A fim de tornar as coisas mais claras, vejamos um
pequeno exemplo.
Na secção anterior, verificámos que a classe Tabel aoi nami ca tinha de implementar a
interface lEnumerable, criando um novo objecto TabelaDinamicaEnumerator no
método GetEnumeratorQ:
cTãss~'Tãbê1aDfri~ãifíicá~f"
Por sua vez, a implementação de Tabel aoi nami caEnumerator era complexa. No entanto,
utilizando um iterador, não é necessário criar uma nova instância da classe
TabelaDinamicaEnumerator e} na verdade, nem sequer é necessário existir essa classe.
Tudo o que é necessário é escrever o seguinte código em Tabel aoi nami ca:
ípubTic lÈhumeratõr GetEnuíneratorQ "" "" " " " " "'
; for (int i = 0; i < Total Elementos; i++)
'• yield return Tabel a [i]; ;
O chamado sfackframe, que indica em que ponto do código é que se está a executar e que chamadas é que
se encontram de momento pendentes.
Em informática, este tipo de métodos chama-se co~rotinas.
196 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
return Tabela[posicão];
}
/* Retorna um enumerador para esta colecção */
public lEnumerator GetEnumeratorQ
for (int i = 0 ; i < Total Elementos; i++)
yield return Tabela [i];
class ExemploCap7_4
public static void Main(string[] args)
Tabelaoinamica tab = new TabelaDinamicaQ ;
for (int i=0; i<20; i++)
tab.Adi ci onaElemento(i);
foreach (int elemento i n tab)
Console.WriteLine("{0}", elemento);
De forma a suportar diferentes tipos de iteração, uma classe pode possuir várias
propriedades, cada uma delas retornando um enumerador diferente. Esses "enumeradores
genéricos" são métodos que retornam um objecto que implementa lEnumerable (na
verdade, System.Collections.lEnumerable). Embora o programador o possa construir
manualmente, caso utilize iteradores, o compilador encarrega-se de gerar o código
apropriado.
"7* Reto rnã únTeriúmé radõ r "para" és~ta"~cóTècçãò", f i m-->p rThcí pi o */"
public lEnumerable DoFimParaPrincipio
get
for ("int i=TotalElementos-l; i>=0; 1—)
yield return Tabela [i];
J. ___
y
Neste caso, o código de iteração com foreach fica:
foreach O"nt elemento Th" tab.õoPnncipioParaFfm)
e
.foreach" (int ~e1errie"ritò~Trí tàb".DoFimParapTfncipiõy ~"
í _ console.writeL|neC"{0}", elemento^; _.
Relativamente a esta funcionalidade, a necessidade de possuir propriedades diferentes e não
métodos simples, surge da classe implementar lEnumerable. No entanto, caso o
programador não necessite de ver a classe, como um todo, como sendo enumerãvel, então,
poderá não implementar a interface lEnumerable. Nesse caso, em vez de propriedades
que retornam enumeradores, poderá definir métodos que o façam. A sua utilização no
foreach é semelhante, mas em vez de se colocar o nome da propriedade, coloca-se a
chamada ao método. Por exemplo, caso DoPrincipioParaFim fosse um método, a
chamada do foreach seria:
foreach Ciht elemento irí tã^DòPrincfpIõParãFTmCJ)
; çonspl.e.wnte.LijieX"ÍQ.3iIlj .elemento}-! - --
A utilização destes enumeradores genéricos é muito interessante quando associada a
métodos "normais" que geram valores. Considere-se o exemplo da listagem 7.5.
y* -
* Programa que calcula quadrados perfeitos entre l e 100
* usando iteradores.
*/
using System;
uslnq System.Collections;
© FCA - Editora de Informática 1 99
C#3.5
class ExemploCap7_5
/* Retorna os quadrados perfeitos entre os valores <a> e <b> */
static lEnumerable QuadradosPerfeitos(int a, int b)
{
int num = a;
while (num <= b)
{
int raiz_inteira = (int) Math.Sqrt(num);
if (raiz_inteira * raiz_inteira — num)
yield return num;
" Quando yield return é chamado, é guardado o valor das variáveis locais,
assim como a posição do código em que a chamada ocorreu. Quando o
iterador é novamente chamado, a execução continua a partir da linha onde o
código havia anteriormente ficado.
~ E possível ter vários enumeradores numa classe. No entanto, caso a classe
implemente lEnumerable, os diferentes enumeradores têm de ser nomeados,
usando propriedades públicas. Caso a classe, como um todo, não seja
enumerável, basta utilizar métodos que retornem lEnumerabl e.
200 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
7.5 GENÉRICOS
Um dos principais problemas que se coloca com a utilização da classe Tabel aol nanri ca é
que esta pode armazenar elementos de qualquer tipo4. Se relembrarmos a definição da
mesma, verificamos que os elementos são internamente armazenados numa tabela de
referências object e, o colocar e retirar elementos da mesma corresponde sempre a tipos
object.
p"rívate object[]"Tabélã;"
public vold AdicionaElementoÇobject elemento) {
publ1_c. object_ QbtemEj_ejnejito_Clnt_pos1capJ)__{_... ,_.J
Poder armazenar quaisquer tipos de dados pode parecer uma vantagem. Mas, na
esmagadora maioria das vezes, o que o programador deseja é armazenar objectos apenas
de um tipo. Como a tabela tem de lidar com objectos genéricos, isto implica que seja
necessário fazer conversões explícitas para o tipo de dados que está a ser utilizado. Ou
seja, se pensarmos que vamos ter uma tabela dinâmica para armazenar valores inteiros, é
sempre necessário converter os objectos retornados em inteiros:
TabeTaDinartrica tab" = riew TabeTaDlnanricaO l ~
:tab.Adi ci onaElemento(10);
Para responder a estes problemas, na versão 2.0 da linguagem C#, foi introduzido o
conceito de genéricos2. Os genéricos permitem parametrizar classes, estruturas,
4 À partida, isto pode parecer um contra-senso, uma vez que armazenar qualquer tipo de objectos parece ser
uma mais-valia. No entanto, como iremos ver rapidamente, essa mais-valia não é sem custos.
5 O conceito de genéricos também existe em Java, a partir da versão J2SE 5.0, e em C-H-, sob o nome de
templates.
© FCA - Editora de Informática 2O l
C#3.5 _
Para indicar que uma classe (método, estrutura, etc.) é parametrizada, basta colocar entre
um sinal de maior e menor (<>) um nome, que irá representar um tipo de dados usado. No
nosso exemplo, chamámos-lhe T. A partir desse momento, T representa um tipo de dados
que pode ser manipulado ao longo da classe. Neste caso, é o tipo de dados subjacente à
tabela presente na classe, aos elementos que são adicionados em Adi ci onaEl emento() e
aos elementos retornados por obtemEl emento C). No caso de Tabelaoinamica, não são
necessárias mais alterações ao código do que as indicadas acima: as substituições de
object por T.
Para criar uma tabela dinâmica que armazena inteiros, basta fazer:
íTab_^aDinam1ca<lnt>"jtab/'=/.new Tabel ab"inaJicá<Tnt>().; 77 ." ","'„ ™ .T :
Como se pode ver, na altura em que a referência tab é definida, é necessário indicar o
tipo concreto, i nt, que a mesma irá armazenar. Também é necessário indicá-lo quando a
mesma é instanciada: new TabelaDinamica<-int>(). A partir deste momento, pode-se
adicionar e retirar elementos directamente da tabela, sem utilizar conversões explícitas.
É como se a classe sempre tivesse sido declarada como tendo a palavra "int" nos locais
onde tem "T". Por exemplo, pode escrever-se:
;TãbéTaDiriamica<int> tab = nèw"TãbelaDihamica<iht>O ; " ~
;tab.AdicionaElemento(10); ;
;tab. Adi ci onaEl emento (70) ;
nrvt a = tab. obtemEl emento (0) ; // ok
int b =_ tab. ObtemEl emento (l).; // .pk
Uma outra questão importante na definição de tipos genéricos é que, normalmente, estes
necessitam de fazer mais do que simplesmente armazenar valores. Em particular,
necessitam de invocar métodos sobre os objectos dos tipos genéricos que lhe são
passados. Suponhamos, que queremos que Par tenha um método imprime C) que imprime
o conteúdo do par, chamando imprime() em primei ro e segundo. Mesmo admitindo que
estes implementam llmprimi vel :
interface IlmprímiveV ~" ......... ........ ...... " " ~ ,
:{ '
void ImprimeQ ;
public T Primeiro;
public K Segundo;
O problema é que o compilador não tem forma de garantir que o tipo T (e também K)
possui efectivamente um método imprime Q. Pior do que isso, não é possível ao
compilador saber se imprime Q está a ser chamado com os parâmetros correctos e, caso o
valor de retorno esteja a ser utilizado, se este está de acordo com a utilização que o
programador lhe está a dar.
Para resolver este problema, ao definir-se tipos genéricos, é possível indicar restrições.
Uma restrição é algo que limita os tipos parametrizados, dando algumas garantias ao
compilador e, na verdade, ao programador, sobre a forma como o genérico vai ser
utilizado. Para tal, utiliza-se a palavra-chave where, seguida do conjunto de restrições que
se aplicam. No exemplo acima, para garantir que T e K implementam llmprimivel
escreve-se:
'stfúct Par<T,K> "
[~ "where' T : límprimiveT
[ where.J j__llmprimiyej
public T Primeiro;
public K Segundo;
istr.uct Par<T,K>
l where t :' Empregado'. "IComparabieV ícTonéãble
Na mesma linha de acção, é possível requerer que um certo tipo parametrizado possua um
construtor por omissão, público, sem argumentos. Para tal, utiliza-se a notação newQ.
Isto permite ao programador, ter a certeza de que consegue criar objectos do tipo em
causa. No nosso exemplo, para se garantir que era possível criar objectos do tipo T, sendo
este do tipo Empregado ou derivado, escrever-se-ia:
struct Par<T, K>
i where T : Empregado. nèwQ ' "_ l
Finalmente, o último tipo de restrição que se pode indicar é se um determinado tipo
parametrizado tem de ser do tipo "referência" (e. g., classe, interface, delegate) ou do tipo
"valor" (e. g,, estrutura, i nt, doubl e). No primeiro caso} usa-se a palavra-chave cl ass:
struct Par<T,K>
[ 'where T : class l
No segundo caso, utiliza-se a palavra-chave struct:
struct Par<T,K>
.... where T : struct _. _ . . ...
Note-se que o tipo não tem obrigatoriamente de ser uma estrutura, tem simplesmente de
ser um tipo valor (e. g., i nt).
Devemos realçar que se deve utilizar as restrições com bastante cautela. E certo que estas
permitem ao programador, ter um maior controlo sobre os tipos de dados que está a
utilizar. Mas ao introduzir-se restrições, diminui-se a utilidade das classes como um todo,
pois serão reutilizáveis em menos situações. Como exemplo, ao impor que T e K
implementem llmp ri mi vel em:
struct Par<T,K>
where T : Ilmprimivel
where K : llmprimivel ...... - .... .. .. . . .
implica que:
Par<string 3 int> pessoa = new_ F 3 ar<stn"ng 1 int>C"Vitor" J 27);. //.Erro!
não seja válido, pois nem st ri ng nem i nt implementam essa interface.
Um aspecto muito curioso deste tipo de métodos é que, na maior parte das vezes, o
compilador consegue inferir automaticamente os tipos de dados que estão a ser utilizados.
Ou seja, em vez de se escrever:
TabeTaDi nánrí ca<i nt> tab = .ObtemTabel aDi nVmf cã<j7it>ItãbTl a) ; ~ ~"_ _" l i
basta escrever:
;TabeTaDTriamTca<int>~_tab = oBtérhTabelabinámicaCtabelã).;" '._" "
Tal é possível, pois, desde que não seja necessário fazer conversão entre tipos, em que
várias conversões são compatíveis, o CLR consegue olhar para o tipo de dados de
tabel a, verificar que este é i nt [] e associá-lo a T[] , presente na assinatura do método:
:stat1c Tabè1apinanrica<T> ObtemTabel aDinahriVa<f>JT[] yãTòres}"_ ."__*! ". L.'.'...
A listagem 7.6 apresenta o exemplo completo.
O que acontece é que a tabela é parametrizada com o tipo T. T tanto pode ser um tipo
referência como um tipo valor. Caso seja um tipo referência, terá de ser colocado a null.
Caso seja um tipo valor, terá de ser colocado a O (ou 0.0, caso seja double). Ou seja, é
necessário ser possível descobrir qual é o valor por omissão de um certo tipo de dados.
Isso consegue-se, usando a palavra-chave default. default(T) retorna o valor por
omissão do tipo de dados T.
Como nota final, devemos alertar o leitor para o facto de que programação usando
genéricos é um tema bastante vasto e, em bastantes casos, complexo. Por exemplo, por
vezes, é necessário definir conversões entre tipos genéricos, criar classes derivadas de
classes genéricas, redefinir virtualmente métodos em classes genéricas e por aí adiante.
Nesta secção, abordámos apenas os aspectos mais pragmáticos e usuais da programação
utilizando genéricos. Resta, ainda, referir que é possível parametrizar interfaces,
6 Este delegate poderia ser usado para, por exemplo, calcular a média dos valores presentes numa tabela.
© FCA - Editora de Informática 2O9
C#3.5
Existe uma classe de números, chamados números complexos, que possuem aplicações
em diversos problemas do mundo real. Uma forma de encarar esses números é como
sendo vectores que possuem duas componentes: uma "parte real" e uma "parte
imaginária". Cada uma das suas partes é, por sua vez, um número real.
O construtor da classe leva dois parâmetros, que representam a parte real e a parte
imaginária do número. Existe ainda uma propriedade que calcula o módulo do número7.
Finalmente, também é feito o override do método TostríngQ, para que seja possível
utilizar números complexos em Console. writeLineQ.
Para somar dois números complexos, basta somar as suas componentes: parte real com
parte real, parte imaginária com parte imaginária. Para implementar o operador soma (+),
basta declarar um método da seguinte forma:
class-, Complexo " " . ; :
Quando o código está a ser compilado e o compilador encontra uma expressão que
envolve um operador sobre estruturas ou classes definidas pelo utilizador, o compilador
verifica, nos tipos de dados envolvidos, se o operador possui uma assinatura que seja
aplicável nessa situação. Se sim, utiliza o método correspondente. Se não encontrar
nenhuma aplicável, então, trata-se de um erro de compilação. A procura dos operadores é
feita de acordo com a ordem dos tipos de dados em causa.
Note-se que o operador soma está a devolver um novo número complexo. Este é o
comportamento que seria de esperar, uma vez que a soma não interfere com os elementos
que estão a ser somados. Apenas resulta num novo número.
Vamos alargar o exemplo um pouco mais. Os números que normalmente utilizamos são
números reais puros. Assim, para somar um número real a um número complexo, basta
adicionar esse número à parte real do número complexo. Ou seja, ao escrever:
!C"òrapTexo"coriip:=" new Complexo (2, 3)T ~ - - -.
com.pl exo_ resultado = 2.0 + çotnp; . ..... ..... :
7 O módulo de um número complexo é dado pela raiz quadrada da soma dos quadrados dos seus
componentes. Este valor representa a magnitude do número complexo.
© FCA - Editora de Informática 21 1
C#3.5
É fácil perceber porque é que o compilador não pode realizar automaticamente a operação de soma
indicada. Existem operações não comutativas: por exemplo 5/4 é diferente de 4/5. Urna vez que se estão a
definir operações sobre típos de dados definidos pelo programador, tem de ser o programador a indicar
explicitamente o que é ou não permitido e como é que ta! é realizado.
212 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
Outro ponto fundamental é que uma declaração de redefinição de um operador tem. de ser
declarada dentro da classe correspondente. Um dos parâmetros de entrada do operador
definido tem de ser obrigatoriamente referência para um objecto dessa classe. Por
exemplo, no caso do operador soma na classe Compl exo, um dos parâmetros do operador
é sempre um Compl exo. Isto permite ao compilador encontrar os operadores adequados.
Falta, ainda, mencionar que não é possível fazer a redefinição de todos os operadores da
linguagem, existindo, também, algumas restrições ao realizar a redefinição de alguns
destes. A tabela seguinte resume essa informação.
f Tipo _~_ Í l OPERADORES :[ RESTRIÇÕES ~_ j1
l Binários de aritmética ;| + - A / % 'l Nenhuma ';
Unários de aritmética . + ~ ++ — ; Nenhuma
Binários de operações sobre bits \ | A << » \a
| Unários de operações sobre bits"j|_.A _~. ~~j| Nenhuma_ \\ "Comparaçã
No caso dos operadores de comparação, é necessário modificá-los sempre aos pares. Por
exemplo, caso se modifique o operador ==, é obrigatório que também se modifique o
operador !=. Caso isso não aconteça, é gerado um erro de compilação. Isto garante que a
semântica de utilização dos operadores de comparação é completa. Ou seja, é possível
fazer uma comparação por igualdade ou por diferença.
} *""
Vejamos agora a seguinte situação. Em certas circunstâncias, pode ser desejáve.1 converter
explicitamente um número complexo num doubl e:
;double valor. = CcómplexçO còtnp; "Y . . ". . Y.Y . -. f
No entanto, esta conversão só é válida caso o número complexo tenha uma parte
imaginária igual a 0. Uma conversão explícita representa uma forma de o programador
dizer ao compilador, que sabe que pode existir alguma perda de precisão na conversão ou,
mesmo, que esta pode não ser válida. (Em princípio o programador sabe o que está a
fazer!).
Note-se que, neste caso, optámos por lançar uma excepção aritmética, caso a conversão
não seja possível. Isto porque, em qualquer situação, é um erro grosseiro encarar um
número complexo como sendo apenas a sua parte real9.
Não é possível definir conversões entre duas classes, em que uma é directa ou
indirectamente derivada da outra.
9 Repare-se que a decisão de lançar uma excepção depende muito das circunstâncias e da classe em causa.
Por exemplo, nunca é lançada nenhuma excepção quando é feito um cast de doubl e para i nt, mesmo que
o número contenha uma parte fraccionaria: é simplesmente feita a truncatura do número, perdendo-se
alguma precisão.
© TCA - Editora de Informática 2.15
C#3.5
A obj = objB; i
ÍB novo B = CH_obj.i // Conversão ...explicita __ .... __ j
é algo de base da linguagem, sendo a compatibilidade entre tipos verificada pelo CLR.
Para defínir uma conversão entre duas classes não relacionadas, basta colocar a definição
da conversão numa delas. Por exemplo, se tivermos as classes cl asseA e cl asses:
cTãssf cTásseA™ "" " " " .
j}"- i
!
jclass ClasseB
> '"
} "'
" No caso de os tipos de dados em causa serem classes, então, aplica-se duas
regras: a) uma das classes não pode derivar directa ou indirectamente da
outra; £>) a definição da conversão tem de estar dentro do corpo de uma das
classes, não importa qual, mas apenas numa delas.
i f (nurnjyjíput-nizador != -1)
: conso1a.'WnteLineC"Bem vindo, utilizador _{p} numerputili.zador) ;
O valor -l é utilizado para distinguir se o número de utilizador do sistema já foi
introduzido ou não.
Mas corno é que é possível distinguir a situação em que, efectivamente, o valor retornado
por um método é válido, de uma situação em que não foi possível ler um valor? Por
exemplo, no caso em que o utilizador introduziu uma letra? Uma solução comum, não
muito correcta, con-esponde em retornar um valor que, no caso particular da variável em
causa, não seja utilizado. No exemplo acima, se o número for sempre positivo, a rotina
poderá retomar -l, sinalizando que houve um erro na introdução de dados. Mais
correctamente, para resolver este problema, dever-se-ia lançar uma excepção.
Neste exemplo, o problema é facilmente resolvido com uma excepção ou com um valor
de retorno que não é utilizado. No entanto, existem muitas situações em que não é.
É comum, nos sistemas de base de dados, haver informação que não se encontra presente,
embora devesse estar. Em resposta a um pedido de informação, por exemplo, qual o
número de bilhete de identidade de uma pessoa com um certo nome, a base de dados
poderá responder com o dado ou, então, com uma indicação de que o mesmo não se
encontra presente (NULL). Isto não corresponde a uma excepção, mas a uma indicação
de que um campo ainda não foi preenchido.
A partir da versão 2.0 da linguagem C#, existe suporte para tipos cujo valor pode ainda
não estar definido: os chamados "tipos anuláveis" (nullable types). Os tipos anuláveis têm
de ser obrigatoriamente tipos valor (value types), isto é: tipos elementares (Int, double,
etc.), estruturas ou enumerações. Para construir um tipo anulável, basta acrescentar um
ponto de interrogação (?) à definição da variável que irá armazenar o valor. Por exemplo:
;int? Valo;""" " "...'"/ '..'.. .7. . ._ . " , _„. ~I\ ... '•
Neste caso, a variável ralo pode assumir como valores null ou um inteiro. Para testar se
uma variável possui um valor atribuído, utiliza-se a propriedade Hasval ue:
Tloúble?" raio; " "
;if (raio.HasValue)
' Console.WriteLine("0 valor do ralo é: {0}", ralo); :
;else
'_. Console. WriteLlne("Ra1o ainda não atribuído");__
Para colocar um valor numa variável anulável, basta fazer a respectiva atribuição. No
entanto, uma vez que uma variável anulável pode não conter um valor, para atribuir uma
destas variáveis a uma variável normal, é necessário realizar uma conversão explícita:
jdbuble?'Talo;" "~ - -- - ..-..-... .„.._. ..
!doub1e guarda;
-ralo = 10; // ok
iguarda =,(double) ralo; , _ _ _ / / O k ; . conversão explícita ;
Quando se tenta aceder a uma variável anulável, se a mesma ainda não possui um valor, é
lançada uma excepção. No último exemplo apresentado, caso ralo não tivesse sido
atribuído, iria ocorrer urna system.invalIdoperatlonExceptíon, assinalando que a
variável ainda não continha um valor.
!area = Math.Pl*ra1o*ra1p;
Neste caso, mesmo que ralo esteja a n u l l , o cálculo de área não irá resultar numa
excepção. Simplesmente, o valor null é propagado para área, ficando esta variável
também a nul l. De facto, o que está a acontecer é aproximadamente equivalente a:
218 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
Este operador chama-se "de aderência a nulo" porque o seu efeito é remover o "?" do tipo
de dados, fornecendo, como resultado, um certo valor por omissão. Por exemplo, em:
^ . RÃtO_NORMÃL = 10.0;
eÇ' râi"o_uti l i zado r ;
7.8 PONTEIROS
Um ponteiro é uma variável que representa um endereço de memória. Enquanto em Java
não é possível utilizar ponteiros, em C# estes estão disponíveis, assim como a sua
associada "aritmética de ponteiros". No entanto, em C#, estes são utilizados muito
raramente. Isto deve-se ao facto de o uso de referências e o operador new permitirem
manipular objectos de uma forma muito fácil e, também, fazer a sua gestão em termos de
memória. Na verdade, as poucas motivações que existem para utilizar ponteiros nesta
linguagem são obter interoperabilidade com código legado, escrito na era pré-.NET, e,
potencialmente, obter maior desempenho em certas operações que envolvam manipulação
muito intensiva de dados em memória.
Para realçar o perigo de uso de ponteiros, a linguagem C# obriga a que qualquer código
que os utilize seja marcado com a palavra-chave unsafe. Assim, para ter um bloco de
código que utilize ponteiros, é necessário utilizar a seguinte sintaxe:
220 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
iunsafe
{
: // código que utiliza ponteiros
Finalmente, ainda para realçar o facto do uso de ponteiros não ser seguro, código que
contenha blocos unsaf e só compila correctamente, se for utilizada a opção de compilação
/unsafe.
7.8.1 SINTAXE
Vejamos os aspectos básicos da sintaxe de ponteiros. Para declarar um ponteiro,
coloca-se o tipo da variável seguida de asterisco (*) e o seu nome. Por exemplo:
;."int* vaiar; '".',_._ l Jl L77..<yalor>_é.um pontei rp para W Inteiro"
Neste caso, a variável valor representa um ponteiro (ou endereço de memória) de uma
variável do tipo inteiro''.
11 Os programadores de C/C-H- devera ter atenção, pois a sintaxe da linguagem é diferente, no que diz
respeito a declarações múltiplas. Em "int* x , y;", as variáveis x e y representam ambas um ponteiro
para inteiro.
© FCA - Editora de Informática 231
C#3.5
jurisafe "
K
| int x = 10;
i int y;
j int* ptr;
! ptr = &x; // valor aponta para a variável x
j y = *ptr; // y fica com o valor 10
Neste exemplo, ao fazer-se ptr=&x ; , a variável ptr irá ficar com o endereço de memória
da variável x. Diz-se que ptr "aponta para x". Para todos os efeitos, escrever *ptr é
equivalente a escrever simplesmente x. *ptr representa o "valor apontado por ptr", isto
é, x. Portanto, ao escrever-se y=*ptr;, isto corresponde a colocar na variável y, o valor
da variável apontada por ptr, fazendo com que o valor corrente de x seja atribuído a y.
E importante notar que *ptr pode ser usado, tanto do lado esquerdo de uma expressão,
como do lado direito. Por exemplo:
Assim como se pode declarar um ponteiro para um tipo elementar, também é possível
declarar um ponteiro para uma estrutura:
istruct Tonto ....... - "- --------- — ,
;{ public int x;
; public int y;
iclass Teste
!{
i public unsafe void FQ
LT
i
Embora seja perfeitamente válido utilizar o operador * para aceder ao elemento apontado,
alterando o seu valor:
Pohtcfp" =~new "Po n to Q";" " ~ ' -
ptr~>x = 10; " " '//" p "."x fica com o"~valor 10"
:ptr->y = 20; U^ p.y _fica_cpm q.yalo.r.20.
Neste exemplo, foi utilizado um ponteiro para uma estrutura. Em C#, não é possível
declarar ponteiros para objectos de classes. Apenas é possível declarar ponteiros para
tipos elementares e estruturas, isto é, elementos que residem no stack e que não mudam
de localização de memória. Tudo o que são elementos que residem no heap, como sejam
os objectos e as tabelas, são geridos pelo CLR, podendo mudar de localização ou, mesmo,
ser reclamados pelo garbage collector. Para esse tipo de elementos (chamados managed
types) não se pode declarar ponteiros, pois a sua localização de memória pode alterar-se
ou ficar inválida.
O código é simples de entender. Existe apenas um ciclo que itera total número de vezes,
copiando o valor apontado pelo ponteiro de origem, para o local apontado pelo ponteiro
de destino. Após a cópia de cada valor, ambos os ponteiros são incrementados de um:
++destino; - - ,l
-H-origem; . _ ...... ........ ..... - — ----- — - ------ ;
Esta é a parte interessante. Por ura lado, é possível incrementar una ponteiro ou somar-lhe
ou subtrair-lhe um valor qualquer. Ao mesmo tempo, ao somar um ao ponteiro, este não é
colocado a apontar para o byte seguinte! É sim colocado a apontar para o elemento
seguinte. Por exemplo, neste caso, origem é um ponteiro para um double. Um double
ocupa 8 bytes. Assim, ao fazer ++origem, o ponteiro é incrementado de 8 bytes.
O compilador trata de gerar o código correcto, de acordo com o tipo de dados em causa.
O código é igualmente válido, sem alterações, para a cópia de inteiros, que só ocupam
4 bytes'.
public static unsãfé " "' " " ' . . . n^ •
void Copi.aRapidaCint* origem, int* destino, int total; - \{
LL. .....: :•
Muitas vezes, associado à aritmética de ponteiros, é utilizado o operador sizeof. Este
operador permite obter, em bytes, o tamanho que uma certa estrutura ou tipo elementar
ocupa em memória. Por exemplo, ao fazer-se:
.cplisõnéVWrTíeUine.C11^^" dpubTe p^cupay£Oy^bytèsllVLsizegfrCãõuble)3j
irá surgir no ecrã, que um double ocupa 8 bytes. O operador sizeof é utilizado, colocando
sempre entre parêntesis, o tipo de dados do qual se quer obter o tamanho.
12 Note-se que o mesrno já não se aplica a ponteiros para estruturas. Apenas para tipos elementares como
byte, int, double.etc.
2.2.4 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
Suponhamos, que queremos obter uma tabela de vinte inteiros, obtida no stack, para uso,
por intermédio de ponteiros. Para isso, escrevemos:
_ tabela = ^stackãTTpc "i_ntl?0] ; . _ J \~. . ".1_7 V T T "~"T """.".. I .". .'".'
O operador stackal 1 oc cria, no stack, espaço para os 20 inteiros, retornando um ponteiro
do tipo correcto. A partir deste momento, podemos utilizar o ponteiro como se de uma
tabela se tratasse:
const. Int TAMANHO = 20;
int* tàbei-a = stackalloc int[TAMANHO];
for (Iní'l=0í n<TAMANHO;
_tabela[i] = 1; _
Note-se que quando o fluxo de execução sai do bloco onde foi realizado o stackal loc, a
memória é automaticamente reclamada, uma vez que o stack associado ao bloco é limpo.
Este tipo de construção é muito útil quando se pretende ter um buffer de memória rápido,
sem o peso de ter um objecto completo no heap.
Tal como já vimos, em. C#, não é possível declarar ponteiros para objectos. No entanto,
consideremos o seguinte código:
Ponto p = new PòntoCS, 10);
i;nt* ptr =_&p.x; _ . . _ _ ...
em que a classe ponto está definida da seguinte forma:
class iponto
{
publlc -jnt x;
pubVic int y;
• putf1i*c pontoCint x, Int y)
thris.x = x; _. . .„
13 Na verdade, não deveríamos chamar a estes elementos tabelas, pois não são vistas como objectos, nem é
possível aplicar-lhes métodos ou obter o valor de propriedades, como Length.
A forma mais correcta de ver este tipo de elementos/tabelas é como bnffers estáticos de memória obtida do
stack, de um certo tipo. O equivalente a um mal loc C) com um cast do C/C-H-, mas em que a memória
provém do stack,
© FCA - Editora de Informática 225
C# 3.5
Para conseguir declarar um ponteiro para um campo de uma classe, é necessário utilizar
um bloco especial, que garante que o garbage collector não desloca o objecto em
memória. Estes blocos chamam-se fixed e estão sempre associados à declaração de um
ponteiro:
ponto" p ="new"Pohto(5, 10);
Caso seja necessário aceder a mais do que uma variável, é possível separar as declarações
por vírgulas, desde que as variáveis sejam do mesmo tipo, ou colocar várias instruções
fixed seguidas. Isto é:
jfTxecT (irít* ptrx = Sp.x, "ptrY~ = Sp'.yD
são equivalentes.
Para concluir esta discussão, falta ainda referir, que é possível declarar variáveis de
instância em classes que são elas próprias ponteiros. No entanto, nesse caso, ou a classe é
declarada como unsaf e ou a própria variável tem de ter esse modificador:
icTass teste " ..... " " ..... "" "" " ' " ...... ™" " ...... " ........... "
|{
p n' vate unsafe Int* ptr;
O método Max C) é sem dúvida útil. No entanto, não podemos escrever expressões do tipo:
int maxl = Matemática".MaxC4, 6)"; " " - .. - ,
j.nt max2 = Matemática.Max(l,. 2, 3,._4); _.
Se desejarmos escrever estas expressões, teremos de criar duas novas versões do método
Max C): uma tendo quatro parâmetros de entrada e outra tendo dois. No entanto, do que
gostaríamos mesmo, seria poder colocar qualquer número de elementos como parâmetro,
encarregando-se o método de encontrar o máximo entre os valores passados. É aqui que
entra em jogo a palavra-chave params.
Ao mesmo tempo, também é possível passar directamente como parâmetro uma tabela,
funcionando o método da mesma forma. Isto é, o seguinte código continua a ser válido:
int[] tàb = t "l,'2, 3, 4 }T " ~ - - ...
. int.max.= Matematica^MaxCtab);
Na utilização desta funcionalidade da linguagem, existem as seguintes regras:
Isto quer dizer que é possível declarar vários parâmetros obrigatórios, sendo os
parâmetros finais do método opcionais. Vejamos como isto poderá ser útil. No nosso
exemplo anterior, se chamarmos o método MaxQ sem parâmetros, irá ser gerada uma
excepção:
int max = Matematica.MaxQ ; '// Excepção i . " 7 " . 7 ' ""
Isto, porque no corpo do método, tentamos aceder ao primeiro elemento, sem
verificarmos se este existe. Se quisermos obrigar a que o método Max C), ao ser chamado,
tenha pelo menos um elemento, podemos alterar o código para:
fpuBlic statlc int waxunt yaiorl, params int n p u t r p s v a i o r e s j ;
{ " " • " " " "
| :
Chamamos a atenção, para o leitor mais interessado, que é pelo uso desta funcionalidade
que é possível especificar um número variável de parâmetros quando se executa um
console.writeLineQ. Ao consultarmos a documentação da plataforma .NET, vemos
que uma das variantes do método Console.writeLineO está declarada da seguinte
forma:
•publi.c'. statiç ;yJi_a;WiteLi_nèrstri^^ ârg)_j
Ou seja, leva um número arbitrário de objectos como parâmetros, após a especificação do
formato da linha a imprimir. A declaração do formato é obrigatória.
Estes métodos estáticos especiais devem ser declarados dentro de uma classe estática,
sem nenhuma propriedade ou variável de instância. Ao criar-se um método estático cujo
primeiro parâmetro é precedido por this, está-se a indicar qual o tipo de dados ao qual o
método será "adicionado". Consideremos um situação em que gostaríamos de ter esta
funcionalidade.
Imaginemos que temos um formulário web onde um utilizador tem de introduzir o seu
nome de utilizador e a sua palavra-chave. Actualmente, a forma mais comum de um site
ser atacado é através do que se chama um "ataque de injecção de SQL". Basicamente, um
utilizador malicioso, em vez de introduzir, por exemplo, o seu nome de utilizador,
introduz uma cadeia de caracteres que é interpretada como código SQL. Caso a aplicação
web não esteja bem desenvolvida, esse código é executado pelo servidor. Vejamos como.
Imaginemos que na verificação do nome de utilizador, o servidor usa o seguinte código
SQL:
'jst s ©"SÈLECT;;* FRÒM usèrX.WHERÊ"'namè"="\>;;userN'ame>"
userName é do tipo string, representando, no nosso programa, a variável que guarda os
dados que vêm do formulário web. Se o utilizador malicioso introduzir como userName a
cadeia de caracteres:
i n i m i g o 1 or ' t r u é ' = ' true
o código SQL acima fica:
•st..= . TRÒM ;users; WHEREjiàmè =" /;ihimTgõ;' or ' true^true 1
como a última parte é sempre verdade ( ' t r u e ^ t r u e ' ) , a expressão é sempre avaliada
como verdadeira. Ou seja, o utilizador malicioso consegue usar um nome de utilizador
que não existe. Aplicando a mesma técnica à palavra-chave, seria possível entrar no site
sem qualquer tipo de autenticação.
Neste caso concreto, o que gostaríamos de poder fazer seria chamar um método
TornaSeguroQ sobre todas as stríng do nosso programa, que fossem usadas em código
SQL. Este método adicionaria uma barra para trás (\ a todos os caracteres especiais
encontrados, tornando-os seguros. Obviamente, seria pouco conveniente definir uma nova
classe derivada de string só para ter esta funcionalidade. A solução para o problema é
definir um método de extensão que será aplicado a stri ng:
l s t at te _c l ass st ri n^.Ex t e n s 1 p ns _ ___ J
púbTic stãtíc"st>iW(FTòrn~aS^ sj
r ••"stffng "result" = ""; " ~ ~ " ~~
(char ch i n s)
; -jf ((ch == ' \ ' ' ) M (crf-
; . • = result += '\ ;
1 result += ch; , . .
•} _ _ _. _ __ _ _ .... .. . : , _ . . _ . . ,
A partir deste momento, torna-se possível escrever:
string" userNameS_èj3uro~ =~\U5^ "~ . . .""".!'
Quando usado num parâmetro, a palavra-chave thi s permite ao compilador perceber que
se trata de um método de extensão. Um ponto importante é que os métodos de extensão
apenas ficam disponíveis no código, se estiverem definidos no espaço de nomes corrente,
ou se forem explicitamente importados, usando a directiva using. No caso de existirem
múltiplas classes, com o mesmo espaço de nomes, os métodos de extensão ficam
disponíveis como se pertencessem todos à mesma classe.
nas instâncias de métodos do próprio tipo e só depois, nos métodos de extensão. Isto
também permite tornar o sistema mais seguro, evitando que os programas sejam
potencialmente atacados pela redefinição de métodos já existentes.
Pelo facto de um contador de referências de um objecto chegar a O, não quer dizer que o
objecto seja imediatamente limpo. O que acontece é que o garbage collector apenas é
corrido de tempos a tempos, limpando todos os objectos pendentes de uma só vez.
O leitor deve de ter em atenção que esta explicação se encontra muito simplificada. Na verdade, para
realizar garbage collection não existe directamente um contador de referências. São necessários métodos
mais sofisticados, nomeadamente devido a implicações em termos de performance e para resolver
problemas como referências circulares de objectos.
232 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
Embora o garbage collector seja uma ajuda preciosa para o programador, uma vez que o
liberta da tarefa de gerir a memória, associados aos objectos, podem existir outros
recursos que têm de ser libertados quando já não são necessários. Exemplos típicos são:
ligações a bases de dados, ficheiros abertos e objectos gráficos.
Outro ponto muito importante é que o código de um destrutor deverá ser o mais breve
possível e rápido de executar. Como o garbage collector pode ser chamado a limpar um
grande número de objectos, chamar todos os destrutores dos objectos em causa pode
demorar bastante tempo, tendo sérias implicações na.performance da aplicação.
7.11.1 SlMTAXE
Um destrutor é declarado, utilizando o nome da classe, precedido de um til. Não possui
parâmetros, valor de retorno ou qualquer tipo de modificador. Por exemplo, na seguinte
classe:
class xpto
' p n" vate st n" n g Nomeobjecto;
public XptoCstring nome)
this.Nomeobjecto = nome;
CtfnsoTe/WrlteLine("Objecto <{0}> construído", Nomeobjecto);
15 Os programadores de C++ devem prestar especial cuidado aos destrutores da linguagem C#. Embora a
sintaxe seja similar, a semântica de utilização é muito diferente nesta linguagem.
16 Mais adiante, iremos ver que também existem convenções para esses métodos, assim como suporte na
linguagem para a sua utilização.
© FCA - Editora de Informática 233
C#3.5
1
0 destrutor faz algo muito simples. Apenas mostra, no ecrã, o nome do objecto que está a
ser eliminado. Se o código seguinte:
jcTãss Tesfe " "~ -..--. - .
K1 static void Main(str1ng[] args)
! Xpto objA = new Xpto("objA");
Concretizemos esta noção no seguinte exemplo. Temos duas classes: Base e Derivada,
em que Derivada herda de Base:
~Bãsé ""........."
:{
: ~Base()
j Console. WriteUne("~BaseO") ;
>}
;class Derivada : Base
{
: -DerivadaQ
Console.WriteL-ine("~DerivadaO") ;
J__________________......._ .....
234 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
surgirá no ecrã:
~DerivadaO '
«Base O _ ___ - - . ...... — - . ______ ... •
Ou seja, ao destruir o objecto em questão, o garbage collector tratou de destruir primeiro,
a parte correspondente à classe Derivada, chamando o seu destrutor, e só depois, a parte
correspondente à classe Base, chamando também o seu destrutor.
Falta ainda referir, que na plataforma .NET e nas linguagens associadas ao CLR, o
destrutor corresponde formalmente a um método virtual chamado Finalize C). Na
verdade, mesmo em C# todos os objectos possuem implicitamente este método, derivado
de systetn. ob j ect. No entanto, a linguagem obriga a que, para que seja considerado
destrutor, o método Finalize Q seja definido com a sintaxe especial atrás indicada:
utilizando um dl e o nome da classe. É portanto um erro de compilação, definir um
método com o nome Finalize Q e um destrutor. O leitor deverá ter em atenção, que na
documentação da plataforma .NET e do CLR, os destrutores são denominados Finalizers.
7. 1 1 .2 DISPOSE E CLOSE
Toda a discussão anterior está relacionada com o facto de haver recursos que não são
libertos automaticamente quando já não são necessários. Por exemplo, após abrir um
ficheiro e escrever para ele, o objecto que o representa pode continuar a existir durante
bastante tempo. No entanto, é conveniente fechar o ficheiro, de modo a que os buffers a
ele associados sejam limpos. Aqui, o ponto-chave é que existe urn método que permite
fechar o ficheiro, libertando, assim, tudo o que lhe está associado após este já não ser
necessário.
Esta discussão é válida, não só para ficheiros, mas para qualquer tipo de recurso. É neste
contexto que entram os métodos closeQ eoisposeQ.
Embora quando um programador define uma classe possa dar qualquer norne ao método
que liberta os recursos associados à mesma, na plataforma .NET é recomendado que este
se chame closeO ou DisposeQ.
Um método de nome cios e Q deverá ser associado a classes que representam recursos
em que existe a noção de "uma ligação". Por exemplo, uma ligação a um ficheiro ou a
uma base de dados. Quando o programador já não necessita dessa ligação, deverá chamar
closeQ sobre o objecto em causa.
Objectos que representem algo transitório, por exemplo, uma janela no ecrã que deverá
desaparecer quando já não é utilizada, deverão ter um método de limpeza chamado
DisposeQ. Quando o programador já não está interessado na entidade que o objecto
representa, deverá chamar este método.
Note-se bem, que estes são métodos para serem chamados pelo programador. Não são
métodos que o CLR execute automaticamente.
Vejamos um exemplo completo de como o c l ose C) 17 pode ser utilizado em conjunto com
o destrutor. Por exemplo, consideremos a classe Ligacaosaseoados, que representa uma
ligação a uma base de dados:
ipúbTic "clãs s LigacaoBaseDàdos - - - • • ..... - ...... - - ....... --••
'. p u b l i c LigacaoBaseDados(string nomeDaBaseoados)
: // Estabelece a ligação à base de dados
GC.SuppressFinalize(this);
FechaBaseDadosQ ;
-LigacaoBaseDàdosC)
FechaBaseoadosQ;
} _
Iremos falar apenas de Cl ose O, embora a discussão também se aplique a Di s pôs e C).
236 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
Nesta classe, ao criar-se um objecto, este liga-se imediatamente a uma base de dados. O
programador é suposto chamar o método closeQ quando já não necessita de aceder a
elementos da base de dados. Isto é, a utilização esperada é:
LigacaoBaseDadòs bd =~new LigacaoBaseDadosC"livros.bd")l
// Utiliza <bd> para aceder aos dados necessários
:bd.CloseQ;
FechaBaseoadosO é um método piivado, definido pelo programador, que corresponde ao
fecho real da ligação à base de dados. Este é um método auxiliar para uso interno da classe.
O código de dose Q é:
.GC.SuppressFinalizeCthis);
FechaBaseDadpsO_;_ __ __ . _ _ ... . . .
A primeira linha indica, ao garbage collector, que o destrutor do objecto em causa não
deverá ser executado quando o objecto deixar de existir. Esta linha é muito importante,
uma vez que a ligação à base de dados irá ser terminada no método closeQ. Como não
existem outras limpezas a realizar no objecto quando este é removido de memória, não
existe mais trabalho a realizar. Assim, não faz sentido correr o destrutor do objecto.
GC.suppressFinalize(this) instrui o garbage collector a não chamar o destrutor sobre a
instância em causa. A segunda linha do método cl ose O trata simplesmente de fechar a ligação.
Caso o programador não tenha chamado o método closeQ, então o destrutor é corrido
quando o garbage collector limpar o objecto. O destrutor chama simplesmente o método
FechaBaseDadosQ.
Note-se que um efeito semelhante pode ser obtido, utilizando uma variável lógica que
garanta que o método FechaBaseoadosQ apenas é corrido uma vez. Isto é, a definição
destes métodos poderia ser:
public class LigacaoBasébadõs
' p n" vate boolean CloseExecutado = false;
Em C#, existe uma sintaxe que permite garantir que o método Dispôs e C) de um objecto é
executado quando o objecto deixa de ser necessário. Note-se que apenas existe este
suporte para DisposeO: o método closeQ não está contemplado18. Note-se que
utilizando esta sintaxe especial, é garantido que o método DisposeO é executado. Isto
contrasta com o destrutor, em que não existe esse tipo de garantia.
Para ama classe suportar esta sintaxe, tem de implementar a interface System.
.iDlsposable. Esta interface apenas define o método DisposeO. Assim, por exemplo,
consideremos a classe cai xaDi ai ogo, que representa uma caixa de diálogo no ecrã:
;pubTic" cTãss CafxaDialogo : systèm.lDispósãble
p u b l i c void DisposeO ;
// Apaga a janela do ecrã, libertando os recursos associados
:> . _.._.. ._
Esta classe implementa a interface IDisposable, implementando o método DisposeO. O
programador é suposto escrever código semelhante a:
'Cai xàDiaTogcf "janela = néw CãixaDiálógoO; " "
.// utilização de janela para os mais diversos fins ;
18 Tal deve-se à forma como o Glose C) e o DisposeO devem ser utilizados pelos programadores. Glose C)
representa um "fecho de uma ligação", logo, deve ser algo que deve ser feito sempre explicitamente por
quem utiliza a classe. DisposeO representa o "libertar de um recurso". Tipicamente, o objecto é o recurso
em si, logo, ao deixar de ter o objecto definido, faz sentido que o recurso seja libertado automaticamente.
238 © FCA - Editora de Informática
Note-se que, tal como nos blocos fixed, é ainda possível definir várias variáveis na
mesma declaração using:
rusing (CãixaDialogd jãnelal = new CaTxabiãlogoO, "
janela2 = new càixaDiaTogoO) • ' ' '-" :
.}
using system;
class Teste
{
private int total ;
Neste exemplo, é definido, no início do ficheiro, o símbolo DEBUG. Isto quer dizer que o
programador ainda está a efectuar testes, não sendo entregue ao cliente, o código
compilado desta forma. Ao ler o ficheiro, o compilador coloca o símbolo DEBUG numa
tabela interna de símbolos definidos. Ao encontrar a directiva #if DEBUG, verifica se o
símbolo DEBUG está definido. Como está, continua a compilação das linhas seguintes até à
directiva #endnf, emitindo o código executável correspondente. Caso o símbolo não
estivesse definido, o compilador pura e simplesmente ignorava as linhas deste bloco.
Desta forma, o programador pode incluir no código fonte, código exclusivamente para
efeitos de depuração de erros, enquanto testa o programa. Quando esse código já não é
necessário e é necessário realizar a compilação final, basta-lhe comentar a linha #define
DEBUG e recompilar.
Os programadores de C/C++ devem ter em atenção que em C#, não são suportadas
macros e que o conjunto de directivas de pré-processamento é muito mais reduzido.
iclass prográmacalculo
Neste caso, o código é, compilado com a opção DEBUG, fazendo com que seja gerado
código executável para uma linha que imprime no ecrã que a versão em execução é de
debug. Existe, ainda, um método que testa se um conjunto de símbolos está definido. De
acordo com o símbolo definido, o código que irá ser gerado é o mais apropriado ao
processador em causa. Isto permite, ao fabricante do programa, criar versões optimizadas
para diferentes plataformas. Neste caso} o código está a ser compilado para Pentium 4.
É ainda de referir, que é possível aplicar operações lógicas entre símbolos, verificando se
vários estão ou não definidos. Por exemplo:
•#Tf DEBUG &&' ÍMPRIMIR_MENSÀGENS
:#endif
programador e não de acordo com o ficheiro modificado pela ferramenta que retirou as
instruções. A directiva #1 1 ne permite fazer esse tipo de acertos.
Para utilizar a directiva #line, indica-se qual a linha do código fonte corrente e
opcionalmente, o nome do ficheiro em causa. Por exemplo:
e 210 "teste. es" ~ ' . ""._" ./ . " _ ' " . " " " ". / . " . . . . " V " * . "
faz com que a linha seguinte à declaração passe a ter o número 210 e que o nome
reportado para o ficheiro seja "Teste.cs".
Note-se que a numeração das linhas seguintes à indicada também é modificada. Com esta
directiva, a partir da linha seguinte, as linhas passaram a ser 210, 211 e assim
sucessivamente. Para instruir o compilador a voltar à numeração normal faz-se:
; #]lne default _ '_" ' " __ ; ~ ...... '__ . ' ; '_ _
J • • ' . . . _ . .... . . . . . . . . . . . .
existirá uma região visualmente expansível, identificada com o nome "Declaração de
variáveis
7.13 DCX:UMENTAÇÃOEMXML
Até agora, sempre que foram utilizados comentários em C#, estes foram comentários de
fim de linha (//) ou blocos de comentários (/* V). No entanto, existe ainda uma forma
de comentários muito útil, que utiliza três barras (///).
A tabela seguinte descreve as tags mais importantes suportadas pelo compilador de C#,
assim como uma breve descrição de cada uma delas.
_TAG__ ' DESCRIÇÃO 1
<code> ^Marca diversas linhas de textç como sendo código. !
<exampl e> , Marca texto como sendo um exemplo. !
Tipicamente, é utilizada em conjunto com <code> j
<except1on> Documenta uma classe como sendo uma excepção. :
A sintaxe é verificada pelo compilador. j
<param> : Representa uma parâmetro de entrada de um método.
<remarks> i Comentários relativos à utilização do elemento em causa. ;
<returns> Indica_ o que é que um método devolve.
<see> Referência para outra ciasse, método ou elemento que deverá !
se£ consultado em conjunto com este. _ !
<summary> ' Apresenta um sumário do elemento em causa.
<value> Comenta uma propriedade. '
Nesta tabela, não são apresentados os pormenores de cada tag, nomeadamente os seus
parâmetros, devido à sua extensão. O leitor interessado deverá consultar a documentação
do MSDN para pormenores. No entanto, é de referir que o VisiialStudio.NET coloca
19 XML é uma "linguagem de marcação" de texto, semelhante ao HTML, que permite fazer processamento
do texto em causa. Por exemplo, enquanto as tags existente em HTML especificam o aspecto do texto (por
exemplo: <hl>Títul o</hl>), as tags, em XML, permitem especificar informação sobre o texlo em causa
(porexempío: <titu"lo>c# 2.0</titu1o>).
244 © FCA - Editora de Informática
TÓPICOS AVANÇADOS
<summary>
construtor da classe.
</summary>
<param name="nome"> O nome da pessoa </param>
<param name="idade"> Alidade., da .pessoa._<Zparam>_
publlc Émprègado(stn"ng nome, inf idade)
, . , <summary>
/// Calcula o ordenado do empregado de acordo com
/// o número de horas que trabalhou.
/// </summary>
/// <param name=IlhorasTrabalho"> Horas de trabalho </param>
J U <returns> Ordenado a receber_</rej£unis> ______
; public "int~Calcúlãòrdenado("int Horástrabãlho)
Access: Project
|í,-[.:^;;-.í;- ft^vl^"';''';- j
Construtor da dasse.
r 1 iíi
ÈÊlDone . 9 M V Computer .;
ARETER " Linhas que contêm três barras (///) representam um comentário de fim de
linha, contendo documentação XML.
Comentários ~ A opção /doe do compilador permite gerar um ficheiro XML com a
emXML documentação de urn ficheiro de código fonte. É ainda possível utilizar o
VisiialStudio.NET para gerar documentação HTML sobre o ficheiro em
causa.
~ As tags mais importantes são: <summary>, que apresenta um sumário do
elemento em causa; <param name=var>, que comenta a variável de entrada
var; <returns>, que comenta o valor de retorno de um método; e
<exampl e>, que apresenta um exemplo de utilização do elemento.
Neste capítulo, iremos examinar as principais classes base da plataforma .NET. Estas
classes são fundamentais para o desenvolvimento de aplicações nesta plataforma, sendo
omnipresentes em todo o código escrito.
8. l A CLASSE SYSTEM.OBJECT
A plataforma .NET possui um sistema universal de tipos, de nome Cominou Type System
(CTS). No CTS, estão definidos os tipos básicos para toda a plataforma .NET,
independentemente da linguagem usada, o que garante interoperabilidade na passagem de
dados. Uma característica importante do CTS é a existência de uma única raiz para todos
os tipos, a classe System, ob j ect. Esta estruturação, com uma única classe base para
todos os tipos, diverge da linguagem. C++, que não tem uma raiz única para as suas
classes. No entanto, é uma característica que há muito é defendida para as linguagens
orientadas aos objectos, pois permite tratar, de forma uniforme, todos os tipos de dados
existentes na linguagem.
Uma das vantagens obtida com a base única dos objectos é o facto de todos os objectos da
plataforma .NET terem uma funcionalidade básica associada, definida em System,
.object. Assim, para todos os objectos, existe uma interface conhecida em toda a
plataforma, que permite a execução de funcionalidades básicas, sem que se tenha de
conhecer o tipo real do objecto em questão.
Uma vez que System.object tem de representar o mínimo denominador comum entre
todas as classes da linguagem, definidas na plataforma ou pelo programador, a sua
funcionalidade é bastante diminuta. Na tabela 8.1, são apresentados os métodos existentes
em system. ob j ect.
MÉTODO ; DESCRIÇÃO
public static ;
bool Equal s (object a, object b) ; Faz a comparação entre dois objectos.
public virtual ! Compara um objecto que é passado por
bool Equals(object other) parâmetro com o objecto corrente.
public static bool i Compara a referência de dois objectos
ReferenceEqualsCobject a, object b) passados por parâmetro.
public virtual
int GetHashcodeQ Devolve um código hash do objecto.
MÉTODO DESCRIÇÃO
protected
object MermberwisedoneO Cria uma cópia do objecto.
protected
virtual void Finalize Q : Destrutor por omissão do objecto.
public Devolve informação sobre o tipo de
Type GetTypeQ dados do objecto.
public virtual Retorna uma cadeia de caracteres que
string TostringC) representado objecto em causa.
Note-se, que é possível chamar qualquer um destes métodos sobre qualquer objecto
existente. Dado que todos os objectos herdam de system.object, estes métodos estão
sempre disponíveis.
Í this.Nome = nomeDapessoa;
return Nome;
l. . _ "" " " " """ " " " " "
Ao fazer:
Empregado empl = new Empregado C"Pedro Bizarro")T
Empregado emp2 ~ new EmpregadoC"Ana uúlia");
;Console.WriteLineC"Primeiro empregado: {0}", empl);
icpnsole.WrjteLlne("Segundo,.empregado:_.{p}" t _ emp2J)_;_
25O © pCA - Editora de Informática
CLASSES BASE
surge no ecrã:
;príme1ro empregado: Pédrõ~l3iza.rTÒ
.Segundo empregado-: _Ana
TostnngO é muito simples de utilizar e muito útil. No entanto, caso o programador
necessite de controlar, de uma forma mais exacta, o modo como são mostradas as
formatações no ecrã, então, deverá implementar a interface iFormattable. Esta interface
será discutida mais tarde, ainda neste capítulo.
Suponhamos que temos uma classe, tal como a classe Empregado. Por omissão, sempre
que se utiliza o método EqualsQ, seja este estático ou de instância, a comparação é feita
por referência. Por exemplo, se tivermos o seguinte código:
Empregado..eqjpl = new~Emp"règàdb("Pedrõ Bizarro"]í; *#'<"'
EmpYecjVdo* emp2 = new Emprêgado("pedro Bizarro");
if £emp:U,Equals(enip2)) . ^ •
Consoler.writeLlneC"etnpl é o mesmo que o emp2") ;
el s£* ' ,. '
Console.WriteUneCempl.é^diferente de_emp_2");
O resultado será que os objectos são diferentes. O problema é que, por omissão, estes são
comparados por referência. Para garantir que os objectos são comparados de acordo com
o seu conteúdo, e não de acordo com a igualdade das suas referências em memória, é
necessário redefinir os métodos EqualsQ. Essa redefinição deverá ser baseada nos
conteúdos e na semântica de utilização do objecto. Ou seja, quando um programador faz
o override de EqualsQ, está a disponibilizar uma classe que possui um método que
permite fazer comparações baseadas em valor.
O primeiro ponto importante deste exemplo é que o método EqualsQ leva como
parâmetro, uma referência do tipo object e não uma referência do tipo Empregado. A
razão é simples de perceber. Se a referência fosse do tipo Empregado, não se trataria de
um override e não se estaria a utilizar polimorfismo. Isto é, caso se estivesse a utilizar
referências para classes base (como object), ao fazer a comparação, não seria chamado o
método correcto. Sempre que se faz override de EqualsQ, é necessário passar-Lhe como
parâmetro, uma referência do tipo object.
O método Equal s Q começa por tentar converter a referência other para uma referência
do tipo Empregado. Ou seja, para ser o mesmo empregado, tem de se estar a comparar
objectos que sejam do tipo Empregado. Caso a conversão não seja possível, é retornado
false, isto é, não se trata do mesmo objecto. Caso sejam, então, são comparados os
nomes dos empregados. Caso sejam iguais, trata-se da mesma pessoa. Caso contrário, são
indivíduos diferentes.
Uma questão fundamental é que uma comparação nunca deverá lançar nenhuma excepção.
Apenas retomar true ou false. Assim, é necessário ter extremo cuidado a escrever este
tipo de código. Por exemplo:
;Émpregãdo émp = new Empregado("Pe"drb B i z a r r o " ) ; " """
if (emp.Equals(null))
Consple.Wn"teLlneC"emp é
deverá retomar f ai se, sem lançar nenhuma excepção.
Uma questão importante é que, regra geral, não deve ser feita a implementação dos
operadores = e ! =. Ao utilizar a linguagem C#, o programador espera poder utilizar estes
operadores para comparar referências e o método EqualsQ, para realizar comparações
baseadas em valor. Em casos muito excepcionais, o programador pode decidir fazer a
implementação desses operadores, permitindo fazer comparações que não são baseadas
em referência. No entanto, não é essa a semântica geral da linguagem C#, pelo que só
muito raramente tal deve ser feito. Um dos casos flagrantes deste tipo de utilização é nos
objectos do tipo string. Os objectos string são comparados baseados em valor e não de
acordo com a sua referência.
Caso o programador queira ter a certeza de que está a comparar objectos baseado em
referências, deverá utilizar o método estático object.ReferenceEqualsQ.
1 No entanto, caso o programador deseje uma implementação muito rápida para este método, poderá fazer a
sua própria implementação, evitando o overhead da chamada virtual. No caso da classe string, é feita a
implementação explicitado método estático EqualsQ.
© FCA - Editora de Informática 253
C#3.5
pelo menos com poucas colisões2. Um exemplo clássico deste tipo de estrutura é a
hashtáble (tabela de haslf\e iremos examinar na secção 8.3.5.
Por omissão, o código hash de um objecto é obtido a partir do seu endereço de memória.
No entanto, a partir do momento em que o programador faz o override de EqualsQ, isso
quer dizer que diferentes objectos em memória, que representam a mesma entidade, vão
possuir códigos hash diferentes. No entanto, isso pode levar a sérios problemas no uso
das hashtáble. Se dois objectos em memória são logicamente o mesmo (ou seja,
EqualsQ retorna verdadeiro), então, têm de possuir o mesmo código hash.
Vejamos um exemplo. O código hash de um empregado pode ser, por exemplo, o número
do seu bilhete de identidade. Diferentes empregados têm diferentes códigos, mas se
existirem objectos em memória que representam o mesmo empregado, então, devem
possuir o mesmo código. Isto permite que, caso o objecto seja armazenado numa
hashtáble, seja possível encontrá-lo rapidamente utilizando o seu número único (ou quase
único).
Caso dois objectos sejam logicamente o mesmo (isto é, caso EqualsQ retome
verdadeiro), GetHashcodeQ deverá retornar o mesmo valor para ambos os
objectos;
2 Diz-se que existe uma colisão quando dois objectos diferentes possuem o mesmo código hash.
3 Em português, o termo correcto para hashtáble é "tabela de dispersão". No entanto, como o termo não é de
uso corrente, optámos por manter a versão inglesa.
O método String.GetHashCodeQ implementa um bom algoritmo de hashing para cadeias de caracteres.
2,54 © FCA - Editora de Informática
CLASSES BASE
class ExemploCap8_l
static void Main(string[] args)
Empregado empl = new Empregado("Pedro Bizarro");
Empregado emp2 = new Empregado("Pedro Bizarro");
Console. WriteLine("empl = {0}", empl);
Console. WriteLineÇ"emp2 = {Q}", emp2) ; _
© FCA - Editora de Informática 255
C#3.5
i f (empl == emp2)
Console.WriteLine("(empl == emp2) = true");
else
Console.Wn"teLine("(empl — emp2) = false");
if (empl.Equals(emp2))
Console.WriteLine("empl.Equals(emp2) = true");
else
Console. Wr"iteLine("empl.Equals(emp2) = false");
if (Empregado.Equals(empl, emp2))
Console.Writel_ine("Empregado.EqualsCempl, emp2) = true");
else
Console. Writel_ine("Empregado. EqualsCempl, emp2) = false");
Console.WriteLineQ ;
Console.Wn"teLine("hash(empl) = {0}", empl.GetHashCodeQ) ;
Console.Writei_-ineC"hash(emp2) = {0}", empZ.GetHashCodeQ) ;
É ainda de referir, que quando se testa a igualdade de value types (por exemplo, no caso
das estruturas), o método EqualsQ e os operadores == e != testam directamente os
valores existentes e não igualdade de referências. Na verdade, object.
.ReferenceEqualsQ retoma sempre false quando é utilizado neste tipo de elementos.
Nos value types, o operador EqualsQ verifica a igualdade de todos os campos presentes
no elemento. No entanto, o programador é livre de fazer o override deste método para
obter outra semântica.
8. l .3 MÉTODO MEMBERWISECLONEO
O método Memberwi secl one C) permite criar uma cópia do objecto sobre o qual é chamado.
Ou seja, sempre que o programador necessita de uma cópia de um objecto, e não
simplesmente de uma referência para o mesmo, deve chamar este método.
define apenas um método: cloneQ. Dentro de cloneO, caso seja aceitável fazer uma
cópia byte a.byte dos campos do objecto, então chamaMemberwisecloneQ:
.clãss Empregado :" ICloneable" " "
; private string NomeDaPessoa;
. public Empregado(string nomeoapessoa) ;
i this.NomeDaPessoa - nomeDapessoa; :
: } .
pubTic~^b~ject clõnéQ " " .
return base.MemberwisecloneO; ^
„..!.... _. . __ _ _.._
public string Nome
get :
{
return NomeDaPessoa;
}
s et
{
this.NomeDaPessoa = value; ;
} ;
. public override string ToStringC)
i return NomeDaPessoa;
1 } ;
Em cloneO, ao fazer base.MemberwisecloneO, é criada uma cópia do objecto
corrente, sendo essa cópia retornada. Note-se, que os dados copiados não são apenas da
classe base System.object mas do objecto real em questão. Por exemplo, ao fazer:
Emp"rêgãdõ"érrfpl" = hew Èmpregado("Pêdro") ;
Empregado emp2 = (Empregado) empl.cloneQ ;
íemp2.Nome = "Paulo";
.Console.Writel_ine( M empl = {0}", empl) ;
.Console. WriteLine("emp2 .=_{0}", emp2) ;
irá surgir no ecrã que empl é Pedro e emp2 é Paulo. Ou seja, o objecto copiado já não
possui nenhuma relação com o objecto original, o que não aconteceria se apenas se
fizesse:
Empregado empl = new "Èmp"rrég*ãdò"C"PedròII5 l "
Empregado^ emp2 = empl; _ _ „
Neste caso, apenas é copiada a referência, ficando ambas as referências a apontar para o
mesmo objecto. Ao modificar o objecto através de uma das referências, a outra também
vê a modificação.
A razão por que é necessário ter cuidado com este método e a razão pela qual
MemberwiseCloneO é declarado protected é que, se no interior do objecto estiver uma
referência, o que é copiado é a referência e não o objecto real em si. Quando é necessário
realizar uma "cópia profunda" de todos os campos internos de um objecto, então, o
método cloneQ deve tratar explicitamente disso. Por exemplo, se em Empregado existir
uma referência para uma tabela com identificadores de projectos em que o empregado
trabalha, para realizar uma cópia completa do empregado, e não só das referências
existentes, terá de se copiar a própria tabela:
class Empregado^: iCTòneãbTe"" " """ ' " "
returh, copia;
a: . _._:.
Note-se que a interface icl oneabl e e o método Cl oneQ representam apenas
convenções de programação. Seria possível implementar estas funcionalidades num
método com outro nome.
Assim, como existe o método Wri.teQ e WriteLineO para enviar cadeias de caracteres
para o ecrã, também existe um método ReadQ e ReadLineQ, que permitem ler do
teclado.
O método ReadLi ne() lê uma linha da consola, retornando uma st n' ng que a representa.
A linha retornada não inclui o carácter de mudança de linha. Ao chamar este método,
pode ser lançada uma lOException ou então uma outofMemoryException. Esta última
excepção ocorre se não existir memória suficiente para retornar a string lida. Quando já
não existem mais linhas de texto a serem lidas da consola, este método retorna nul "l.
Tipicamente, a consola é o teclado. No entanto, caso a entrada do programa tenha sido redireccionada para
outro local (por exemplo, para a saída de um ficheiro), então, a entrada pode representar outra coisa.
Quando falamos de consola, estamos a falar das streams associadas do sistema operativo. Isto é, o Standard
input, Standard output e Standard error.
260 © FCA - Editora de Informática
CLASSES BASE
Quando já não existem mais linhas a ler (linha==null) ou quando o utilizador escreve a
palavra "fim", o programa termina.
Um outro ponto que importa ficar a saber é que console possui três propriedades
importantes: In, Out e E r r o r . Estas propriedades representam as streams de standard
input, standard output e standard error associadas pelo sistema operativo, ao programa.
Mais tarde, iremos examinar com detalhe o mecanismo de streams em .NET.
Para realizar a conversão inversa (de int para string), utiliza-se o método Toint32():
Int dez = Convert_.tolnt3_2C n lQ"); _ _ . _ _ ".",'_ '.1.^
É de recordar, que as palavras Int, double e similares são simplesmente outros nomes
para os tipos presentes no CTS. A tabela 4.1 do capítulo 4 contém a correspondência
entre esses tipos e as classes internamente utilizadas pela plataforma .NET.
Uma questão muito interessante é que ao converter valores para string, também é
possível indicar qual a base de conversão a utilizar. Por exemplo, para converter 240 para
uma string, em que queremos que a conversão seja feita em hexadecimal (base 16),
faz-se:
.string".vaTor = ^Cpnvert\ToStringC24Q 1 _ 16) ;.. __/ ""'_"-- '. "..'_.'.'. „ __
o que resulta em "fO". Ou seja, este conjunto de métodos overloaded também possui um
segundo parâmetro que representa a base de conversão a utilizar.
Falta ainda referir, que no caso de uma conversão de uma st ri ng para urn valor não ser
possível, é lançada um a excepção do tipo system.FormatException.
Mas, se os objectos de string não podem ser alterados, de que forma funciona o
operador de concatenação? Será que quando se está a fazer uma concatenação de cadeias
de caracteres os objectos não são modificados? Isto é:
string frase =""õTã"
.frase*-
não faz com que "Mundo" seja adicionado à cadeia de caracteres em frase? A resposta é
sim e não. O que acontece é ser criado um novo objecto do tipo string que possui
tamanho suficiente para armazenar "Olá " e "Mundo". Em seguida, são copiadas para esse
objecto ambas as cadeias de caracteres e, finalmente, a referência de frase é colocada a
apontar para o objecto. Como podemos ver, a operação de concatenação sobre objectos
do tipo string é muito pesada, envolvendo a criação de objectos temporários e a
realização de cópias. Assim, este tipo de operação deve ser evitado ao máximo. Na
verdade, em termos de performance e de espaço desperdiçado em todo o processo, o
código que escrevemos anteriormente é péssimo:
string inversa = ""~;
,f o r (i nt i=f rase. Length-1; 1>=0; 1 —)
1nversat= frase[1J ; __... „ . . _„.„. „ _„
Neste caso, em cada iteração, é criado um objecto temporário para armazenar o resultado,
o conteúdo de Inversa é concatenado com o carácter em f rase [1], sendo colocado nesse
objecto temporário, a referência 1 nversa é colocada a apontar para o objecto temporário
e, finalmente, o objecto anteriormente apontado por Inversa é colocado à disposição do
garbage collector. Como se pode ver, são criados imensos objectos temporários e são
feitas imensas cópias, o que tem sérios impactos na velocidade de execução do código.
Sempre que são realizadas operações deste tipo, o programador não deverá utilizar a
classe string mas sim a classe s t r i n g B u i l d e r . Esta classe será examinada na próxima
secção. Para já, fica a nota, que caso num programa existam muitas operações que
envolvam cadeias de caracteres, é necessário ter bastante cuidado com elas.
Para terminar esta secção, é apresentada, na tabela 8.3, uma lista das principais operações
suportadas pela classe system.string. No entanto, é de referir, que esta classe possui
© FCA - Editora de Informática 263
C#3.5
muitas mais operações que permitem manipular cadeias de caracteres das mais diversas
formas.
OPERAÇÃO '.. DESCRIÇÃO
int Length { get; > j Propriedade que obtém o tamanho da cadeia de ;
caracteres. ;
Int compareTo(string other) : Compara a string corrente com uma outra '
string.
static !
string Concat(string a, string b) Concatena duas cadeias de caracteres. ;
A.RETKR l " Um objecto da classe string representa uma cadeia imutável de caracteres.
O operador + permite concatenar cadeias de caracteres, o operador []
A classe permite obter caracteres específicos de uma string, o operador == permite
System.string comparar duas cadeias de caracteres e a propriedade Length permite
determinar o tamanho de uma cadeia de caracteres.
Dado que um objecto string é imutável, é necessário ter algum cuidado, de
forma a evitar que existam problemas de performance ao manipular cadeias
de caracteres. Sempre que existem muitas manipulações de caracteres de
uma string, é preferível utilizar a classe stri ngBuil der.
:*.2QOO
1000 *
>Vr 1200 *'•; " . •
:* 1400
'* 1600 * - - '
:* 1800
1 *v 2000 *
De forma geral, após indicar qual o número do parâmetro a imprimir, coloca-se uma
vírgula e o número de espaços reservados para o elemento a imprimir. Para alinhar à
esquerda, coloca-se o sinal menos. Para alinhar à direita, não se coloca nada ou, então,
coloca-se um sinal mais. Após a especificação do número de caracteres reservados, é
ainda possível indicar de que modo é formatado o parâmetro. Para isso, coloca-se dois
pontos seguidos da especificação de formatação. Vejamos um exemplo:
const-doubTè' PI = 3/1415926535; " " " ";
:consple.wrí_teLineÇno,ycilqr _ d e pi é { O j O : F3} . ",_PI) ; _ j
indica que PI deverá ser impresso como sendo um número de vírgula flutuante (F),
utilizando três casas decimais de precisão, sendo a última casa arredondada. Neste caso,
estamos a reservar O espaços para a escrita do parâmetro, pelo que este é escrito
imediatamente. O resultado desta execução é:
'O valor de pi.e 3Y142. _.""_." "_".' "". " "" ". "".„".",~~~. "T"...""_ ~ .'." . '
Regra geral, após a letra que indica a especificação de formatação, existe uma ou mais
indicações sobre a forma como o elemento em causa deverá ser formatado. No caso de F,
trata-se do número de casas decimais a utilizar. É também de referir que, caso não se
reserve espaço para a cadeia de caracteres a imprimir, então, pode-se omitir o uso da
vírgula e do 0. Ou seja, {0,0: F3} e {O: F3} são equivalentes.
valores em
P percentagem ; "{0:P}", 0.43 43,00 % ;
geral
Note-se, que esta tabela não é de forma alguma exaustiva e que cada uma das
especificações de formatação possui, por sua vez, uma forma própria de se utilizar. Toda
esta informação encontra-se disponível na documentação da plataforma. No entanto, para
a maioria das situações, estas especificações são suficientes.
Assim, para ser possível fazer formatações específicas, basta fazer o processamento da
string format. Vejamos um exemplo. Suponhamos que, na classe Empregado, queremos
poder utilizar duas especificações de formato. N quererá dizer que o nome da pessoa deve
ser apresentado, i significa que a idade da pessoa deverá ser apresentada. Assim, ao
escrever:
Empregado emp'= new Empregado("Pèdro Pereira", 23);
Console.WriteLine('' {0} ", emp) ;
deverá surgir apenas "Pedro Pereira". Ao escrever-se:
Console.WriteLineC". {O :NI> ", emp) ;
deverá aparecer "Pedro Pereira/23". E ao escrever-se:
:Console.writeLine(" {O :í} ", emp) ; " " " " . " ~" "
deverá aparecer apenas "23".
return resultado;
}
Neste exemplo, existem apenas alguns pequenos pontos para os quais vale a pena chamar
a atenção. A classe Empregado implementa a interface Iformattable tendo o método
TostringQ definido. A primeira coisa que é feita nesse método é verificar se format é
n u l l . Caso seja n u l l , isso quer dizer que não foi especificado um formato em especial
(isto é, {0}). Neste caso, é apenas mostrado o nome da pessoa. Caso seja especificado N
ou I, é criada uma stríng com as componentes nome/idade. No entanto, caso seja
especificada uma letra inválida, é lançada uma system. FormatExcepfionQ. No final, é
retornada a stri ng construída.
Falta referir um último ponto. Sempre que é necessário formatar uma cadeia de caracteres,
pode utilizar-se o método estático string.FormatQ. Existem diversas variantes deste
método, mas a mais útil leva como parâmetros uma string de formatação e um conjunto
de parâmetros a substituir nessa stri ng. Por exemplo:
intx=~10; .......... " "" .............. "
string rés = String.Format("o valor de x é {0}", x);
neCr_es) ; ________
coloca na variável rés a cadeia de caracteres "O valor de x é 10", mostrando-a depois no
ecrã. Este método é especialmente útil quando se pretende realizar formatações
intermédias de cadeias de caracteres dentro do método TostringQ da interface
Iformattabl e ou, quando se está a formatar cadeias de caracteres para enviar através de
uma interface de rede.
Uma expressão regular representa um padrão que obedece a um conjunto de regras. Por
exemplo, um número inteiro é constituído por um ou mais algarismos. Isto é, um número
inteiro é uma sequência de um ou mais caracteres de O a 9. Isto constitui uma expressão
regular e representa-se por: [0-9]+. Os parêntesis rectos representam um dos caracteres
no seu interior. Ao escrever 0-9, isso quer dizer um dos caracteres de O a 9, inclusive.
O sinal de mais quer dizer que o carácter anterior se repete uma ou mais vezes7.
Vamos então ver de que forma podemos obter todos os valores da variável contas. Para
isso, basta o seguinte código:
.string "contas -""Pereira 4343 euros\n" + " ~" ;
"Germano 12534 euros\n" +
"sacramento 212 euros\n";
string padrão = @""[0-9]+""'; "~ - — — — - • - — - - - -- ,
Se escrevermos a+ isso representa a letra 'a' uma ou mais vezes. Se escrevermos [a-z]+ isso representa
uma ou mais letras seguidas, sendo cada uma das letras um carácter na gama 'a' a 'z'.
272 © FCA - Editora de Informática
CLASSES BASE
quisermos guardar o resultado de uma captura, isto é, de uma subexpressao que faz parte
do match do padrão completo especificado, então, coloca-se essa captura entre parêntesis.
No exemplo, pretendemos capturar, em cada linha, o nome da pessoa e o seu saldo.
Assim, a expressão regular correspondente será8: @" (\S+) ([0-9]+) euros". Como
um todo, esta expressão realiza o match a nível da linha. Dentro de cada linha, temos duas
capturas: uma para o nome e outra para o saldo.
console.WriteLineO;
'console.WriteUne("valpr total = {O}' 1 , total)_; _ __ . . . . _ ; ;....
Neste exemplo, para cada linha correspondente a um match resultante, são substituídas na
cadeia de caracteres "Nome: $1 - Valor: $2" as posições SI e S2 pelo nome e valor
correspondente, encontrado nessa linha. Essa cadeia de caracteres é, depois, enviada para
o ecrã. É também adicionado à variável total, o resultado da conversão do valor
encontrado para inteiro. No final da execução do programa, é mostrado o total de todas as
contas.
Caso o programador deseje, é mesmo possível especificar nomes para as capturas, em vez
de utilizar a sua posição. Para isso, indica-se o seu nome com ?<nome> após a abertura de
parêntesis. Por exemplo, se a expressão regular for @" (?<nome>\s-K) (?<valor>[0-9]+)
Euros", então, pode escrever-se código como:
•int total - 0; -. — . - -
,forea-cr> ;(Match m In resultado)
total *f= Convert.Tolnt32(m.Result("${yalor}.")^^_ _._•
Finalmente, por vezes é útil indicar que um certo conjunto de caracteres deve ser visto
como um grupo, mas que não deve ser guardado enquanto captura. Para isso, utiliza-se o
símbolo ?: a seguir ao abrir parêntesis. Por exemplo, suponhamos que queremos
especificar duas palavras, mas apenas guardar a última. Para tal, poderíamos utilizar a
seguinte expressão: @" (?:\s+) (\s+)".
8 Note-se que o carácter @ impede que \ seja interpretado como uma sequência de escape.
© FCA - Editora de Informática 273
C#3.5
Nesta altura, não podemos deixar de alertar o leitor para o facto de apenas estarmos a
aflorar o tópico de expressões regulares. As expressões regulares são extremamente
poderosas e permitem realizar muitos tipos de operações e de agrupamentos. Nós apenas
estamos a cobrir os aspectos mais básicos. Se o leitor fizer algumas pesquisas, irá verificar
que as possibilidades são imensas. A nível da plataforma .NET, a API de expressões
regulares é extremamente grande e com imensas possibilidades. No entanto, a sua curva
de aprendizagem é bastante íngreme e, só após algum treino, é possível tirar completo
partido da mesma.
A tabela 8.5 apresenta a tabela de caracteres especiais que podem ser usados em
expressões regulares.
EXEMPLO DE EXPRESSÃO ; EXEMPLOS DE MATCH
; DESCRIÇÃO REGULAR
É ainda de referir, que certas sequências assumem um significado especial quando usadas
dentro de parêntesis rectos. Vejamos algumas delas. \ deixa de representar a fronteira de
uma palavra e passa a representar o carácter backspace. A deixa de representar o início da
string e passa a representar uma negação. Por exemplo, [Aã] representa qualquer
carácter que não seja 'a'. Portanto, [Aa-zA-ZO-9] representa qualquer carácter não
alfanumérico.
9 URI significa Uníform Resource Identifier. Uma cadeia de caracteres como http:/Av\v\v.dei.uc.pí representa
um URI. É uma generalização do conceito de URL (Unifonn Resource Locutor}. A diferença é que um
URI identifica urn recurso genérico, enquanto um URL representa uma localização de um recurso. O
significado é algo semelhante, sendo os URI mais genéricos. Actualmente, as especificações modernas
utilizam o termo URI em detrimento de URL.
© FCA - Editora de Informática 275
C#3.5
8.3 COLECÇÕES
Ha cerca de dez anos, era comum os programadores implementarem estruturas de dados
como árvores binárias, hashtables e similares. Isso acontecia porque tais estruturas de
dados são essenciais para criar programas eficientes e rápidos. No entanto, à medida que
as plataformas de programação evoluíam, tal foi-se tomando cada vez menos comum.
Tipicamente, os ambientes de programação passaram a incluir bibliotecas que
disponibilizam estruturas de dados avançadas que escondem a complexidade da sua
implementação. Por exemplo, em C++, existe a STL (Standard Template Library) e em
Java, existe a API Collections. Embora uma implementação eficiente das estruturas
disponibilizadas neste tipo de bibliotecas seja complexa, a sua utilização em si não o é.
Ao utilizá-las, o programador tem acesso a estruturas de dados muito eficientes e que, na
maioria dos casos, lhe resolvem os seus problemas em termos de armazenamento de
dados10.
Na versão 1.x da plataforma .NET, apenas existiam colecções que permitiam armazenar e
retirar referências baseadas em object. Isto é, todos os métodos associados a estas
classes recebiam como parâmetro, referências object, retomando também referências
deste tipo. Como vimos na secção sobre genéricos, isto implica que se faça conversões
explícitas para tipos de dados correctos, o que tem implicações a nível de perfonnance e
segurança de código, para além de ser pouco amigável para o programador. Com a
introdução dos genéricos na versão 2.0 da plataforma .NET, as colecções foram
completamente reformuladas para os suportar, tornando-se mais amigáveis e seguras para
o programador. De facto, actualmente, as colecções genéricas correspondem à forma
recomendada de utilização deste tipo de funcionalidade. Apesar disso, as classes antigas
continuam presentes na plataforma, uma vez que são úteis quando se necessita de
armazenar colecções heterogéneas de objectos. Neste capítulo, discutiremos ambas as
interfaces de programação, embora dando mais ênfase às colecções genéricas.
1 Estamos a falar de dados efémeros, que residem em memória. Para armazenamento a longo prazo,
tipicamente, utiliza-se bases-de-dados, que dão suporte persistente à informação.
276 © FCA - Editora de Informática
CLASSES BASE
• Existe uma propriedade chamada count que permite obter o número de elementos
presentes numa colecção;
CLASSE CLASSE
TRADICIONAL GENÉRICA DESCRIÇÃO
(COLLECTIONS) (COLLECTIONS.GENERIC)
Funciona como uma tabela cujo tamanho cresce
ArrayList LI St<T> automaticamente quando já não existe espaço para
novos elementos. __.....
Armazena os objectos linearmente, utilizando uma
— l_inkedl_ist<T>
lista duplamente ligada.
Armazena uma representação compacta de blts.
BitArray í ™ Funciona como uma tabela de à/tsem que cada à/t .
funciona como um bool .
Representa uma colecção de pares {chave, va/or},
A sua organização é baseada na chave, sendo o
Hashtable Di cfi onary<Key , Vai ue> :
acesso aos seus elementos muito eficiente. Não
existe nenhuma relação de ordem entre as chaves.
Representa uma colecção de valores. A sua
— HashSet<Va~lue> implementação é baseada numa tabela de
dispersão, à semelhança de Hashtabl e.
Semelhante a Dictionary, mas em que os pares
— sortedDictionary
<Key,Value> {chave, valofy são mantidos ordenados. 0
ordenamento é baseado no valor da chave.
Representa uma colecção de pares {chave, valoi}
Sorteduist só rtedLi st<Key , Vai ue> aos quais, é possível aceder através da chave ou de
um índice.
Representa uma colecção de objectos em que o
Queue<T> primeiro objecto a ser colocado na colecção é o
Queue ;
primeiro a sair (FIFO). Tipicamente, esta colecção é
chamada de fila.
Representa uma colecção de objectos em que o
Stack 5tack<T> último objecto a ser colocado na colecção é o
primeiro a sair (LIFO). Muitas vezes, esta colecção
é chamada de pilha.
Nas secções seguintes, em que descreveremos cada uma destas classes, iremos concentrar-nos
nas versões utilizando genéricos, uma vez que são a forma recomendada de utilizar este
tipo de funcionalidades. No entanto, a API das classes correspondentes em
System.collections é bastante semelhante, pelo que praticamente todas as observações
também se aplicam a estas classes.
Para criar uma tabela dinâmica, basta criar um novo objecto da classe List. Caso se
utilize o construtor sem parâmetros, a tabela é criada com espaço para um certo número
de elementos. Ao ultrapassar esse valor, a tabela cresce automaticamente, acontecendo
isso sempre que se esgota o seu espaço. Para construir um novo objecto deste tipo,
baseado em inteiros, basta fazer:
^Lislxinit^ tabela = new "List<int>0 ; " . '. ' L' ' .. . '
No entanto, também é possível indicar o espaço inicial reservado:
:List<int> tabela = n.ew List<int>(20J;. /_'. "" ' . ' '
Para adicionar elementos à tabela, utiliza-se o método A d d Q , para lhes aceder, utiliza-se
o operador []. Por exemplo:
•List<HTt> lista = hew List<int>Q J
for (int 1=0; i<20; i++)
lista.-Add(i); ;
lista fique com espaço para 10 elementos, fazer lista[2], irá resultar numa excepção.
List sabe quantos elementos estão presentes na lista. Este valor é modificado sempre que
métodos corno AddQ e Remove Q são chamados. No entanto, caso uma tabela contenha
elementos, é lícito modificá-los utilizando o operador []:
List<int> lista = new Lisixint>O ;
for (int i-0; i<20; i++) =
lista.Add(i);
for Çprvt i-0; i<lista.count; Í-H-)
listaOi] = 2-i; _ // Correcto! __
Para remover elementos, utiliza-se o método Remove (), que apaga a primeira ocorrência
de um certo elemento, ou o método RemoveAtO, que apaga um elemento numa certa
posição. No entanto, não é muito aconselhável utilizar estes métodos, pois, em termos de
performance, são bastante pesados. Apagar um elemento do meio de uma tabela dinâmica
exige que se movam, todos os elementos que estão após o mesmo, uma posição para trás.
Isso é uma operação lenta. Caso essas operações sejam frequentes, provavelmente, será
mais sensato utilizar um outro tipo de estrutura de dados (e. g. sortedoi cti onary).
No caso da classe ArrayList, esta não é parametrizada pelo tipo T. Em vez disso, utiliza referências
object. Esta diferença deverá ser tida em conta nesta tabela, assim como nas tabelas seguintes, que se
aplicam a outras estruturas de dados.
28O © FCA - Editora de Informática
CLASSES BASE
NULL
t
FIRST • * Anterior "Br jno" Próximo
Nodo de uma
lista duplamente
,,
\L
Nesta lista, existem três elementos guardados. Cada elemento é armazenado num nodo
que contém os dados propriamente ditos e as referências para os nodos anteriores e
seguintes. Como é óbvio, a referência para o nodo anterior do primeiro elemento será
nul 1, assim como a referência para o nodo seguinte do último nodo.
Uma lista ligada permite adicionar e remover, muito eficientemente, dados do início e do
fim da mesma. Também permite armazenar dados com pouco desperdício de espaço,
quando comparado com as tabelas dinâmicas, pois não existem "slots por ocupar". No
entanto, aceder a um elemento que se encontra no meio da lista ou mesmo procurar um
certo elemento é muito ineficiente, pois implica sempre percorrer um grande número de
elementos até se chegar ao pretendido.
MÉTODO/PROPRIEDADE DESCRIÇÃO •
T
Retorna o primeiro elemento da lista.
fijTSt í get; }
T
Retorna o último elemento da lista.
Last { get; J
Li nkedLi stNode<T> Adiciona obj ao início da lista. Retorna uma referência i
AddFirst(T obj) para o nodo acabado de adicionar.
Li nkedLi stNode<T> Adiciona obj ao fim da lista. Retorna uma referência para
AddLast(T obj) o nodo acabado de adicionar. i
vold
Apaga os elementos da lista.
ClearQ
BOOl :
Contai ns (T obj) Verifica se obj se encontra na lista.
BitArray possui diversos construtores, mas o mais utilizado é o que especifica o número
de bits em causa. Por exemplo, para criar uma tabela de 100 bits, faz-se:
LBitÀrray/"t"abela' = .'n^ 7"
A tabela é criada com todos os bits a false. Uma forma alternativa de criar a tabela é
indicando o número de bits a utilizar e o seu valor lógico. Por exemplo, em:
;BitÃfray~ tabela =\new
A tabela terá 100 bits cujo valor lógico é true.
A ideia do crivo de Eratóstenes é muito simples. Imaginemos que temos uma tabela com
todos os inteiros até ao valor onde queremos calcular os números primos. Começa-se no
número 2 e elimina-se todos os seus múltiplos. Em seguida, passa-se ao número três e
também se elimina os seus múltiplos. O número 4 já tinha sido eliminado anteriormente
de modo que se salta. Continua-se no 5, eliminando os seus múltiplos, e assim
sucessivamente. No final, a tabela conterá apenas os números primos menores do que o
tamanho da tabela.
A figura seguinte ilustra a ideia para valores inferiores a 100. Os números primos são
mostrados a mais escuro, sendo os números riscados, os múltiplos eliminados.
11 13 44 17 19
23 34 29
31 Saí 37
41 43 43 44 47
53
61 64 67
71 73 79
83
97 98
Int n = DEFAULT_MAX;
// Leitura do primo máximo a calcular
try
Console. WriteLine("Qual o valor limite para calcular primos? ");
n = Convert.ToInt32(Console.ReadLineO) ;
}
catch
{
Console. WriteLine("valor incorrectamente introduzido. ") ;
Console. WriteLine("Continuando com {0}", DEFAULT_MAX) ;
Este exemplo é simples de entender, uma vez que segue à risca o algoritmo anteriormente
descrito. Note-se que a tabela de bits possui n+1 elementos e não n, devido ao facto de as
tabelas começarem em O e não em 1. Para além deste facto, o único ponto digno de nota é
que não é necessário percorrer a tabela até ao último elemento (n), mas apenas até à sua
raiz quadrada. Este resultado deriva de um teorema elementar de números primos12.
Caso n não seja primo, então pode ser decomposto em pelo menos dois factores: n=a*b. Assim, um dos
factores tem de ser obrigatoriamente menor do que Vn, pois senão Va*Vb seria superior a n. Logo, ou n é
primo ou tem um factor não maior do que Vn.
284 © FCA - Editora de Informática
CLASSES BASE
BitArray(int n, bool i m" t) ' Cria uma nova tabela de n bits inicializados com o valor
init.
Int Número de elementos na tabela.
Count { get; }
B00~l
this[index] { get; set; } Obtém/altera o ó/f índex.
BitArray Realiza um andb\nár\o com os bftsde outra tabela .
And(BitArray other)
or(BitArray otherO Realiza um orbinário com os bftsde outra tabela.
BitArray Inverte todos os bits da tabela. 0 resultado dessa
NotQ inversão também é retornado.
void
SetAlKbool value) Altera todos os bits para um novo valor.
BitArray
Xor(BitArray other) Realiza um AD/- bina rio com os b/tsde outra tabela.
Nós não iremos entrar em muitos detalhes de como é que este processo é implementado,
mas a ideia base é simples. Existem diversos tipos de hashtables, mas uma hashtable
simples consiste numa tabela dinâmica em que cada elemento da tabela é uma lista ligada.
Quando é necessário adicionar um objecto à tabela, é calculado o código de hash da sua
chave (que examinámos quando falámos de comparação de objectos). Este código é um.
valor numérico e tipicamente diferente para cada elemento. Para além disso, o código
encontra-se espalhado ao longo de toda a gama de valores suportada pelo tipo de dados
em causa. Após o cálculo do código, é acedida a posição da tabela com esse código e o
elemento adicionado à lista ligada correspondente. Quando é necessário obter o elemento,
é simplesmente calculado o código hash da sua chave, sendo o elemento pesquisado no
local correcto. A fim de tipicamente ser apenas necessário realizar uma pesquisa, sempre
que a tabela de hash fica com muitos elementos, aumentando as colisões e logo o número
de elementos em cada lista ligada, a tabela cresce de tamanho e os elementos são
redistribuídos pela mesma.
Para concretizar toda esta discussão, vamos, então, ver um exemplo de utilização. Existem
diversas formas de criar uma hashtable^ mas a mais directa e a mais utilizada é usando o
construtor sem parâmetros:
Dlct1onary<string J 1nt> listaTelefonica = new Dictionary<string ,int>O ;
Ao criar uma nova tabela, é necessário indicar qual o tipo dos dados que irá corresponder
às chaves de pesquisa e qual o tipo de dados que irá corresponder aos dados armazenados.
No caso acima, a tabela irá conter pares (nome, telefone). Os nomes, que constituem a
chave, irão ser armazenados como cadeias de caracteres (string) e os números de
telefone como inteiros (int).
Sempre que se adiciona um elemento a uma hashtable, tem de se indicar a sua chave,
sobre a qual o código de hash irá ser calculado, assim como o objecto a adicionar.
A forma mais simples de adicionar e obter elementos de uma tabela é usando o operador
G:
nist"áTèléfõnfca["Péd'ro"Mota"]" ~= 914144678; "" ....... """ " " .
listaTelefonica["3oana Monteiro"] = 934525995;
H i stajelefqrn ca ["Susana^ Piedade"] = . 961122123.J ..... __......._______
Neste exemplo, estamos a adicionar três elementos à tabela. Os elementos propriamente
ditos são "914144678", "934525995" e "961122123". As chaves utilizadas são "Pedro
Mota", "Joana Monteiro" e "Susana Piedade". Ou seja, é sempre adicionado um par
[chave, valor}.
Neste exemplo, caso quiséssemos descobrir o telefone de "Pedro Mota", bastaria fazer:
•iht""téTè'fóné"'=nistàTê1efòhicã[IIPedro Mota"];
Console.Wn'teLineC"Pedrg^ Mota: {0}." ,. telefone);
Note-se bem o que está a acontecer. Quando se escreve:
= ~ 9I41"4"4678ij_ ...... . _ . . '" ". . '7. "_.."_ .
é chamado o método GetHashCodeQ sobre a string "Pedro Mota". Ou seja, é obtido o
código de hash da chave. "914144678" é o elemento a armazenar, ficando associado a
"Pedro Mota".
Ao escrever-se:
•int .telefone^- ^
286 © FCA - Editora de Informática
__ CLASSES BASE
é novamente chamado GetHashCodeQ sobre "Pedro Mota", sendo esse código utilizado
para pesquisar a tabela. No entanto, como podem existir vários elementos com o mesmo
código de hash, "Pedro Mota" é realmente utilizado para encontrar o elemento correcto.
(O código de hash permite encontrar o slot correcto, sendo, depois, pesquisados, um a um,
os elementos em colisão.) Caso a chave seja encontrada, é então retornado o elemento
correspondente. Isto é, "914144678". Caso o elemento não esteja presente, é lançada uma
KeyNotFoundException. É também possível obter o elemento associado a uma chave,
usando o método TryGetVal ue(), que em vez de lançar uma excepção, retoma se a chave
se encontra na tabela ou não:
:int telefonei ' "" ----- -•-•• - .._....-. . . -, - .
:if OIstaTeief-onica.TryGetValueC^edpo Mota", out telefone).). , . . <.
'-, (Con^o.lè.-Wn'teLine("Pedro Mota: {0}", telefone);
else . " í " ' '" " '
ConsoTe,W.nteLjne(nO telefone de Pedro Mota não é .conhecido.11);
Muitas vezes, as hashtables são também chamadas de tabelas associativas, pois guardam
associações [chave, valor}.
elemento com a mesma chave que outro já presente, o anterior é substituído pelo novo.
Finalmente, os elementos de uma hashtable não possuem nenhuma ordem específica.
Assim, ao utilizar o operador foreach para percorrer todos os elementos da tabela, os
elementos podem aparecer por qualquer ordem. Os elementos retornados em foreach são
do tipo Keyvaluepan rxTkey,Tvalue>, tendo duas propriedades públicas: Key e value.
Como exemplo, a forma de iterar ao longo de toda a tabela empregados seria:
foreach (kéyvaluepairxstring, Empregado elemento i n empregados)
Console.WriteLine("{0} / {!}", elemento.Key, _el emento. Value);
A tabela seguinte resume os principais elementos da classe Díctionary<Tkey,Tva1ue> e
Hashtable.
MÉTODO/PROPRIEDADE j DESCRIÇÃO
DicrionaryQ | Cria uma nova hashtable.
Int
Count { get; } Número de elementos na tabela.
Obtém ou adiciona um elemento à tabela, utilizando a
chave key. Sobre o objecto key, é chamado o método
Tval u e GetHashCodeQ. Caso já exista um elemento com a
thris[Tkey key] { get; set; } chave apresentada, este é substituído. Ao tentar obter-se o
elemento e este não exista, Hashtable retorna null
enquanto Dictionary lança uma excepção.
icol lection
Keys { get; } Obtém uma lista de todas as chaves presentes na tabela.
icol lection
Values { get; } Obtém uma lista de todos os objectos presentes na tabela.
Adiciona o elemento value à tabela, utilizando a chave
voíd key. Sobre o objecto key/ é chamado o método
Add(Tkey key, Tvalue value) GetHashCodeQ. Caso já exista um elemento com a
chave apresentada, é lançada uma ArgumentException.
void
ClearQ Apaga todos os elementos da tabela.
BOOl Determina se uma determinada chave está presente na
Contai nsKeyCTkey key) tabela.
Bool Determina se um determinado objecto está presente na
Contai nsvalue (Tvalue value) tabela.
Tenta obter o valor associado à chave key. Caso seja
Bool possível, o mesmo é devolvido em value, retornando
TryGetValue(Tkey key, true. Caso não seja possível, retorna false, colocando
out Tvalue value) em value o valor por omissão para esse tipo de dados.
(Este método só existe na classe oi cti onary.)
void Remove o objecto associado a key, assim como a
Remove (Tkey key) respectiva chave.
não possuindo nenhuma ordem particular. Como o nome indica, a implementação desta
classe é baseada em hashtables.
Como resultado final, a colecção apenas possui três elementos. Como este tipo de dados
não pode conter elementos duplicados, as tentativas de adicionar o mesmo item não serão
bem sucedidas. O método AddQ retorna de um valor lógico que indica se o item foi ou
não adicionado com êxito.
HashSet suporta todas as operações comuns entre conjuntos. Por exemplo, o seguinte
código calcula a intercepção e união entre dois conjuntos.
Hashset<1nt> corrjuntoA = new HashSet<1nt>(new int[] { l, 2, 3, 4 }) ;
Hashset<int> conjuntos = new Hashset<int>(new int[] { 3, 4, 5, 6 }) ;
Ienumerable<int> reuniao_A_B = conjuntoA.union(conjuntoB);
Ienumerable<1nt> intercepcao_A_B = conjuntoA.lntersect(conjuntoB);
Console.write("Reuni ao:\t n );
foreach..Cipt vai in reuniao_A_B)
console.WriteC" {0} ", vai);
Console.WriteLineQ ;
Consola ( .Wn"teC <l Intercepção:\t") ;
foreach '("int vai i n intercepcao_A_B)
Console.WriteC" {0} ", vai);
O resultado é:
^união: 1 2 3 4 5 6
itercepção: 3 4
tabela seguinte resume os principais elementos da classe Hashset<T>.
MÉTODO/PROPRIEDADE DESCRIÇÃO
HashSetQ Cria um novo conjunto.
Int Número de elementos no conjunto.
Count { get; }
(cont.)
elementos armazenados, sendo essa ordem associada às chaves existentes. Para tal, as
chaves utilizadas têm de implementar a interface icomparable. Alternativamente, no
construtor de sortedoictionary pode ser fornecido um objecto que implemente
Icomparer.
O código seguinte cria um sortedoí cti onary e adiciona informação sobre cinco pessoas
ao mesmo:
sórtedoi cti onary<strí hg, fnt> tabél a ='rièw"sortedoi cti ònary<stri h
:tabe]a["Paino Marques"] = 11345465;
tabe]a["carlos Bernardes"] = 10609129; >
:tabelcP["Catan'na Reis"] =14235455; * --•
tabela["Rui Oliveira"] = 12992334; ..
tabela["Andreia Reis"] = 15003244;
Em qualquer altura, é possível obter informação sobre uma pessoa em particular, tal como
se de uma hashtable se tratasse:
fint BI; ~" " " " "" ' • - --•-
if (tabela.TryGetValue("Andreia Reis", out BI))
ConsoTe.WriteLine("Andreia Reis: {0}", BI); :v-
e] se . - -> .: -
Console.WriteLineC'Andreia Reis: Não encontrada!") ;_
No entanto, também é possível listar toda a tabela, baseando-se no ordenamento das
chaves. Ao executar:
foreach (KeyYaluePair<string,int> pessoa in tabela)
: conso1é'.WriteLine_C"{p}_\ .{!}, ",..pessoa.Key, pessoa,vaiue)j . ; ' _; ;
surge:
Andreia Reis
Carlos Bernardes
Catarina Reis,
Rui Oliveira
ou seja, os elementos encontram-se ordenados pela chave (nome), independentemente da
ordem pela qual foram introduzidos. Os enumeradores desta classe percorrem-na pela
ordem das chaves.
14 A regra de ordenamento das pessoas não é muito relevante para já. É possível especificar, de forma simples
diversas formas de ordenamento.
© FCA - Editora de Informática 291
C#3.5
O método compareToQ é chamado sobre os objectos que constituem a chave, de tal forma
a ser possível ordená-los. Este método deverá devolver O caso os objectos sejam
idênticos, um valor menor do que O caso o objecto seja menor do que o que é passado
como parâmetro e um valor positivo caso seja maior. Este método deverá lançar uma
ArgumentException caso other não seja do mesmo tipo do que o objecto em causa e,
caso seja passado nul l como argumento, deverá devolver um valor positivo.
Caso o programador não queira ordenar um sortedoi cti onary de acordo com o método
compareToQ das suas chaves, então deverá indicar no construtor de SortedDictionary
um objecto que implemente a interface icomparer. Esta interface requer que sejam
implementados diversos métodos:
interface System.Collections.lcomparer<t>
/-' compara dois valores, retornando l, se a>b; -l se a<b e O se a==b */
int Compare(T a, T b) ;
/* Retorna se a==b */
bool Equals(T a, T b ) ;
/* Retorna o código de hash de um objecto */
int GetHashCode(T obj) ;
15 Internamente, SortedDicfionary é implementado corno sendo uma "árvore binária equilibrada", do tipo
"vemelha-preta". Este tipo de estruturas, tipicamente, demora um tempo proporcional a Iog2(N) a encontrar
uni elemento, em que N é o número de elementos existentes. As hashtables, tipicamente, demoram um
tempo constante.
292 © FCA - Editora de Informática
CLASSES BASE
system. Gol lections, enquanto a sua versão genérica, que usámos nos exemplos acima,
reside em System.Collections.Generic. Quanto à interface icomparable, ao contrário
do que seria de esperar, reside em System.
(cont.)
MÉTODO/PROPRIEDADE DESCRIÇÃO
Tenta obter o valor associado à chave key. Caso seja
bool possível/ o mesmo é devolvido em value/ retornando
TryGetva"lue(Tkey key, ; true. Caso não seja possível, retorna false, colocando em
out Tvalue value) value/ o valor por omissão para esse tipo de dados. (Este
método só existe na classe Dictionary.)
void Reduz o tamanho interno da tabela para o número de
TrímExcessQ elementos presentes nesta ou para um tamanho adequado.
Vejamos um exemplo simples. Numa repartição pública, as pessoas são atendidas por
ordem de chegada. Assim, ao chegarem três pessoas, estas são colocadas na fila:
Quèué<stn'hg> filaDéEspèra~ =~~hew ~Queue<string>OT
filaDéEspèra.Enqueue("Paulo Simões");
filaDeÈspena.Enqueue("Luis silva") ;
fi1aDeEspera.EnqueueC n Edmundo _Mpnteiro");
(com.)
int
çount { get; > Número de elementos na fila. j
void
ClearQ Apaga todos os elementos da fila. :
MÉTODO/PROPRIEDADE DESCRIÇÃO
StackQ ! Cria uma nova pilha,
int
Count { get; } Número de elementos na pilha. :
void
Apaga todos os elementos da pilha.
ClearO
Boo] j
Contai ns (T obj) Determina se um determinado objecto está presente na pilha.
Retorna o objecto que se encontra no topo da pilha sem o
T
remover. Caso a pilha esteja vazia, é lançada uma
PeekQ
Inval i doperati onExcepti on.
Remove e retorna o objecto que se encontra no topo da pilha.
T
Caso a pilha esteja vazia, é lançada uma
PopO
inval i doperati onExcepti on.
Voi d ;
Push(T obj) Adiciona um objecto ao topo da pilha.
Chama-se operações de entrada e saída a chamadas que envolvem a manipulação de dados de e para
dispositivos periféricos. Um exemplo clássico é o acesso ao sistema de ficheiros de um disco.
30O © FCA - Editora de Informática
CLASSES BASE
path é uma classe utilitária que permite realizar operações sobre cadeias de
caracteres que representam nomes de ficheiros. Por exemplo, é possível
determinar directamente se a extensão de um ficheiro está ou não incluída numa
string que representa o nome de um ficheiro, combinar cadeias de caracteres
que representam directórios e ficheiros ou descobrir qual o directório temporário
do disco.
Tal como referimos anteriormente, pilelnf o e Flle são similares, assim como
Dl rectoryinf o e Dl rectory. Concretizando, tanto é possível escrever:
iFLleTMõv^^ @"c:Xter"P\xp_tn...txt.'l3.;."_. ."J_."'.'.'".'.'.-_.Vr~"!' "•
para alterar o nome do ficheiro "c:\temp\xpto.txt" para "c:\temp\xpti.txt", como é
possível escrever:
fFilèíhfo xptò"= nèw Fnelnfo"Ç@"c:\temp\xpto.txt II >r
[Xptg_.MpyeTo^C©"c:\temp.\xpti.. txt") ;
0 efeito é -exactamente o mesmo.
Uma questão muito importante é que antes de realizar uma operação sobre um ficheiro
(ou directório), é aconselhável verificar se este existe. Isso pode ser conseguido,
utilizando a propriedade Fll elnf o. EXT sts ou, caso se esteja a utilizar File, através do
método estático Fi 1 e . EXI sts Q . Por exemplo:
iFilelriTõ xpto = new Fi l èlnfo(@" c:\tlimp\xpto.tx~t"} ; ..... "
1 f (xpto.Exists)
L . xptp_.MpveTp(@"c:\temp\xpti .
Apesar deste tipo de precaução, existe, ainda, um conjunto vasto de formas de MoveToQ
falhar. Por exemplo, o ficheiro pode ter sido apagado entre a altura em que foi feito o
teste e a altura em que MoveToQ é executado, o utilizador pode não ter permissões para
ler o ficheiro, pode existir um erro ao ler o ficheiro do disco e assim sucessivamente. O
que isto quer dizer é que existe uma panóplia de excepções, que podem ser lançadas em
virtude de algo correr mal. A maior parte das excepções derivam de
System. 10. lOException, mas existem algumas excepções, que não estão directamente
relacionadas com entrada/saída, que podem ser lançadas. Por exemplo, caso o utilizador
não tenha permissões para aceder a um ficheiro, é lançada uma System .
.security.SecurityException. Assim, deve proteger-se este tipo de operações por um
bloco try-catch:
; try
•í
Filelnfo xpto = new FllelnfoC@"c:\temp\xpto.txt"); i
1 i f (xpto.Exists)
[ xpto.MoveTo(@"c:\temp\xpti.txt");
! else ;
Console. Writel_ine(@"o ficheiro c:\temp\xpto.txt não existe! ") ; \h (Exce
Note-se que o facto de estarmos a apanhar as excepções não invalida que se faça um teste
explícito à existência do ficheiro. Tal como discutimos anteriormente, as excepções
devem ser utilizadas em situações de erro e não em situações em que um simples teste
basta. De facto, é tão comum um utilizador enganar-se a escrever um nome de um
ficheiro que isso deve ser verificado com um teste simples, explícito, e não com um
mecanismo de tratamento de erros genérico como as excepções.
Tal corno dissemos atrás, a funcionalidade associada a estas classes também é coberta nas
classes File e Directory sob a forma de métodos estáticos. O programador é livre de
utilizar as que entender.
As tabelas 8.16 e 8.17 mostram os principais métodos das classes Filelnfo e Dkectorylnfo.
MÉTODO/PROPRIEDADE DESCRIÇÃO i
(cont.)
MÉTODO/PROPRIEDADE: DESCRIÇÃO |
Dateti me Obtém ou altera a hora e a data da última escrita ,
LastWriteTime { get; set; } no ficheiro.
Long
Length { get; } Retorna o tamanho do ficheiro em bytes. \m o nome canón
string
Name { get; }
StreamWriter Cria um StreamWriter que permite adicionar ••
AppendTextO texto ao final do ficheiro.
Copia o ficheiro para um novo ficheiro, sendo
Filei nfo
CopyTo(string destName) especificado o nome deste. É retornado um ;
objecto que representa o novo ficheiro, :
FileStream Cria o ficheiro, retornando a FileStream :
CreateC) associada.
StreamWriter
CreateTextQ
Cria o ficheiro para escrita de texto. :
Void
Apaga o ficheiro.
DeleteQ
Move o ficheiro para um certo destino. Caso dest ;
Void
MoveTo(string dest) especifique um nome de ficheiro no fim, o ficheiro
muda de nome.
Abre o ficheiro, utilizando um certo modo
FileStream
Open(FileMode mode,
(exemplos: open, Append) e para um certo tipo '•
FileAccess acess) de operação de acesso (exemplos: Read, Wri te,
ReadWrite).
FileStream
OpenReadQ . . . . :
Abre o ficheiro apenas para leitura.
streamReader !
Op_enText() . . . .:
Abre o ficheiro para leitura de texto.
FileStream
OpenWriteQ Abre o ficheiro apenas para escrita.
Void Refresca o estado do objecto, a partir do estado
RefreshQ ; corrente do ficheiro.
MÉTODO/PROPRIEDADE | DESCRIÇÃO
(cont.)
MÉTODO/PROPRIEDADE ' DESCRIÇÃO
DateTime i Obtém ou altera a hora e a data do último acesso ;
LastAccessTime { get; set; } ! ao directório.
DateTime í Obtém ou altera a hora e a data da última escrita
LastwriteTime { get; set; } no directório.
strlng
Natne { get; > Obtém o nome canónico do directório.
Directoryinfo j Obtém um objecto que representa o directório
parent { get; } acima do corrente.
Directoryinfo Obtém o directório raiz do sistema de ficheiros do
Root { get; } directório em causa.
vold
createQ Cria o directório. :
Dl recto ryinto
CreateSubdi rectoryC '- Cria um subdirectório. :
stríng path) í
void '•
Apaga o directório. :
DeleteQ !
Apaga o directório/ especificando se os '
void
De1ete(bool recursive) subdirectórios e os ficheiros nele contidos devem
i ser apagados.
Di recto ryinfo[] Retorna uma tabela com os subdirectórios ;
Getoi rectori es () presentes no directório.
Directorylnfo[] Retorna uma tabela com os subdirectórios
Getoi rectori es ( presentes no directório que obedecem a um
string wildcard) certo padrão (uso de wild cards).
Filelnfo[] Retorna uma tabela com os ficheiros presentes -.
GetFilesQ | no directório. :
Finalmente, falta-nos abordar a classe path. Esta classe dispõe apenas de métodos
estáticos, permitindo ao programador, manipular, de forma simples, cadeias de caracteres
que representam directórios e ficheiros. Por exemplo, para alterar a extensão do nome de
um ficheiro de ".txt" para ".doe", basta fazer:
string ? -noyóNòmè =/ Pã±H.chanjeExiénTiP^
Para descobrir quais os caracteres que não se pode utilizar no nome de um ficheiro, pode
fazer-se:
cbnsol e.Wn"teLineCrc|iract_eYeslxnvalJi~dps: "{0};'' r.PltifrinyalIdpathChàrsJj. ]
MÉTODO/PROPRIEDADE DESCRIÇÃO
static readonly char 0 carácter utilizado para separar o nome dos
Di rectoryseparatorchar directórios.
static readonly char[] Caracteres que não podem ser utilizados no nome í
invalidpathchars de um ficheiro ou directório.
static string Altera a extensão de path para uma nova .
ChangeExtensnonC extensão. Caso ext seja null, a extensão é •
string path, string ext) apagada.
static string Combina duas paths, adicionando caracteres
Combine (
string pathl, string path2) separadores apropriados, se necessário.
static string Obtém apenas a parte correspondente ao :
GetoirectoryNameCstring path) directório em path.
static string Obtém apenas a extensão do nome especificado
GetExtension (string path) em path.
static string ' Obtém a parte correspondente ao nome do .
GetFileName(string path) ficheiro e a extensão em path.
static string Obtém apenas a parte correspondente ao nome,
GetFileNameWithoutExtension(
string path) sem extensão, do ficheiro em path.
static string Obtém a path completa relativamente ao
GetFull path (string path) argumento passado como parâmetro.
static string Obtém a path correspondente à raiz utilizada no
GetPathRoot(string path) argumento passado como parâmetro.
static string Retorna o nome de um ficheiro único em disco,
GetTemp Fi 1 eName () criando esse ficheiro com tamanho 0.
static string Retorna o caminho para o directório temporário
GetTempPathQ em disco.
static bool
HasExtensionCstring path) Verifica se o argumento possui uma extensão.
static bool Verifica se o argumento corresponde a um
ispathRootedCstring path) caminho absoluto ou relativo. ,
As streams são combinadas ern cadeia para fornecer funcionalidades cada vez mais
elaboradas. Por exemplo, é possível ter uma stream simples que apenas é capaz de
devolver um byte de cada vez, lido de um certo dispositivo. Sobre esta stream, pode-se
colocar uma outra stream capaz de juntar bytes e de os encarar como inteiros, cadeias de
caracteres ou números de vírgula flutuante. Do ponto de vista do programador, este
apenas tem de criar o encadeamento de streams que necessita, utilizando a última para
realizar as operações complexas que necessita. A figura 8.4 ilustra a ideia.
O programador utiliza a
A P I fornecida pela
stream que se encontra
no final da cadeia.
0 segundo tipo de streams leva como parâmetro, no seu construtor, uma stream do tipo
básico. Por exemplo, em:
| ç p y - - - — -- ----- - -- - - - -
a expressão
cria uma stream básica associada a um ficheiro ("xpto.txt"). Esta stream apenas permite
operações como ler e escrever blocos de bytes. Para conseguirmos escrever texto para o
ficheiro, é necessário encapsular esta stream numa outra mais poderosa: streamWriter.
No construtor desta stream, é colocada uma referência para a stream associada ao
ficheiro.
Tendo sempre este conceito em mente, vamos, então, examinar a hierarquia de classes
relativa a streams. Esta hierarquia é apresentada na figura 8.5.
Sysiem.Object
System.lO.BinatyWriter System.lO.BInaryReader
System. IO.TextWriter
,
' t
der System.IO.StreamWn
Nós não analisaremos profundamente todas estas classes, mas iremos dar uma pequena
descrição das classes mais derivadas. Isto é, as que são realmente utilizadas pelo
programador. Recomendamos que se consulte a documentação pormenorizada de cada
classe, sempre que é necessário utilizar uma delas. A tabela seguinte resume qual a
utilização de cada uma destas classes.
CLASSE DESCRIÇÃO
stream Representa a classe base de onde todas as streams básicas derivam. ;
Trata-se de uma stream que faz bufferíng dos dados lidos (ou escritos) de uma •
Bufferedstream outra stream, por forma a aumentar o desempenho das leituras e escritas nesta. !
É construída utilizando uma instância de Stream. :
É uma stream básica cuja fonte de dados se encontra em memória/ sob a forma
MemoryStream . de uma tabela de bytes. É muito útil quando é necessário preparar dados para
enviar para a rede ou para um outro dispositivo
Representa uma stream associada a um ficheiro, sendo utilizada por outras
FileStream
5frea/77spara realizar leituras e escritas para disco.
Implementa uma stream que permite ler dados sob a forma de texto de uma
StreamReader stream básica (instância de Stream). No entanto/ também possui um construtor
que permite associar directamente a stream a um ficheiro.
É uma stream que permite realizar leituras de dados a partir de uma string
StringReader
em memória.
Implementa uma stream que permite escrever dados sob a forma de texto para
Streamwriter uma stream básica (stream). No entanto/ também possui um construtor que
permite associá-la directamente a um ficheiro.
É uma stream que permite realizar escritas de dados numa cadeia de caracteres
Stringwriter
em memória. Essa cadeia de caracteres é uma instância de stringBuilder.
Trata-se de uma stream que permite ler dados/ sob a forma binária, de uma
BinaryReader
stream básica (uma instância de stream).
Representa uma stream que permite escrever dados/ sob a forma binária/ para '
BinaryWriter
uma stream básica (uma stream).
Quando se abre um ficheiro, é necessário especificar o seu nome (name), o modo em que
se quer abri-lo (mode) e qual o tipo de acesso que se irá realizar (access). Existe, ainda
uma variante deste construtor onde é possível especificar o tipo de partilha que deverá
existir relativamente ao ficheiro. Tanto FileMode como Fi l eAccess são enumerações,
sendo os seus valores possíveis apresentados na tabela seguinte.
j VALOR DESCRIÇÃO
FileMode. Append Caso o ficheiro não exista/ cria o ficheiro. Caso exista/
abre-o, sendo as escritas realizadas no fim deste.
Fil eMode. Create Caso o ficheiro não exista, cria o ficheiro. Caso exista,
o ficheiro é colocado com tamanho 0, sendo reescrito.
Caso o ficheiro não exista, cria o ficheiro. Caso exista, :
Fi 1 eMode . CreateNew
é lançada uma excepção.
Uma forma muito usual de utilização deste construtor é especificar apenas o modo de
abertura. Neste caso, o tipo de acesso é para leitura e escrita, o que cobre todas as
situações possíveis de utilização. Por exemplo:
Fílestream log = 'new FiTeStre_ámCl]pa-.txtn^ FlleMpdé_lAppend)j "
De seguida, iremos ver de que forma é que esta classe é utilizada para ler e escrever
ficheiros de texto e também ficheiros binários.
writer.writeLineQ;
No final das escritas terem sido feitas, é essencial que as streams associadas ao ficheiro
sejam fechadas. Caso isso não seja feito, podem existir dados que não são escritos em
disco. Para fechar toda uma cadeia de streams, basta chamar o método cl os e Q na stream
mais acima:
A seguinte listagem ilustra este exemplo. Nesta listagem, o acesso ao ficheiro encontra-se
protegido por um bloco try-catch, tal como seria de esperar.
try
StreamWriter writer = new StreamWriter("tabuada.txt") ;
writer. Writel_ine("** Tabuada **") ;
writer.writeLineQ ;
1 Une = reader.ReadLineQ ;
: if ("line != null)
Console.Wri teLi ne(li ne);
J while ("Hne |= null);
y
Console.Wr1teLlneCe,5tacj<Traçe3
< _• • •- .- ' - - '. ..
Existe um último ponto com o qual é necessário ter algum cuidado. Hoje em dia, quando
se escreve um ficheiro de texto, este encontra-se codificado de uma certa forma. Embora
a maioria dos caracteres apareçam "normalmente", existem certas sequências de
caracteres que representam letras especiais. Isto deve-se ao facto de o código ASCII
apenas permitir representar 255 letras diferentes (correspondente a 8 bits)}1.
Caso o programador deseje, pode especificar outras formas de codificação, tanto para ler,
como para escrever ficheiros. A forma de codificação é passada como argumento no
construtor de streamReader e streamWriter. Algumas outras formas possíveis de
codificação são: ASCiiEncoding, urricodeEncoding e UTF7Encod1ng. Todas estas formas
de codificação são implementadas como clas.ses em System.Text.
" Para ler um ficheiro de texto, basta ter uma instância de StreamReader.
Essa instância pode ser obtida, associando uma Filestream a um
StreaniReader ou criando directamente um StreamReader, indicando o
Ficheiros de nome do ficheiro em causa. Exemplo:
texto l
StreamReader ficheiro = new streamReader("log.txt");
- Para escrever para um ficheiro de texto, basta ter uma instância de
StreamWriter. Essa instância pode ser obtida, associando uma Filestream
a um streamWriter ou criando directamente um streamWriter, indicando
o nome do ficheiro em causa. Exemplo:
StreamWriter ficheiro = new streamWriter("dados.txt");
17 Estritamente falando, o código ASCII apenas especifica codificação para 7 bits (isto é, 127 caracteres).
A utilização do oitavo bit é urna extensão, existindo vários mapeamentos possíveis na zona 128-255.
314 © FCA - Editora de Informática
CLASSES BASE
A classe Binarywriter possui versões do método WriteQ que permitem escrever todos
os tipos básicos do CTS (Cominou Type System). Por exemplo, existe um Bi narywrí ter.
.Write(char), um Bi n a r y W r i t e r . w r i te (st ri ng), Binary.Write(int) e assim,
sucessivamente.
/*
* programa que calcula números primos até um certo máximo
* escrevendo esses números num ficheiro. Finalmente,
* o ficheiro é lido e os números mostrados no ecrã.
*/
using System;
using System. IO;
using System. Coí "l ections;
// classe que trata de todos os assuntos relacionados com
// números primos
class Primos
{
private int[] TabelaPrimos;
if (TabelaPrimos == null)
fich.Write(O);
e! s e
{
fich.WriteCTabelaPrimos.Length) ;
foreach (int primo in TabelaPrimos)
fich.Write(primo) ;
}
fich.closeC) ;
}
// Carrega a tabela presente num ficheiro para memória. Podem
// ser lançadas excepções. TabelaPrimos é limpa no inicio deste
// processo,
public void CarregaTabelaFicheiro(string nomeFichei ro)
TabelaPrimos = null;
FileStream fs = new FilestreamCnomeFichei ro, FileMode.Open) ;
BinaryReader fich = new BinaryReader(fs) ;
int totalprimos = fich.ReadlntSZQ ;
TabelaPrimos = new int[totalPrimos] ;
for (int i=0; i <total Primos;
TabelaPrimos[i] = fich.Readlnt32() ;
fich.closeC) ;
if (TabelaPrimos != null)
Serialização é o nome dado ao processo de converter um objecto numa forma que possa
ser guardada persistentemente. Isto, tipicamente, envolve guardar o seu estado interno.
Por exemplo, pode-se serializar um objecto para um formato binário, sendo possível
transportá-lo pela Internet, reconstruindo-o nurna outra máquina. Da mesma forma, é
possível serializar um objecto, guardando-o num ficheiro. Mais tarde, é possível voltar a
abrir o ficheiro e recuperar o objecto.
Se tivermos um objecto deste tipo, ou mesmo uma tabela de objectos, então, é muito
simples escrevê-lo para uma stream. Suponhamos que queremos escrever um objecto
Empregado num ficheiro binário. Basta-nos criar uma instância de BinaryFormatter e
chamar nesta, o método serializeQ, com a stream que se quer utilizar:
Empregado emp = new Emprègado("Pedro Abreu","26);"
Uma questão importante é que, caso o objecto contenha referências para outros objectos,
o sistema encarrega-se de guardar todo o grafo de objectos. No entanto, cada objecto que
se encontra num dado grafo apenas é guardado uma vez. O método DeserializeC)
encarrega-se de reconstruir o grafo de relações, tal como estas existiam, antes de o
objecto pai ser guardado. Obviamente, que para ser possível guardar todos os objectos
referenciados a partir de uma classe, é necessário que as classes correspondentes sejam
também marcadas com o atributo Serializable. Por exemplo, se em Empregado
existisse um campo que fosse do tipo Automóvel, então, a classe Automóvel também teria
© FCA - Editora de Informática 31 9
C#3.5
de ser marcada como serializable. Caso isso não acontecesse, ao serializar Empregado
iria surgir uma seri ai i zati onExcepti on.
using System;
using System.IO;
usi ng System.Runti me.Seri ai i zati on.Formatters.Bi nary;
usi ng System.Runti me.Seri ai i zati on;
[Serializable]
class Empregado
private string NomeEmpregado;
private int IdadeEmpregado;
public Empregado(string nome, int idade)
NomeEmpregado = nome;
IdadeEmpregado = idade;
class Exemplocap8_6
const string NOME_FICHEIRO = "dados.dat";
static void Main(string[] args)
Empregado[] empresa = new Empregado[l
new Empregado("Nuno santos", 24),
new Empregado("Ricardo Rodrigues", 22),
new Empregado("Paulo Germano , 26),
new Empregado("Pedro Pereira", 23)
j>
try
// Escreve para ficheiro os empregados da empresa
Filestream ficheiro
= new FileStreamÇNOME_FICHElRO, FileMode.Create);
Em todo este processo, existe um ponto subtil mas extremamente importante: apenas
informação que diz respeito aos objectos em si é guardada no processo de socialização.
Isto quer dizer que, por exemplo, uma variável estática de uma classe não é seríalizada.
Apenas variáveis que dizem respeito a cada objecto em si o são. Isto faz sentido, uma vez
que quando chamamos serial i ze(), estamos a pedir para serializar um objecto e não
uma classe.
Assim como os campos estáticos não são guardados no resultado de uma serialização, o
programador também pode definir outros campos que não devem ser guardados. Para
isto, basta marcar todos os campos a não serializar com o atributo Nonserialized. Por
exemplo, suponhamos que em Empregado existe um campo chamado UltimaLeitura que
representa o dia e a hora em que o objecto foi lido de ficheiro. Esta informação pode ser
utilizada para, por exemplo, ver se o objecto está actualizado ou não. Não faz sentido
serializar esta informação, uma vez que este campo é actualizado assim que o objecto é
lido de ficheiro. Assim, deve-se marcá-lo como Nonseri ai i zed:
[Serial i zabTe] ' " " " ~~
class Empregado
private string NomeEmpregado;
private int idadeEmpregado;
[NonSériaTizèd] '*
P ri vate pateTime Ul ti maLertu^ra;
objecto que não se encontre nos seus campos ou realizar algum tipo de operações de
sincronização. Caso o programador queira especificar de que forma deve ser feita a
serialização, então, para além de marcar a classe como se ri ai i zabl e, terá de
implementar a interface iserializable. Vamos ver um exemplo simples que
implementa ise ri ai i zabl e para serializar manualmente os objectos de Empregado.
; void GetObjectDataC
; Serializationlnfo info // os dados do objecto i
StreamingContext context // O contexto da stream destino ;
)i
i NomeEmpregado = info.GetString("nome") ;
; IdadeEmpregado = info.Getlnt32("idade") í
}
// chamado por SerializeQ para guardar os dados do objecto
public
void GetobjectData(Serializationlnfo info, streamingContext context)
// Coloca a infçrmação necessária em info
info.AddValue("idade" , IdadeEmpregado) ;
info.Addvalue("nome" , NomeEmpregado) ;
Valor = info.GetString("valor");
Outro = new Xpti(info, context);
} ;
public
; void GetObjectData(Serializationlnfo info, StreamingContext context) '.
info.Addvalue("valor", Valor); :
: Outro.GetobjectDataCi nfo, context);
}
Claro que isto apenas funciona para classes que implementem iserializable. Para
serializar objectos de classes que não implementam esta interface, basta adicionar o
objecto a i nf o e mais tarde retirá-lo:
'[Serializable] ' " " " ' " "" " " " "
class Xpto : iserializable '.
public
void GetobjectData(SerializationInfo info, StreamingContext context)
info.AddValue("valor", Valor);
info.Addvalue("outro", Outro);
Aqui, o único truque é que é necessário utilizar o método GetVal ue() que permite retirar
um objecto que foi serializado para i nfo. Este método exige que se indique o tipo de
dados que se está a ir buscar.
Para terminar esta secção, resta-nos fazer uma chamada de atenção para o facto de o
atributo serializable não ser herdado. Isto quer dizer que, caso o programador declare
uma classe derivada de uma classe serializable, também necessita de marcar esta
classe como sendo Serializable. Caso a classe base implemente iserializable, então,
o método GetobjectDataQ dessa classe deverá ser marcado como vi rtual. Caso isso
não aconteça, não há garantia de que o método correcto seja chamado quando se está a
fazer a serialização de uma classe derivada. Finalmente, neste caso, também é necessário
criar o método GetobjectDataQ na classe derivada, assim como o construtor de
reconstrução do objecto. Em GetobjectoataQ, deverá ser chamado o método da classe
base para reconstruir o objecto base e no construtor, deverá ser chamado o construtor
base. O código seguinte ilustra a forma correcta de realizar estas operações.
irsérializabTe] " "" ;
xlass Base : iserializable
;{ ;
private int ValorBase;
• public Base(SerializationInfo info, StreamingContext context)
ValorBase = info.Getint32("valorBase");
[~ publiç vTrtuaT 7 ~ I ~ V l ~ L l
void GetobjecfDataCsèrializatioriinfò" info, s£réairnngContex~t context)
info.AddValue("valorBase", ValorBase);
;}
[Serializable]
class Derivada : Base
.{
!._.P.GVa.te int ValorDerivada;
base.GetObiectData(infOj context);
info.AddValue( n valorDerivada", ValorDerivada) ;
}
ARETER " Serializar um objecto consiste em criar uma representação do seu estado que
possa ser guardada de forma persistente.
^
Serialização de ~ Todas as classes que possam ser serializadas têm de ser marcadas com o
objectos para atributo Serializable.
formato binário " Todos os campos que não devem ser serializados devem ser marcados com
o atributo Nonserialized.
~ Para serialízar um objecto para um formato binário, cria-se uma instância de
BinaryFormatter, chamando nesta, o método SerializeQ. Neste método é
necessário indicar a síream para a qual o objecto é escrito, assim como o
objecto em causa:
BinaryFormatter formatador = new BinaryFormatterO;
formatador.SerializeCstreamSaida, objecto);
~ Para recuperar um objecto, utiliza-se o processo inverso, chamando o
método DeserializeO- É necessário realizar uma conversão explícita para
o tipo de dados correcto:
BinaryFormatter formatador = new BinaryFormatterO ;
Xpto obj = (Xpto) formatador.Deserialize(streamEntrada);
~ O grafo completo de objectos referenciados pelo objecto indicado em
sen" ai i ze Q é guardado. Todas as classes envolvidas têm de estar marcadas
como sendo Serializable.
" Caso o programador deseje, pode controlar manualmente o processo de
serialização. Para isso, necessita de implementar a interface is e ri ai i zabl e.
Esta operação não ê em geral recomendada e, tipicamente, não é necessária.
formatador.Serialize^icheiro, emp) ;
ficheiro-.Close()J ......
é criado um ficheiro XML em disco que contém o objecto serializado. O ficheiro tem o
seguinte aspecto:
<?xmT version="1.0"?> ,
<Empregado xmlns:xsd="http://www.w3.org/2001/XMLSchema 11
xml as :xs4="http://www. w3.org/2001/XMLSchema-instance11> . V '
<tfome>Carla Fonseca</Nome> • *
<Xdacie>23</ldade> ! - -. !
</Empregado>
Note-se que ao criar o objecto formatador, que é uma instância de Xml Serial i zer, é
necessário indicar o tipo de dados do objecto que irá ser serializado. Isto é conseguido
utilizando o operador typeof. Esta informação é necessária para formatador saber qual o
tipo de dados com que tem de marcar a raiz do documento.
Para recuperar urn objecto formatado em XML, basta realizar o processo inverso:
FileS-tream ficheiro = new FiTeStream(NOMÉ_F±CHEIRO, FileMode.Open);
XmlSerTaTizer formatador = new XmlSerial izer(typeof(Empregado);;
ficheiro^.CloseO J
Não iremos falar muito mais de serialização e de recuperação de objectos em XML. No
entanto, iremos indicar alguns ponteiros para leitores que queiram explorar este tópico
mais a fundo.
Em primeiro lugar, apenas o formato e a informação pública contida nos objectos são
guardadas por este processo. A definição do tipo de dados, assim como informação sobre
o assembly não é incluída. Por exemplo, ao ser feita a serialização de Empregado num
sistema e, posteriormente, a sua recuperação noutro sistema, não é garantido que o tipo de
dados (a classe) correspondente seja a mesma.
Os dados gerados por Xml se ri ai i zer são compatíveis com os tipos de dados definidos
pelo W3C (http://www.w3c.org) na XML Schema Definition Language (XSD) 1.0. Um
schema indica a forma como é feita a correspondência entre os campos de uma classe
(incluindo o seu nome e valor) e as tags que surgem num ficheiro XML. Em termos desta
correspondência, o programador pode seguir três abordagens:
Existem diversos atributos que podem ser utilizados para controlar a serialização, em
XML, de um objecto. Nós não os iremos cobrir todos, iremos, apenas, ilustrar de que
forma é que se pode utilizar estes atributos para obter uma correspondência diferente
entre o ficheiro XML e os campos de uma classe.
Como vimos no exemplo Empregado, o ficheiro XML gerado tem o seguinte aspecto:
;<?xiTfl vérsTon="1.0"?> ~""
:<Empregado xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xml ns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Nome>Carla Fonseca</Nome>
<Idade>23</Idade>
i</Ernpregadg> _
A classe possui o nome Empregado, logo no ficheiro XML surge a tag "Empregado".
Existe um campo Nome e um campo idade, logo, são estes que surgem no ficheiro XML.
Para isso, basta anotar a classe com dois atributos. XmlRoot que indica o nome do tipo
raiz que está a ser serializado. Xml El ement é utilizado para indicar outro nome para os
campos da classe. Assim, Empregado fica:
[serial i zabTéJ ~~ ~~~ " " '"' "" ~
[Xml RootC"Trabal nado rAssalariado")]
p u b l i c clas_s Empregado
18 Note-se que o utilitário xsd. exe é muito poderoso, permitindo, não só gerar classes a partir de um schema,
como gerar um schema a partir das classes, inferir um schema a partir de um ficheiro XML e, também,
converter schemas XDR em XSD.
19 Não nos podemos esquecer de que um dos grandes objectivos do uso de XML é a interoperabilidade.
Assim, para que dois sistemas se entendam, um ponto importante é que utilizem as mesmas tags. Isto é, o
mesmo schema.
328 © FCA - Editora de Informática
CLASSES BASE
J" " "" ~' ' ~ "" * "V *^£> ~" ~~ ~' "~
; CXmlETement("NomeDapessoa")] ' *^ ^*"
: public strlng Nome
get'{' return NomeEmpregado; } : ;
; set { NomeEmpregado ~ value; } i
; [xmlElementC"ldadeDapessoa")]
: publi.c -int idade
; get {, return IdadeEmpregado; }
::-{;- IdadeEmpregado = value; }
Finalmente, o leitor deverá ficar consciente de que, muitas vezes associada à serialização
de objectos em XML, é referido o protocolo SOAP (Simple Object Access Protocol). Este
protocolo permite realizar chamadas a métodos remotos, existentes noutras máquinas, em
que a informação é trocada em XML. Mesmo não considerando o uso do protocolo
através de uma rede, este tópico é muitas vezes discutido quando se fala de serialização
XML e de "envelopes SOAP" (que também são XML, mas com algumas características
especiais). Por exemplo, é possível criar um ficheiro com um envelope SOAP em XML.
Fica aqui o alerta para o caso de o leitor ver documentação que se refira a XML and
SOAP Serialization, tal como acontece com o MSDN.
Existem diversos motivos pelos quais é útil poder ter diversos fluxos de execução num
programa. Vejamos alguns. Suponhamos que temos um browser de Internet (por
exemplo, o internet Explorer ou o Firefox). Ao ir buscar uma página web à Internet, é
possível ir buscar primeiro o texto da página e depois, sequencialmente, cada uma das
imagens presentes na página. No entanto, tal processo é muito fastidioso do ponto de
vista do utilizador. Primeiro aparece o texto, a primeira imagem, a segunda imagem e
assim sucessivamente. É muito mais interessante o utilizador ver o texto e cada uma das
imagens aparecer mais ou menos simultaneamente. A utilização de threads permite
programar isto de forma simples. Ao receber o texto, o browser verifica as imagens que
existem na página e cria uma thread para fazer o carregamento de cada imagem. Cada
imagem irá ser carregada simultaneamente e mostrada à medida que vai chegando.
© FCA - Editora de Informática 331
C#3.5
Uma outra razão para o uso de threads é a performance. Imaginemos que é necessário ler
de um ficheiro, um certo conjunto de dados e processá-los. O acesso a disco é uma
operação lenta. Enquanto os dados são lidos do disco, o processador da maquina está
simplesmente à espera que os dados sejam lidos, estando sem ser utilizado. Após a leitura
dos dados, é feito o seu processamento e, durante esse tempo, poder-se-ia estar a ler os
dados seguintes. No entanto, se a aplicação apenas tiver uma thread, tal não é possível.
Esta situação é ilustrada na figura 9.2. Usando apenas uma thread, todo o processamento
é feito sequencialmente.
•l
b) Execução com duas threads. Enquanto uma lê os dados, a outra faz o
processamento dos dados existentes.
| | Leitura de disco
Hl Processamento
Note-se que as threads não estão realmente a executar simultaneamente. Ou seja, criar
três threads para realizar cálculos intensivos que envolvem a utilização do processador
não irá fazer com que a aplicação execute três vezes mais depressa. Ao criar diversas
threads, o sistema operativo limita-se a criar um stack para cada thread e a gerir o local
do código em que cada thread se encontra a executar. Periodicamente, o processador
interrompe uma thread e coloca outra a executar. Neste processo, o sistema operativo
guarda o local onde se encontra a thread em questão, assim como todas as variáveis
associadas à thread, e restaura o estado que a thread que irá executar a seguir tinha
332 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE
quando foi retirada de execução. Cada thread executa durante um certo período de tempo
(chamado time slicé) ou até bloquear por algum motivo. Por exemplo, se uma thread
tentar ler dados de um ficheiro, esta irá bloquear até que a leitura termine. Entretanto,
outras threads podem executar.
Neste capítulo, iremos explorar de que forma é que, na plataforma .NET, se desenvolvem
aplicações que utilizam diversas threads, assim como de que forma é que se sincroniza a
sua execução. Não iremos, no entanto, abordar aspectos mais avançados de programação
concorrente e de sincronização avançada, uma vez que tais tópicos são bastante
complexos, muitas vezes, envolvendo uma compreensão profunda de mecanismos
internos dos sistemas operativos.
9. l GESTÃO DE THREADS
Criar uma nova thread na plataforma .NET é extremamente simples. O programador
necessita de criar uma classe com um método que não possua parâmetros de entrada e que
retome void. Este será o método onde uma nova thread começará a executar e
corresponde à assinatura do delegate system.Threading .Threadstart:
'public delegate void Threadstart() ;.
Vamos, então, implementar uma classe chamada worker. Esta classe irá fazer algo muito
simples. Irá dormir durante um certo tempo e, quando acorda, imprime uma string no
ecrã. A implementação de Worker poderá ser:
class Worker
!{ - ' :
Para criar uma nova thread que utilize este código, são necessárias duas coisas. Por um
lado, é necessário ter um objecto do tipo Worker. Ao mesmo tempo, é necessário criar um
objecto que representa a nova thread (o novo fluxo de execução). Esse objecto é do tipo
Thread. Na prática, faz-se:
Worker "dorminhoco = riew WorkerCLOOO, "oTáT")';"
Thread dorminhocojhread = new Thread (new Threadsta_rtCdorminhocp.Run));_
Neste caso, dorminhoco é um objecto do tipo Worker, que irá imprimir, de segundo a
segundo, a mensagem "Olá". dormi nhocoThread é o objecto que representa a nova
thread e é criado, especificando que esta irá executar o método RunQ de dormi nhoco.
Nesta altura, a nova thread ainda não está em execução. Para iniciar a sua execução, é
necessário chamar o método startQ no objecto que representa a thread:
A listagem 9.1 contém um programa com duas threads. Uma das threads dorme durante
um segundo e imprime a sua mensagem, enquanto a outra dorme durante cinco segundos,
também mostrando uma mensagem.
/*
* programa que ilustra a criação de threads.
*/
using System;
using System. Threading;
class Worker
{
p ri vate int Tempo;
private string Msg;
public Worker(int tempo, string msg)
this.Tempo = tempo;
this.Msg = msg;
class Exemplocap9_l
static void Mai'nÇstn'ng[] args)
9. l . l ComROLQ DE THREADS
Como vimos, sempre que se quer criar urna thread, é necessário criar um objecto que
represente essa thread. Esse objecto é do tipo Thread e permite ao programador, controlar a
sua execução. Vejamos os principais métodos associados à utilização de uma thread Para
isso, iremos utilizar o objecto dorminhocoThread, anteriormente criado.
O método suspendC) permite suspender a execução de uma determinada thread até que
Rés ume C) seja chamado. Por exemplo, se fizéssemos:
dorminhocoThread.SuspendXl;..
a thread dormi nhocoThread iria ficar parada até que se fizesse um:
dorminhocoThread\Resumé0j. __
Fazer um suspend Q numa thread já suspensa não tem efeito.
por exemplo, em casos em que uma thread está bloqueada durante muito tempo ou que
tem de ser notificada, assincronamente, de certos acontecimentos.
Finalmente, existe mais um método bastante útil. O método 3oin() permite esperar que
uma outra thread morra, suspendendo a execução da thread corrente até que isso
aconteça. Vejamos um exemplo simples.
Suponhamos, agora, que o método Run Q de Worker irá simplesmente imprimir uma linha
com o identificador da thread a correr. O identificador da thread irá ser, simplesmente, um
inteiro que lhe é passado como parâmetro, no construtor. Ou seja, Worker irá ser:
cias s Worker '•
// Total de escritas a realizar :
const int TOTALJA/RITES = 6 0 ; :
// identificador da thread
private int id;
public WorkerCint i d)
this.Id = id; ;
public void RunQ
{ :
for (int i=0; i<TOTAL_WRITE5; i++)
console.Write("{0}", id) ;
} ;
>
ao executar o programa, pode surgir:
Fim do programa
oooQooooppoooooooooopooooQpoopooppoopooooooooppooooooopooooo
Ou seja, "Fim do programa" surge antes da impressão dos zeros. Como ambas as threads
estão a executar ao mesmo tempo, de forma concorrente, neste caso, a thread origina]
imprimiu a sua mensagem antes da thread workerThr. Assim, a fim de garantir que a
thread original apenas executa após workerThr, basta chamar workerThr. Dói n Q:
class Testeaoin
{
static void Main(string[] args)
(com.)
l MÉTODO/PROPRIEDADE DESCRIÇÃO
9.2 SINCRONIZAÇÃO a
this.Id = id;
this. impressora = impressora;
}
public void RunQ
for (int i=0; Í<TOTAL_POEMAS; Í-H-)
// Escreve um poema
Thread. SleepCIO); .-,._,..,„
String poema = "Poema " + i + do utilizador id= + Id;
// imprime o poema
Impressora. Imprime(id, poema) ;
' Trabalho de l
;poema O do utilizador id=l
'— Fim do trabalho de l —
Trabalho de O
:poema l do utilizador id=0
'.— Fim do trabalho de O —
Trabalho de 2
Poema O do utilizador id=2
— Fim do trabalho de 2 —
Note-se que as impressões das várias threads não surgem ordenadas, uma vez que cada
thread. está a competir por utilizar o processador. O sistema operativo da máquina
encarrega-se de comutar entre as diversas threads em execução, de acordo com a
prioridade de cada thread e com o tempo que esta já esteve a executar.
trabalho. Quando esta thread terminou a sua execução, a thread correspondente à pessoa
4 voltou a executar, terminando a sua impressão: "— Fim do trabalho de 4 —".
Este não é o comportamento que se deseja para o programa. Um trabalho que se encontra
a ser impresso numa impressora nunca deverá ser interrompido a meio. (Imaginemos só a
confusão de folhas que sairiam, numa impressora real!).
Um bloco de código que não deverá ser interrompido a meio, em que apenas uma thread
o pode estar a executar em cada momento, chama-se uma secção crítica. Todo o código
presente numa secção crítica tem de ser executado de forma atómica, sem ser interrompido.
Resolver este problema no programa é muito simples. Basta utilizar a palavra-chave
lock:
public void Imprime(int idutilizador,l string msgj • • — - . - ........ . . . 5 ...
"lock" (this)..........~ ~ " ""' ..... " ~ ...... ...... " ..... "~ ~
{
console. writeLine(" ------ Trabalho de {0} ----- ", idutilizador) ;
Console.WriteLine(msg) ;
Console. writeLine("~- Fim do trabalho de {0} — " , idutilizador) ;
Console. writeuineQ ;
_ __ ___ __ _
y; "". ...... " "" :;"""". _:T:_________" . . ._T;:L:___:.:_____".;;:.. :r__::"~';"7~~
Sempre que se faz um lock sobre um objecto, apenas uma thread pode executar dentro
do bloco marcado como lock, para o objecto em causa. Todas as outras threads
bloqueiam à entrada do bloco de código, para aquele objecto, até que a thread que se
encontra a executar no seu interior saia. Ou seja, o bloco é executado atomicamente e
apenas por uma thread de cada vez.
Note-se bem que as threads têm de fazer o lock sobre o mesmo objecto. Se diferentes
threads executarem o lock sobre objectos diferentes, irá existir atomicidade na execução
do bloco de código em causa, mas apenas para as threads que façam lock sobre os
mesmos objectos. No caso do exemplo acima, o lock é feito unicamente sobre o objecto
da impressora em causa.
Se uma função pode ser invocada sem problemas, por diversas threads, diz-se que é
thread safe. A versão original de imprime Q não era thread safe. A listagem seguinte
apresenta a implementação deste programa, já com a protecção necessária, sendo thread
safe.
/*
* Programa que ilustra a utilização de lock.
*/
using System;
using System.Threading;
using System.IO;
class Impressora
lock (this)
Console.Writel_ine(" Trabalho de {0} ", idutilizador);
Console.WriteLine(msg);
console.writeLine("— Fim do trabalho de {0} --", idutilizador);
Console.WriteLineQ;
class Utilizador
private const int TOTAL_POEMAS = 20;
// Escreve um poema
Thread.Sleep(lO);
String poema = "Poema " + i + " do utilizador id=" + Id;
// imprime o poema
Impressora.imprimeCid, poema);
}
class Exemplocap9_2
Imaginemos que temos uma classe ContaBancaria, com um método Levantamento C),
que permite retirar dinheiro da conta. Um levantamento apenas pode ocorrer se existir
saldo suficiente:
class ContaBancaria
Caso várias threads possam chamar simultaneamente Levantamento C), pode haver um
grave problema. Suponhamos que tendo duas threads, a primeira chama Levantamento Q,
sendo bloqueada após o teste da condição 1 f (saldo > montante). Neste momento, a
condição era verdadeira, preparando-se a thread para fazer o levantamento em. causa.
Entretanto a segunda thread é escalonada e faz também um levantamento. Infelizmente,
esta thread retira todo o dinheiro existente na conta. Ao voltar a executar, a primeira
thread irá continuar a sua execução, efectuando o levantamento e resultando num saldo
negativo. Como podemos ver} este código deveria ser executado em exclusão mútua. Isto
é, apenas por uma thread de cada vez:
p u b l i c bool Levantamento (int montante)
lock (this)
i f (saldo > montante)
{
Saldo -= montante;
return true;
}
else
return false;
............ " ••
Examinemos, agora, um segundo exemplo, aparentemente ainda mais simples. Admitamos
que duas threads chamam o método incrementaVendasQ da classe Loja:
class Loja
!}_.„.. . „ . . . ... _ . ._
Aparentemente, é impossível haver problema: apenas estamos a incrementar o valor de
uma variável. No entanto, se verificarmos o código MSIL gerado pelo compilador,
encontraremos o seguinte1:
j.mêtfíòcl~ pubTicliidebysíg ~Trístance~void incrementãVendasQ cil managed
, // Code size 15 (Oxf)
.maxstack 3
IL_0000: ldarg.0
: IL_0001: dup ;
IL_0002: Idfld int32 Loja::Vendas
: IL_0007: l de.14.l
IL_0008: add
IL_0009: stfld int32 Loja::Vendas
: lL_OOOe: ret
.}..// e"d .pf method Loja::lncrementavendas
O ponto importante é que para incrementar a variável vendas, em termos de código
intermédio, são utilizadas quatro instruções:
"ÍU-0002:" Idfld ~~ int32 Loja: :Vendas //OBTÉM O VALOR DA VARIÁVEL DE
MEMÓRIA
:IL_0007: l de.n 4.l // CARREGA O VALOR l
ÍIL_0008: add // ADICIONA AMBOS
:Í_U=Q009.I _stfl_d . _ Jnt32. Loja:.:Vendas //COLOCA O RESULTADO EM MEMÓRIA
Se duas threads, por acaso, executarem este código mais ou menos simultaneamente,
pode acontecer o seguinte: uma thread carrega o valor da variável em memória (por
exemplo, 12) para um registo, após o qual é suspensa. A segunda thread faz o mesmo}
incrementa a variável (13) e coloca-a em memória. Finalmente, a primeira thread volta a
executar, incrementa a sua cópia da variável (com o valor antigo, 12) e coloca-a também
em memória. Resultado: duas threads executaram o método IncrementaVendasC) mas a
variável apenas surge incrementada uma vez (isto é, com o valor 13).
A lição a reter é simples: caso várias íhreads manipulem variáveis partilhadas entre elas,
é necessário garantir a atomicidade na sua actualização e manipulação.
Para conseguir examinar o código MSIL de uma classe, utiliza-se a aplicação "i l dasm. exe.
344 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE
Sempre que é necessário fazer urn l ock sobre diversos objectos, é muito importante obter
o lock de cada objecto pela mesma ordem em todas as threaãs. Caso contrário, poderá
surgir uma situação de impasse (deadlock) em que nenhuma das threads consegue
avançar. Por exemplo, suponhamos que temos duas threads que fazem lock em dois
objectos diferentes por ordem inversa:
// threadA ..... // threadB
lock (objl) lock (obj2)
lock (obj2) lock (objl)
// utiliza ambos os objectos // Utiliza ambos os objectos
Para terminar, falta referir de que forma se pode conseguir atomicidade no acesso às
variáveis estáticas de uma classe. Neste caso, não se pode fazer um lock sobre um objecto,
pois estas variáveis estão associadas à classe e não a um objecto em particular. Para se
obter atomicidade no acesso às variáveis estáticas de uma classe, deve fazer-se um l ock
sobre o objecto que representa o tipo de dados da classe. Isto é:
nock(typeof(lmpresso,ràj} v ' =; - ^
1 // Acesso atómico às>. var^áv^s ~es;M"t.1cas de Impressora
} ••••;.••• ;- .-•„.:•'.'*- " - • • .'
...... . . - : : " . . . " . ' - -. - - - . .. - - - - ~
Urna secção crítica representa um bloco de código que tem de ser executado
ARETER atomicamente por uma thread, sem ser interrompida.
&>*^
Atomicídade e Sempre que várias threads podem aceder simultaneamente a variáveis
secções críticas partilhadas, na maioria dos casos, tal acesso tem de ser feito em exclusão
mútua, numa secção crítica.
Se diversas threads fizerem lockCobj) { ... >, apenas uma thread
executa o bloco de código de cada vez. A execução é feita em exclusão
mútua.
Para obter exclusão mútua no acesso às variáveis estáticas de uma classe,
faz-se 1ock(typeof(Nomeoaclasse)) • £ • . . }
Para evitar situações de impasse, os 1 ock sobre diversos objectos devem ser
feitos pela mesma ordem, em todas as threads intervenientes.
9.2.3 ACUVSSEMUTEX
A classe Mutex representa um mutual exclusion lock. Ou seja, um objecto que permite
obter exclusão mútua na execução de secções críticas. A sua funcionalidade é idêntica ao
uso que demos até agora à palavra-chave l ock, só que sob a forma de uma classe.
"exc]usapMutua.ReleaseMutex'Qj" " _~
Na verdade, este código é equivalente a escrever:
,// Criação de um objecto' para "servi r de Mutex
'object mutex = new objectO;
lock (mutex)
// Execução com atoml cidade
Uma dúvida muito válida é perguntar porque é que não se faz simplesmente lock(obj)
quando se necessita de ter atomicidade no acesso a variáveis do objecto obj. A razão é
simples. Sempre que se executa uma secção crítica tendo atomicidade, apenas uma thread
executa de cada vez. Se numa classe, existirem diversas variáveis de instância não
relacionadas, mas que têm de ser acedidas com atomicidade, então, sincronizar todas as
threads da aplicação que têm de aceder a estas leva a uma notória perda dt performance.
Deve-se utilizar o mínimo de sincronização possível e em blocos de código breves. Neste
exemplo, deve-se criar diferentes objectos de sincronização para as variáveis não
relacionadas, sincronizando apenas as threads que estão a aceder às mesmas variáveis.
Para isso, cria-se vários Mutex, fazendo a sincronização com estes. Por exemplo, vejamos
a classe ContaBancarla, com os campos Saldo e Nomeei lente:
class contaBancarla
O método Levantamento () necessita de interagir com o campo Saldo, mas não com
Nomecllente. Por outro lado, uma propriedade cliente, que representa o nome do
cliente, apenas necessita de interagir com o campo Nomecllente. Se várias threads
podem interagir com esta classe, não deverão sincronizar todas no objecto. As que
invocam Levantamento C) apenas devem sincronizar entre si, assim como as que
interagem com a propriedade Cliente, apenas devem sincronizar com as que também o
fazem. Ou seja, o código para este exemplo pode ser algo semelhante a:
class ContaBancarla
. private Int saldo;
private string Nomecllente;
private Mutex SaldoMutex = new MutexQ;
: private Mutex NomeClIenteMutex = new MutexQ;
set
{
Nomeei i enteMutex, WaitOneQ ;
Nomeei lente = value;
Nomeei i enteMutex . Rei easeMutex C) ;
" Os objectos de Mutex permitem obter exclusão mútua entre threads que
ARETHR executam concorre n temente.
_.
Classe „ .
Mutex " Para criar um Mutex, faz-se: Mutex mutex = new MutexQ;
- Para criar uma secção crítica, acedida em exclusão mútua, faz-se:
mutex.WaitOneQ;
// Secção critica
mutex.ReieaseMutex();
- A classe Mutex evita que seja necessário sincronizar todas as íhreads que
utilizam um certo objecto, quando utilizam variáveis de instância não
interrelacionadas.
9.2.4 MONJTORES
Como vimos anteriormente, lock permite executar um bloco de código em exclusão
mútua. Ao fazer lock(obj), a sincronização acontece a nível do objecto referenciado por
obj. No entanto, os blocos lock permitem fazer mais do simplesmente executar em
exclusão mútua.
Na prática, o que a palavra-chave l ock faz é instruir a thread que a utiliza, a tentar entrar
no monitor em causa. O código do bloco l ock corresponde à execução dentro do
monitor:
: lock(obj)
í // , _ , , . ,monitor
.
T,hre.ad a executar em exclusão mu^|||d||g^^do
Na plataforma ,NET, não é possível obter uma referência para um objecto que represente
explicitamente o monitor de um objecto. No entanto, existe uma classe Monitor. Esta
classe apenas contém métodos estáticos, permitindo à thread corrente, realizar operações
sobre um certo monitor. Na verdade, ao executar um l ock, tal como no código anterior, o
que é executado pelo CLR é sensivelmente:
Monitor. E h t e r C o b j ) ; ~ ........ ~
,// Thrfead a executar em exclusão. mútua dentro do monitor
A razão porque estamos a referir tudo isto é devido a existirem duas operações muito
importantes, para além de entrar e sair de um monitor, que uma thread pode fazer quando
se encontra dentro do monitor. Essas operações são Walt Q e pui se C). Apenas a thread
que se encontra a executar dentro do monitor as pode invocar.
A operação waitQ faz com que a thread que se encontra no monitor bloqueie e,
simultaneamente, liberte o acesso ao monitor. Note-se que caso existam outras threads
bloqueadas à espera do monitor, nenhuma é acordada. No entanto, uma thread que tente
entrar no monitor, consegue adquiri-lo.
© FCA - Editora de Informática 349
C#3.5
Ao fazer pulseQ, a thread que se encontra dentro de um monitor faz com que uma das
threads que se encontre bloqueada no monitor, fique em condições de executar. Depois de
esta thread sair do monitor (a thread que faz pulseQ), a thread que foi colocada em
condições de executar irá tentar obter o monitor. Caso não consiga obter o monitor, volta
a bloquear. Existe uma outra operação semelhante a esta chamada PulseAll Q.
A diferença entre pulseQ e PulseAll C) é que esta última faz com que todas as threads
que se encontram bloqueadas no monitor fiquem em condições de executar. Isto é, de
competir para entrar dentro do mesmo.
Na prática, um monitor é implementado internamente com duas filas de espera. Uma das
filas contém as threads bloqueadas à espera de entrar no monitor. O CLR encarrega-se
automaticamente, de dar acesso a uma thread^ sempre que a que se encontra no monitor o
abandone. A outra fila identifica as threads bloqueadas devido a waltQ. Sempre que
existe um waitQ, a thread em causa bloqueia, é colocada nesta fila e liberta o monitor.
Sempre que existe um Pulse Q, uma das threads que se encontra na fila de WaitQ é
colocada na fila de entrada do monitor, ficando, então, bloqueada até lhe poder ter acesso.
PulseAll C) coloca todas as threads bloqueadas devido a waitO, na fila de threads à
espera de entrar no monitor. A figura 9.3 ilustra o processo.
Caso o leitor nunca tenha feito programação concorrente, tudo isto pode parecer muito
complexo. No entanto, na prática, e após algum hábito, toma-se relativamente fácil de
utilizar. Vamos, agora, ver um exemplo prático do tipo de estruturas que os monitores
permitem implementar.
MONITOR
Apenas uma thread pode
executar no seu Interior
Em programas com várias threads, muitas vezes, surge a necessidade de ter uma ou mais
threads a enviar dados para uma ou mais threads que os irão tratar. Dado que a
35O © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE
velocidade a que as threads que produzem dados (produtores) pode ser bastante diferente
da velocidade a que as threads que os tratam (consumidores) os conseguem processar,
existe um buffer entre ambas. Se uma thread produtor não dispuser de espaço no buffer
para colocar um bloco de informação, bloqueia até que haja espaço. Caso uma thread
consumidor não disponha de dados para processar (buffer vazio), bloqueia até que haja
dados produzidos. A figura 9.4 ilustra a ideia.
Imaginemos que uma thread chama colocaQ. A primeira coisa que acontece é um
pedido de acesso ao monitor. Caso exista alguma thread a colocar ou a retirar um
elemento, a thread irá bloquear até poder aceder ao monitor. Suponhamos que a thread
entra no monitor. A thread, então, verifica se existem posições livres na tabela. Enquanto
não existirem posições livres, a thread bloqueia (operação waltQ). Caso isto aconteça,
irá ser uma thread consumidor que lhe irá fazer um PulseQ, quando houver uma posição
livre.
Um aspecto importantíssimo é que quando se faz este tipo de testes, na condição testada,
é sempre necessário colocar um w h l l e e não um 1f. Tal como dissemos quando
descrevemos pulseQ, quando esta operação é feita, uma thread bloqueada é colocada
em condições de executar. No entanto, a thread irá competir pelo acesso ao monitor.
Neste exemplo, caso haja o azar de uma outra thread conseguir realizar um colocaQ,
antes da ihread que estava bloqueada em wal t () conseguir adquirir novamente o monitor
e continuar a sua execução, então, a posição da tabela que estava livre já não o estará. Ou
seja, si otsLl vres estará a 0. O ciclo whl l e garante que a thread só consegue continuar a
execução, caso slotsLlvres seja realmente diferente de 0.
Após o ciclo whlle, é garantido que a thread tem o monitor, logo, está em exclusão mútua,
e que existe uma posição livre. A thread coloca, então, o seu elemento. O buffer
apresentado funciona em regime circulai*, o que implica que ao chegar à última posição da
tabela, a próxima posição a escrever terá índice 0. Isso é conseguido com a operação resto
de divisão com o tamanho da tabela.
A última operação realizada pela thread é um PulseAll C). Isto garante que caso exista
alguma thread consumidor à espera de itens, esta irá verificar a existência de novos itens.
Uma questão bastante importante neste exemplo é que é feito um PulseAll Q e não um
pulseQ. Como só estamos a utilizar um monitor, caso fizéssemos um PulseQ simples,
então, poderia ser colocada em estado pronto a executar, uma thread que se encontrava
bloqueada no waitQ de colocaQ e não uma thread consumidor. Ou seja, poderia ser a
thread errada a ir verificar a condição. PulseAllQ garante que todas as threads que
estejam bloqueadas devido a um waltQ irão verificar a suas condições, inclusivamente
as threads produtor. No entanto, estas últimas irão bloquear novamente, caso, entretanto,
não tenham surgido posições livres no buffer. A condição wh i l e garante isso.
A listagem 9.3 mostra um programa que faz uso da classe Buffersincronizado. Existe
apenas um produtor e um consumidor. O produtor envia 20 inteiros não negativos para o
consumidor. No final envia -l indicando que o consumidor deverá terminar.
O consumidor lê cada valor do buffer e envia-o para o ecrã. Para além disso, dorme
500 milissegundos antes de voltar a retirar um novo valor. Ao executar o programa, é
possível ver que o produtor consegue colocar os primeiros 5 valores imediatamente no
buffer (isto porque o tamanho do buffer é 5). No entanto, após estes primeiros valores
estarem no buffer, apenas será possível colocar um novo valor no buffer de 500 em
500 milissegundos, o tempo que o consumidor demora a retirar um valor do buffer.
/*
* produtor/consumidor usando um monitor.
*/
using System;
using System. Th reading;
class Buffersincronizado<Tltem>
{
private TItem[] Itens;
p ri vate int slotsLivres;
private int SlotsOcupados;
private int PosLeitura;
private int PosEscrita;
public BufferSincronizadoCint tamanho)
Itens = new TItem [tamanho] ;
SlotsLivres = tamanho;
SlotsOcupados = 0;
PosLeitura = 0;
PosEscrita = 0;
// Acede ao monitor
lock (this)
// Bloqueia até que exista um dado para retirar
while (slotsOcupados =- 0)
Monitor. Wait(this) ;
// Existe um dado para retornar e estamos
// em exclusão mútua. Retira o elemento.
++SlotsLivres;
— SlotsOcupados;
dadoARetornar = Itens [posLeitura] ;
PosLeitura = (PosLeitura + 1) % Itens. Length;
// Notifica que existe mais um slot livre
Monitor.PulseAll (this) ;
// Retorna o objecto
return dadoARetornar;
class produtor
this.Buffer = buffer;
}
public void RunQ
for (int i = 0; i < TOTAL_ENVIOS ; i++)
Buffer. Coloca(i) ;
console.WriteLine("produtor: Coloquei {0}", i) ;
}
Buffer.Coloca(-l) ;
class consumidor
private Buffersincronizado<int> Buffer;
public Consumi dor (Buf f erSincronizado<int> buffer)
this.Buffer = buffer;
}
public void RunQ
int recebido;
do _
class Exemp1ocap9_3
{
const int TAMANHO_BUFFER = 5;
Como o leitor deve notar, fazer programação concorrente, utilizando threads não é trivial,
sendo possível existirem erros muito subtis. Há que usar de muita precaução.
Relativamente ao uso de monitores, existem duas regras-de-dedo que se aplicam na maior
parte das situações. Primeiro: ao testar uma condição para bloquear uma thread num
monitor, o teste da condição deve ser feito com while e não com i f. Isto garante que a
condição é efectivamente verdadeira após a thread retornar de WaitQ. Caso contrário, a
thread é novamente bloqueada. A segunda regra-de-dedo é que, normalmente, deve
utilizar-se PulseAllC) e não pulseQ. Tipicamente, isto implica que todas as threads
testam se a sua condição já se verifica. Se as condições estiverem protegidas com while,
apenas as threads em condições de executar irão continuar o seu processamento.
Monitor.puiseAll(obj);
>
cria um semáforo que representa o conjunto de impressoras disponíveis numa sala de uma
reprografía, estando todas disponíveis. O primeiro parâmetro representa o valor inicial do
semáforo; o segundo, o valor máximo que poderá tomar (número de recursos).
Os semáforos são primitivas básicas existentes em todos os sistemas operativos modernos. Na verdade, os
monitores, assim como outros elementos de sincronização, são normalmente construídos à custa de
semáforos.
Esta forma de implementar semáforos difere radicalmente do que normalmente é encontrado nos sistemas
operativos. Tipicamente, os semáforos são criados indicando apenas o número de recursos actualmente
disponíveis. O número máximo de recursos associados é tipicamente "infinito".
358 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE
Suponhamos, agora, que existe uma rotina imprime C) que envia texto para uma das
impressoras disponíveis ou bloqueia até que possa imprimir, caso estejam todas ocupadas.
Esta rotina é executada concorrentemente por várias threads que tentam enviar os seus
textos. Uma implementação simplificada deste método será:
publlc void imprimeCstrlng texto)
// Espera que exista"uma impressora" livre
. impressoras.waitOneQ;
EnviaparalmpressoraCtexto, idimpressora);
Tock (impressorasLivres)
ImpressorasLivres.Enqueue(idlmpressora);
Impressoras . Rei easeQ ;
> _
Note-se que a obtenção de um identificador de impressora tem de ser feito em exclusão
mútua, pois podem existir diversas ihreads a executar ao mesmo tempo, após waitoneO-
Neste caso, o lock é feito sobre o objecto impressorasLivres. Após a impressão, é
também necessário libertar o identificador, tendo isso de ser feito em exclusão mútua.
A listagem 9.4 mostra o exemplo completo do uso de imprime Q. Este programa cria 5
threads, em que cada thread tenta imprimir simultaneamente um texto. Existem 3
impressoras disponíveis numa entidade Reprografia, para as quais os trabalhos são
enviados. Cada trabalho demora entre O e 15 segundos a ser impresso.
/*
* Exemplo de programação concorrente utilizando semáforos.
*/
using System;
using System.Threading;
using system.Collections.Generi c;
class Reprografia
private const int TOTAL_IMPRESSORAS = 3;
private Queue<int> impressorasLivres;
private Semaphore Impressoras;
private Random rnd;
/ftVf ir íVVí ft Vr A A fí ir Vr * A ft * íí * ií Aft* * íf -Í! A * * * * * * * ir ir * * * * * A /
public ReprografiaQ
impressoras = new Semaphore(TOTAL_iMPRESSORAS, TOTAL_IMPRESSORAS);
impressorasLivres = new Queue<int>O ;
for (int 1=0; 1<TOTAL_IMPRESSORAS; I-H-)
impressorasLivres.EnqueueCi);
rnd = new RandomC);
lock (impressorasLlvres)
Impresso rãs Li vres.Enqueue(1dlmp ressona) ;
Impressoras . Rei easeQ ;
}
class cliente
{
private Int Id;
private Reprografia Loja;
public Cl1ente(1nt 1d, Reprografia loja)
Id = I d ;
Loja = loja;
}
class ExemploCap9_4
statlc void Main(string[] args)
Reprografia loja = new ReprograflaC) ;
for (int 1 = 0; 1<5; i++)
Cliente cliente = new Cl i ente (i, loja);
(new ThreadCnew ThreadStartCclIente.Run))) .startC) ;
Para criar um objecto do tipo AutoResetEvent, é necessário indicar o seu estado inicial.
true indica que o evento se encontra sinalizado, f ai se indica que ainda não ocorreu:
AutoResetEvent novosDádosDlsporijvens ="new AutoResetEvent(fãlse);
Uma thread que queira esperar que o evento seja sinalizado, apenas tem de chamar o
método waitOneQ:
novospadpsDlsponiveis^.WaltQneQ.j"" 7
A thread em causa irá bloquear. Uma thread que queira sinalizar o evento, apenas tem de
chamar set C):
novosDadosDl sponi yej s;. Set Q";"
Uma das threads que se encontra bloqueada em waitoneO é então desbloqueada, sendo o
evento colocado automaticamente no estado não sinalizado. Daí o nome AutoResetEvent.
Quando se coloca um evento como estando sinalizado, este irá permanecer sinalizado até
que uma thread faça WaitOneQ. Ou seja, o comportamento não é semelhante ao
Monitor, pui se (), em que pui s e C) apenas coloca uma thread em condições de executar.
Se não existir nenhuma, o pui se Q é perdido.
Note-se que estes eventos não estão relacionados com o mecanismo de eventos associado à programação
baseada em componentes.
364 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE
9.2.6.2 CLASSETHREADPOOU
Embora as threads sejam muito úteis e possam levar a aumentos significativos de
desempenho, demoram algum tempo a criar. Em certas aplicações, como por exemplo
servidores de web ou bases de dados; normalmente, utiliza-se uma thread por cliente.
Para evitar andar a criar e a destruir threads, tipicamente, implementa-se um esquema em
que existe um conjunto de threads que é inicialmente criado e que ficam bloqueadas no
objecto de sincronização. Sempre que existe um cliente ou um pedido a tratar, é
desbloqueada uma das threads livres, sendo esta thread utilizada no processamento em
causa. Quando o processamento termina, a thread bloqueia novamente. A este tipo de
estruturas chama-se \rnis. pool de threads.
O programador pode utilizar esta classe para escalonar trabalhos que devem ser
executados pela. pool Para adicionar um trabalho à. pool, utiliza-se o método estático:
Ta^^ cáljback) "" '
cal l back representa um delegote que encapsula o objecto e o método a executar, quando
existir uma thread livre para efectuar o processamento.
É de referir que a utilização desta classe não é suportada em todos os sistemas operativos,
sendo lançada uma excepção nos sistemas que não a suportam.
Um outro tipo de sincronização, que por vezes é necessário, corresponde à situação onde
existe uma ou mais threads a consultar um certo conjunto de dados (leitores) e, de tempos
a tempos, existe uma ou mais threads que actualizam esses dados (escritores). Enquanto
as threads estão a ler, todas as threads podem ler simultaneamente, não devendo estar a
executar em exclusão mútua6. Quando existir uma thread a fazer uma actualização, esta
thread deverá executar em exclusão mútua. Não podem existir threaãs a fazer leituras,
nem outras threads a realizar actualizações. Este tipo de situações ocorre, por exemplo,
em bases de dados bancárias, em que várias sucursais consultam ao mesmo tempo o saldo
de um cliente e, de tempos a tempos, existe uma que actualiza o valor do saldo. Durante a
actualização, é necessário garantir atomicidade.
Se as threads apenas estão a ler, então, podem executar concorrentemente. Os problemas só acontecem se
existir uma thread a actualizar valores enquanto outras lêem.
366 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE
9.2.6.4 CLj\SSETlMER
A classe Tl me r não é propriamente uma classe de sincronização, mas sim uma classe que
permite chamar, periodicamente, um certo método num objecto. O construtor de Timer
mais utilizado possui o seguinte protótipo:
íTimeTCTi'merCallbãck^clJJ"b>íKZIP&"êct .estado^ int Jnjc1qL Int período) ;
Uma questão importante é que a thread utilizada em Timer é uma thread de background.
Ou seja, quando não existir nenhuma outra thread a executar no programa, o programa
termina.
/*
* Programa que ilustra o uso de Timer.
*/
using System;
using System.Threading;
class Mostrador
Segundos = 0;
class ExemploCap9_6
const int SEGUNDO = 1000; // l s = 1000 ms
public static void MainQ
Console.ReadLineQ ;
No entanto, todos os tópicos mencionados constituem apenas uma pequena parte dos
serviços de acesso à rede, disponíveis na plataforma .NET. Em particular, a arquitectura
para partilha de objectos remotos entre aplicações, chamada Remoting, não será coberta
neste livro. Para os programadores de Java, o Remoting é sensivelmente o equivalente ao
RMI (Remote Method Invocatioii). Dada a sua extensão, não é possível abordar a sua
utilização, num capítulo de acesso à rede.
Listagem 10.1 — Programa que carrega uma pagina web e. a mostra na consola
(ExemploCaplO__l.cs)
Note-se que é utilizado um bloco try~catch para proteger o caso de haver uma excepção.
Existem várias razões para a leitura falhar: o servidor pode não ser conhecido, a página
pode não estar disponível, o utilizador pode não ter permissões para ler a página, a rede
pode falhar, entre outras. É sempre necessário proteger este tipo de código por um bloco
try-catch 1 .
Assim como é possível receber informação de um servidor web, também é possível enviar
informação para um servidor. Para enviar um ficheiro local para um servidor web,
utiliza-se o método UploadFileQ:
const string FICHEIRO_LOCAL = "index.html";
Webclient cliente = new WebdlentÇ);
cl i ente. Upl oadFi l eCJrttp: //wyvw. dei . u c.pt/i ndex. html", _ FlCHElRp_LOCAtO ;
Claro que para esta chamada funcionar, o servidor web tem de permitir realizar este tipo
de operações. O método uploadoataQ funciona de forma idêntica, mas com a diferença
de que a informação a enviar para a rede advêm de uma tabela de bytes especificada no
programa. Por exemplo:
byte[] imagem;
'// código que gera uma imagem na tabela imagem.
Até agora, temos estado a utilizar a classe Webclient apenas para aceder a servidores
web. Isto é, servidores que suportam o protocolo HTTP, o que é indicado na localização
do recurso a utilizar, isto é, no Unifonn Resource Jdentifier - URI. No entanto, a classe
Webclient foi criada por forma a ser genérica e aceder a recursos de rede diversos,
identificados por diferentes URI. Por exemplo, servidores de FTP ou simples acesso a
ficheiros. Actualmente, esta classe apenas suporta URI começados por http, https e.file.
1 Devemos referir que o código deste exemplo não é totalmente robusto. Caso exista uma excepção, a stream
não está a ser fechada. A fim de não complicar o exemplo, o tratamento de erros foi ligeiramente
simplificado, especialmente, porque o método Cl ose C) também pode lançar excepções.
© FCA - Editora de Informática 371
C#3.5
MÉTODO/PROPRIEDADE DESCRIÇÃO
Uploadvalues (string address, : Envia um conjunto de pares <nome/ va[or> para o recurso
NameValueCoTlection data) ; : de rede identificado em address.
Vamos ver apenas mais um exemplo de utilização desta classe. Imaginemos que
queremos fazer uma pesquisa no motor de procura Google (http://www.google.com'),
procurando a palavra "Computador". Para isso, é necessário fazer um pedido GET2,
usando o URI http://www.google.com/searcii e indicando que o campo "q" (de query)
deverá ser "Computador". Em termos do protocolo http, isso corresponde a fazer GET
utilizando a cadeira de caracteres: c'http://www.google.com/search?q=Computador". Na
prática, o programador não tem de codificar esta cadeira de caracteres manualmente.
Basta adicionar à propriedade Querystring os campos a enviar. No código seguinte, é
Não iremos entrar em detalhes sobre as formas como são feitos os pedidos aos servidores web. Apenas
importa saber que os principais pedidos são GET, que permite obter informação do servidor, e POST, que
permite enviar informação para o servidor. Quando se faz úrn GET, a informação a obter é codificada
directamente no URI,
372 © FCA - Editora de Informática
ACESSO À INTERNET
ARETER " Para aceder a um recurso na Internet, utiliza-se a classe Webcl 1 ent.
" Os métodos Webcllent.DownloadFlleQ e WebClient.UploadFlleO
Acesso a
permitem obter e colocar ficheiros num servidor.
recursos " Os métodos Webdlent.OpenReadO e Webdlent.openWriteO permitem
Standardrda , obter streams para leitura e escrita num servidor.
Internet l
" Sempre que se acede a um recurso na rede, é necessário proteger as chamadas
com um bloco try-catch.
Vejamos como é que, usando estas classes, se pode obter uma página web e mostrá-la na
consola. Para criar um novo pedido, utiliza-se o método estático WebRequest.createQ
este método devolve uma instância de WebRequest específica do protocolo em causa.
O código seguinte obtém a página em http://www.dei.uc.pf:
WebRequest pedido = WebReciuèst.CreàteC"http:/7www'.de1 ~.uc.pt/"D; :
WebResponse resposta = pedldo.GetResponseQ;
instância de webRequest, mas sim de uma classe derivada desta. Essa classe representa a
implementação do protocolo específico em causa, sendo possível ao programador, se
necessário, fazer uma conversão explícita para a classe derivada. Por exemplo, ao ser
invocado WebRequest.create("http://www.dei .uc.pt/"), é devolvida uma instância
de HttpWebRequest 4 . Ao chamar createQ, este método examina a natureza do pedicjo
que lhe é passado, criando uma instância da classe concreta apropriada.
Hoje em dia, tipicamente, um computador não faz o carregamento directo das páginas
web de um servidor. É cada vez mais frequente a existência de uma máquina intermédia,
chamada proxy, que é consultada antes de os pedidos serem feitos ao servidor web final.
O que um proxy faz é realizar os pedidos em nome dos clientes, guardando também as
páginas web resultantes. Caso o proxy receba um pedido para o qual já tenha armazenado
a página web em questão, então, em vez de redireccionar o pedido para o servidor web
real, pode responder imediatamente com a página web que tinha armazenado. Como um
proxy é utilizado por diversas máquinas, isto leva a um aumento significativo de
desempenho, uma vez que aceder a um proxy é tipicamente muito mais rápido do que
aceder a um servidor que se encontra em parte incerta na Internet.
Ao utilizarmos as classes da plataforma .NET que fazem pedidos à rede, caso não exista
nenhum servidor de proxy configurado, os pedidos são feitos directamente aos servidores
finais5. No entanto, é muito fácil configurar um proxy para ser utilizado para todos os pedidos
efectuados. Para isso, basta configurar a propriedade estática GlobalProxySelection.
. select. Por exemplo, suponhamos que, no início de um programa, queremos configurar
o servidor de proxy para http://proxy.dei.uc.pt:8080/t antes da classe webcl i ent ser
usada. Para isso, basta fazer:
[77~ Confi guração do proxy
IwebProxv proxv = new WebProxvC"http://proxv.dei.uc.pt:8Q80/"):
Caso o programador não queira configurar um proxy para ser utilizado globalmente, mas
apenas um proxy para ser utilizado num pedido, tipicamente, pode utilizar a propriedade
proxy da classe correspondente. Por exemplo, para fazer uni pedido via. proxy, utilizando
a classe webRequest, basta fazer:
'WebProxy proxy = new webproxyC"httpV//proxy.de1 ;uc.pt:8"0"80/") ;
WebRequest pedido = WebRequest.CreateC http://www.google.com/");
pedido.Proxy = proxy;
Na Internet, cada máquina tem um nome e um endereço associado. O nome é uma cadeia
de caracteres que representa a máquina em questão. Por exemplo, ivww.fco.pt é o nome
de uma máquina. O endereço da máquina representa a sua morada na rede, sendo o
mesmo utilizado para conseguir fazer chegar informação à mesma. Um endereço é um
número de 32 bits, sendo tipicamente representado como quatro grupos de dígitos, cada
grupo de dígitos podendo tomar valores de O a 255. Por exemplo, 195.22.2.66 representa
um endereço de uma máquina. A estes endereços chamam-se endereços IP. A
representação de um endereço IP em quatro grupos de dígitos denomina-se por endereço
na forma decimal.
Na Internet, existem servidores especiais, chamados Domam Name Servers (DNS), que
se encarregam de manter as tabelas com as correspondências entre nomes e endereços. Na
prática, sempre que é feito um pedido a um servidor, por exemplo, a http://www.fca.pt, é
necessário converter o nome do servidor num endereço. Só depois, pode o pedido ser feito
realmente ao servidor. Um DNS funciona como uma lista telefónica onde se pode
consultar o endereço de uma pessoa a partir do seu nome. Só dessa forma é possível fazer
chegar correspondência à pessoa.
possível converter uma cadeira de caracteres num endereço utilizando, o método estático
parseQ. O extracto de código seguinte ilustra o uso desta classe:
string endereço = "195.22.2 . 66" ;"" " "
: try
/*
* Programa que ilustra a utilização de um DNS.
*/
using System;
using System.Net;
class ExemploCaplO_2
static void Main(string[] args)
i f (args.Length != 1)
console.Writel_ine("Argumentos inválidos. Indique o nome " +
"da máquina.");
return;
string maquina = args[0];
try
// obtém informação sobre a máquina
iPHostEntry info = Dns.Resolve(maquina);
Console.WriteLine("Nome principal:");
Console.Writeí_ine("\ {0}", info.HostName);
// Endereços da máquina
Console.writel_ine( Endereços da máquina:");
IPAddress[] endereços = info.Addresstist;
foreach (iPAddress endereço i n endereços)
Console.WriteLine("\ {0}", endereço);
catch (Exception e)
{
Console.WriteLine(e);
Listagem 10.2— Programa que carrega uma pagina webe. a mostra na consola
(ExemploCaplO_2.cs)
Por exemplo, anteriormente, vimos que para fazer uma pesquisa no motor Google pela
palavra "computador", é necessário haver um URI com a forma:
http:/Avww.google.com/search?q~computador
Para criar um Uri com este endereço, que pode ser utilizado em classes como Webcl i ent,
basta fazer:
'Uri questão '-' new Uri CMnttpT//www. gooçfl é". còm/search?q=computádqrl_l) ',_ ,
Utilizando o objecto questão, é possível consultar partes do URI. Por exemplo, ao fazer:
!Uri
questão = new UriC n http://www.google.com/search?q=cõmputádòr");
Console.writeLine("{0}\n", questão.AbsoluteUri);
; Absolutepath: /search
AbsoluteUri: http://www.google.com/search?q=computador
•Host: www.google.com
Port: 80 \:
! Scheme: http _ __ _ i
Um ponto importante é que, ao criar um novo Uri, por omissão, é feita a codificação
necessária dos campos presentes6. Por exemplo, se existirem acentos, vírgulas, espaços,
ou similares, estes são automaticamente codificados de acordo com as especificações do
W3C. A propriedade AbsoluteUri mostra o Uri tal como ele é, incluindo os caracteres já
codificados. O método ToStríngO mostra o Uri numa forma perceptível, em muitos
casos, sem incluir esta codificação.
Um aspecto importante de uri é que os seus campos, após este ter sido criado, são apenas
de leitura. Caso seja necessário manipular os campos de um URI ou especificá-los
parcialmente, então, utiliza-se a classe uri BUÍ l der. Por exemplo, vejamos a construção
de um URI em que a expressão a pesquisar no Google se encontra numa string7
questaoDoUtil i zador:
77 String com" a questão' dó"utilizador"" """
string questaoooutilizador - "música clássica";
6 Existem construtores com um parâmetro que indica se deve ser feita a codificação ou não.
7 Note-se que não é necessário especificar todos os componentes do URI utilizando propriedades. É possível
especificar o URI completo, ou quase completo, como uma string no construtor. Não o fazemos aqui a fim
de mostrar algumas propriedades de Uri Bui! der.
378 © FCA - Editora de Informática
ACESSO À INTERNET
;questaò.scheme = "http";
.questão.Rost = "www.gooqle.com";
questão.path = "/search ;
jquestao.Query = questaoDoUtilizador;
Embora não seja frequentemente referido, é possível utilizar outros protocolos de transporte e invocação,
apesar destes serem maioritariamente baseados em HTTP.
© FCA - Editora de Informática 379
C#3.5
Uma das grandes vantagens dos web services é permitirem a um servidor na Internet,
publicar um conjunto de funções (serviço), sendo possível aceder a estes, utilizando
protocolos bem estabelecidos e que já são omnipresentes na Internet. Isto também permite
uma maior interoperabilidade entre sistemas existentes.
A grande diferença entre a utilização de web services e tecnologias como o CORBA, Java
RMI e DCOM é que, no caso dos web services, estes assentam numa estruturajá existente
na Internet: servidores web e XML. No entanto, apesar dos web services assentarem em
SOAP e XML, essa utilização é invisível para o programador, que apenas vê classes e
métodos.
Em termos tecnológicos, existe, ainda, uma peça importante dos web services: a WSDL
(Web Service Descríption Languagé). Para um programador conseguir utilizar um serviço
que se encontra num servidor web, é necessário que este conheça o serviço. Isto é, que
saiba os métodos e os tipos de dados envolvidos. Quando um web service é publicado,
conjuntamente, é publicada a sua especificação em XML. Essa especificação é escrita em
WSDL. Na prática, o WSDL é apenas a especificação do conjunto de tags, bem conhecidas,
que podem ser utilizadas quando se descreve um serviço. Mais à frente, iremos ver um
exemplo de uma destas especificações, mas, do ponto de vista do programador, tal não é
importante.
Agora que já vimos um pouco da teoria por detrás dos web services, vamos, então, ver de
que forma é que estes são criados e usados. Para criar web services, é necessário ter um
servidor web que suporte SOAP a executar. Os web services correm dentro deste servidor.
Em particular, na tecnologia Microsoft, é utilizado o Internet Information Server (ES).
Nesta secção, partimos do princípio de que o IIS se encontra a correr na máquina local.
Para criar web seivlces na plataforma .NET, tanto é possível utilizai' o VisualStudio.NET
como realizar as operações necessárias manualmente. Embora o VisualStudio.NET disponha
já de uma excelente interface para criação de web seivices, nós optámos por mostrar os
passos envolvidos na criação manual de web seivices, evitando, assim, que surjam dúvidas
relativas a aspectos que o VisualStudio.NET escondo, do programador.
.using system;
usnng System.web.Services;
publlc class Algoritmos : Webservice
. [WebMethod]
public int Soma(int valorl, int valor2)
return valorl+valorZ;
O ficheiro de um web seivice tem de começar com uma linha contendo a directiva
@webservice. Isto indica que o ficheiro corresponde a um web seivice. Neste caso, é
indicado que a linguagem utilizada no serviço é C# e que a classe correspondente ao
serviço se chama Algo ritmos.
9 O leitor deverá ter algum cuidado com a utilização de proxies. Caso exista um proxy configurado, deverá
fazer o bypass do proxy para acessos locais. Caso não o faça, é provável que o endereço http://localhost
seja visto como representando a máquina ao proxy.
10 Não é obrigatório que a classe derive de WebServi cê, mas, normalmente, tal é conveniente. Ao fazer com
que a classe derive de Webservice, existe bastante informação sobre o cliente do web service que fica
disponível, como campos protected da classe base.
© FCA - Editora de Informática 381
C#3.5
E este é todo o processo. Para colocar o web service a funcionar, basta criar um directório
no servidor web e copiar para lá o ficheiro. Assumindo que foi criado um directório
"Matemática" na raiz do directório dos documentos do IIS (isto é, "wwwroot\, basta c
Algoritmos
Som»
T«l
HK fctljuíng a t twncfe SOA* Itawl *nd rtiBBim- The pliIchaMiri fi Md ta bt rt&ttHt Mti Ktud nlu«J-
T r~HiigÍP"ne
Usando esta página, é mesmo possível testar o web service, preenchendo os parâmetros
somai e soma2. Por exemplo, se o mesmo for testado com os valores 10 e 20, surgirá uma
nova página web com a seguinte informação:
<?xmT vérsiòh="1.0" encoding="utf-8" ?>
<1nt xmlns="http://tempun' .qrg/">3p</int>
Olhando com atenção, é possível ver o resultado 30. O leitor mais atento, certamente, que
se está a perguntar que representa http://tempuri.org/. Na verdade, deveria estar
especificado um nome único, associado ao produtor do web service, que funciona como
um espaço de nomes, http://tempuri.org/ é utilizado nos casos em que não é especificado
um espaço de nomes para o web service, representando este URI, um espaço de nomes de
desenvolvimento e teste. Na secção 10.2.3, veremos como é que é colocado o URI
correcto.
Como se pode ver, criar um web service é muito simples. Urn ponto bastante importante
de que o leitor deverá lembrar-se é que a interface web, que acabamos de mostrar, é
382 © FCA - Editora de Informática
ACESSO À INTERNET
apenas para teste de web services e para verificar se estes se encontrara disponíveis. Os
web seivices, propriamente ditos, destinam-se a ser invocados por código em aplicações
definidas pelo programador. Será isso que iremos mostrar de seguida.
GO m pii
<s:sequence>
<s:element minOccurs="l" maxõccurs="i" nàme="va1orl""type-"s:int"
<s:element m1nOccurs="l"_maxoccurs=nl^_ name="valo_r2" type="s:int"
</s:sequence>
</s:complexType>
</s:element>
s:element narne== SpmaResppnse > _ _ _
<s: comp.l exType>
<s:seqúence>
<s:elemerit mihdccurs="l" maxõccurs="l" name="SomãRèsúlt"
type="s;int" _/>
</s:sequence>
</s:complexType>
</s:element>
Ou seja, a operação soma C) leva como parâmetros dois inteiros (int) de nome vai o ri e
valor2, resultando num inteiro (int) de nome somaResult.
Algo que não foi feito na listagem 10.3, mas que deverá ser feito em código robusto, é
proteger o acesso ao web service com um bloco try-catch. Existem diversas razões
pelas quais um pedido pode falhar, desde um problema na rede, até ao ficheiro "asmx" ter
sido apagado do servidor. Tipicamente, as excepções lançadas são do tipo System.
.Net.WebException, mas podem ocorrer outras.
Finalmente, em conjunto com web sennces, existe um outro conjunto de ficheiros que
podem ser definidos e que, normalmente, são automaticamente criados pelo VisualStitdio.
.NET. No entanto, estes ficheiros não são essenciais.
Existe um protocolo chamado DISCO que permite a clientes remotos, descobrir quais os
web seivices presentes numa máquina. Ou seja, não basta publicar a especificação de web
sennce em WSDL, é necessário haver uma forma, de clientes remotos encontrarem essa
especificação. O protocolo DISCO permite exactamente isso. Se no directório de um web
service for colocado um ficheiro com o seu nome e com a terminação "disco" (por
exemplo: "Algoritmos.disco"), então, este ficheiro é utilizado para definir quais os web
sennces que são expostos pelo servidor e os schemas WSDL associados. Isso permite a
clientes remotos, fazerem a descoberta dinâmica do serviço1 .
Existe também um outro ficheiro Standard chamado "Web.config" que pode ser colocado
na directoria do web service. Este ficheiro permite definir configurações específicas para
o web sennce, como, por exemplo, parâmetros de autenticação.
[WebMethod]
public int soma(int valorl, int valor2)
return valorl+va~lor2;
}
11 Note-se que, no exemplo que apresentámos, o cliente sabe qual é o URI do web service. O que o DISCO
permite é evitar que o URI tenha de ser conhecido à partida, existindo um processo de descoberta
dinâmica.
386 © FCA - Editora de Informática
ACESSO À INTERNET
Existe uma variável privada, chamada Total invocações, que é incrementada sempre que
ContalnvocacoesO é executado. Aparentemente, sempre que o método fosse invocado,
Total invocações seria incrementado, sendo o resultado retornado para o cliente. Tal não
acontece. Na verdade, sempre que existe uma invocação, é criado um novo objecto que é
utilizado para fazer a invocação. Não existe um objecto permanentemente no servidor a
atender pedidos remotos12.
Context, que encapsula toda a informação HTTP específica do pedido que está
em decurso. Em particular, a informação do cabeçalho do pedido e o objecto de
resposta;
12 Existe urn API chamado Remoting que permite ter esse tipo de funcionalidade.
© FCA - Editora de Informática 387
C#3.5 _
De todas estas propriedades, iremos, apenas, examinar de que forma se pode guardar
informação, relativamente a uma aplicação e a uma sessão.
: [WebMethod]
: public int ContainvocacoesQ
Int total invocações = (Int) Appl1cat1on[TOTAL_lNVOCACOES] ; :
• -H-total Invocações ; ;
Appl1cat1on[TOTAL_lNVOCACOES] = total Invocações ;
; return total Invocações; !
Neste caso, é definida uma constante chamada TOTAL_INVOCACOES que será utilizada
como chave da informação a guardar na aplicação. Quando o primeiro objecto é criado, é
colocado em Appl 1 catl on o valor O, correspondendo ao total de invocações já realizadas.
Tal é conseguido com:
rif CAppT1'caffon[tOTAL_ÍNVõCACOÉs] "== null) " " •-•--••
AppJ1cat1on[TOTAL^INVOCACOES] = 0 ; _,. ..
Um aspecto importante desta tabela é que antes de a primeira invocação ser realizada,
fazer Appl1cat1on[TOTALJCNVOCACOES] resulta numa referência n u l l , pois ainda não
existe nenhuma informação armazenada, correspondente a essa chave.
Assim como é possível guardar informação relativa à aplicação global, é também possível
guardar informação relativa a uma sessão com um cliente. Por cada sessão com cada
cliente, existe um objecto diferente onde é possível obter e colocar informação. Para isso,
utiliza-se o objecto session. Por omissão, os métodos associados a um web sei-vice não
suportam sessões. Para activá-las, é necessário colocar Enable5ess1on=true, no atributo
WebMethod. De resto, a sua utilização é similar a Application:
p u b l i c class Algoritmos : Websèrvlce
p ri vate const string TOTAL_INVOCACOES = "invocações Algoritmos";
public Algoritmos C)
i f (SessÍon[TOTAL_INVOCACOES] == null)
Session[TOTAL_INVOCACOES] = 0;
}
[WebMethod(EnableSessi on=true)]
public int ContalnvocacoesQ
int totalInvocações = (int) session[TOTAL_!NVOCACOES];
-H-total invocações;
Session[TOTAL_INVOCACOES] = totalInvocações;
return totalInvocações;
; >
Note-se que todos os métodos que necessitam de utilizar sessões devem ter
Enabl esession=true. No entanto, a sessão não é por método, é relativa ao web semice
como um todo. Enablesession=true apenas faz com que o objecto session seja
disponibilizado no objecto. Caso esta propriedade não seja colocada a true, Session será
uma referência para nul l .
rcãtfòh[TOTAUlíNVOCACOÉS] = totalInvocações;
ÃppTi cati on. UnLocKjO:;
return totalInvocações;
E, no entanto, de referir que este método garante a exclusão mútua sobre todos os
elementos presentes na tabela associativa. Caso exista um grau elevado de concorrência
no acesso à tabela, haverá uma elevada serialização da execução das threads, levando a
uma perda de performance. Nesses casos, será melhor utilizar objectos de exclusão mútua
independentes, fazendo lock e unlock sobre estes.
Até agora, temos estado a examinar de que forma é possível utilizar serviços
disponibilizados na Internet. No entanto, estes serviços e protocolos associados, como o
HTTP, partilham de uma característica comum: são implementados sobre o conjunto
protocolar TCP/IP. O TCP/IP é a base que permite a troca de informação entre quaisquer
duas máquinas na Internet. Embora haja uma tendência cada vez maior para o
programador utilizar protocolos de alto nível, e para os API disponíveis esconderem o
que é utilizado para troca de informação na rede, muitas vezes, ainda é necessário
implementar serviços puramente baseados em TCP/IP.
Iremos, agora, examinar de que forma é que se pode utilizar este protocolo na plataforma
.NET.
Sempre que duas aplicações comunicam, utilizando o protocolo TCP, é criada uma
ligação entre elas. Cada um dos pontos terminais da ligação, um em cada aplicação,
possui um número associado, chamado porto. Quando uma aplicação cliente se liga a uma
aplicação servidor, necessita de saber qual o endereço em que a máquina servidor se
encontra e em que porto é que a aplicação servidor se encontra à escuta.
Para criar uma aplicação servidor que fica à escuta de ligações, utiliza-se a classe
TcpListener. Uma aplicação que se queira ligar a um servidor utiliza a classe
Tcpcl i ent. Ambas as classes existem no espaço de nomes system. Net. sockets.
O segundo ponto importante é que em aplicações que envolvam comunicação por rede, é
habitual haver falhas. Isto quer dizer que os métodos associados a recursos de rede
lançam diversas excepções. Estas devem ser tratadas. Neste aspecto, pelo facto de haver
um problema com um cliente, uma excepção que ocorra nesse cliente não deve afectar
outros, ou o funcionamento do servidor. Há que ter cuidado com esse aspecto.
catch (Exception e)
{
console.WriteLine(e.Message) ;
Console.writeLine(e.StackTrace) ;
}
final l y
if (LigacaoServidor != null)
LigacaoServidor. stopQ ;
// Programa principal
class ExemploCaplO_4
Para testar este programa, pode-se utilizar o programa telnet. Este programa permite
ligar directamente a um porto de um servidor. Assim, basta colocar o programa a executar
numa janela e, noutra à parte, fazer:
telnet localhost 7500 " ' " ---
O resultado é apresentado na figura 10.2, após terem sido enviadas algumas frases para o
servidor.
Para criar uma aplicação cliente, utiliza-se a classe Tcpclient. Ao criar um objecto desta
classe, indica-se o nome do servidor e do porto onde se ligar, sendo estabelecida a ligação.
Após esse momento, pode-se obter a stream associada e comunicar com o servidor13:
itcpcTient ligação = new Tc~pClient("localhost", 7500);"
;NetworkStream streamLigacao = ligação.GetStreamC);
iStreamReader leitura = new streamReader(streamLigacao);
'StreamWriter escrita = new streamWriter(streamLigacao);
:escrita.Writel_ine("olá servidor!");
;escrita.Flush() ;
ARETER " O protocolo TCP é orientado à ligação e suporta entrega por ordem de
dados, assim como retransmissão automática de dados perdidos na ligação.
Utilização do ~ Para desenvolver um servidor TCP, cria-se uma instância de TCPListener,
protocolo TCP indicando o porto a escutar, e faz-se startQ para activar o servidor. O
método AcceptrcpcIientO bloqueia a thread que o chama, até que um
cliente se ligue ao servidor, retornando uma instância de Tcpclient que
representa o cliente ligado. É, então, possível obter uma stream de dados
associada a esse cliente.
" Para desenvolver um cliente TCP, cria-se uma instância de Tcpclient,
indicando-se o nome do servidor e do porto onde o cliente se irá ligar.
Após isto, é possível obter uma stream associada ao servidor, chamando
GetStreamC) no objecto criado.
" Após qualquer comunicação entre clientes e servidores, as stream e as
ligações utilizadas devem ser fechadas.
- Deve-se proteger as invocações de objectos que envolvem uso da rede com
um bloco try-catch.
Ao contrário do TCP, o protocolo UDP é utilizado para trocar mensagens curtas, não
sendo orientado à ligação. O protocolo UDP também não fornece garantia na entrega de
Para receber uma mensagem, basta criar uma instância de udpcl i ent, indicando em que
porto é que se quer escutar, e chamar o método RecelveQ. Este método necessita de um
objecto do tipo iPEndPoi nt, que é utilizado para obter o endereço da máquina que enviou
a mensagem, e retorna a mensagem recebida. A mensagem recebida é uma tabela de
bytes. O código seguinte ilustra o processo, mostrando a mensagem recebida em texto:
// cria um cliente ÚDP ^associado ao" porto "PORTO - • - - • • •
const int PORTÇ = 7500;
Udpclient servidor = new Udpclient(PORTO);
-// Recebe uma mensagem de qualquer endereço
iPEndpoint cliente = new iPEndPointCnPAddress.Any, 0);
byte[] mensagemRecebida = servidor.Receive(ref cliente);
// Descodifica e mostra a mensagem . - .-
string mensagemTexto = Encoding.ASCii.Getstring.(mensagemRecebida);
Console.WriteLine(mensagemTexto); . _ _
Note-se que, ao criar IPEndpoint, é indicado que o endereço correspondente é "qualquer
um" (iPAddress.Any), e o segundo parâmetro, que representa o porto de envio, é nulo,
que representa qualquer porto. Isto permite receber mensagens de qualquer endereço. No
final da invocação de Recei vê (), cl i ente irá conter o endereço da máquina que enviou a
mensagem. O endereço pode ser extraído, fazendo cliente.Address, que retorna um
objecto do tipo iPAddress, que já examinamos. Dado que a variável é utilizada como
parâmetro de entrada e de saída, esta é passada por referência (ref).
Para enviar uma mensagem, basta possuir uma instância de udpclient e chamar o
método send(). Este método possui como argumentos a mensagem a enviar (uma tabela
de bytes), o número de bytes a enviar, o nome da máquina destino e, finalmente, o porto
destino. Para enviar uma mensagem de texto para um servidor, basta fazer:
"privâté" const int PORTO = 7 5 0 0 ^ ....................
; private const string SERVIDOR = "localhost";
Tanto o construtor como os métodos SendO e ReceiveQ possuem diversas variantes que
podem ser utilizadas da forma mais conveniente ao programador, No entanto, a
funcionalidade base é a que foi apresentada.
As duas listagens seguintes apresentam um programa para enviar uma mensagem, usando
UDP e um programa para a receber.
class ExemploCaplO_5
public static void Main(string[] args)
try
Udpclient servidor = new Udpclient(PORTO);
Listagem 10.5 - Programa que ilustra a recepção de uma mensagem usando UDP
(ExemploCaplO_5.cs)
398 © FCA - Editora de Informática
ACESSO À l NTERN ET
/*
* Programa que ilustra o envio de uma mensagem UDP.
*/
using System;
using System.Net.Sockets;
using system.Net;
using System.Text;
class ExemploCaplO_6
{
private const int PORTO = 7500;
private const string SERVIDOR ~ "localhost";
public static void Main(string[] args)
{
try
// Cria uma nova mensagem para enviar
byte[] mensagem = Encoding.ASCII.GetBytes("Olá servidor!");
// Envia a mensagem
UdpClient cliente = new UdpcIientQ;
cli ente.Send(mensagem, mensagem.Length, SERVIDOR, PORTO);
}
catch (Exception e)
Console.WriteLine(e.Message);
Listagem 10.6 — Programa que ilustra o envio de uma mensagem usando UDP
(ExemploCaplO_6.cs)
A ideia da LINQ é resolver um dos maiores problemas das aplicações actuais orientadas aos
objectos. Por um lado, os objectos são excelentes para serem manipulados nas linguagens
de programação. Por outro lado, actualmente, o armazenamento de dados faz-se,
tipicamente, em bases-de-dados relacionais e ficheiros XML, o que vem dificultar a sua
manipulação. A LINQ consiste numa linguagem declarativa, perfeitamente integrada em
C#, que minimiza a distância que existe entre dados e objectos. Consideremos o exemplo
apresentado no capítulo 7:
Vá r ãTuhoQúery = "" ' , --^-,~ t " ' " :
from aluno In alunos ' ""
: where aluno.ld == -j d
• select new { aluno. Npme^ a] uno. Apeado, _ ai. u no._ idade J;
Este código irá produzir um resultado em alunoQuery, consistindo num conjunto de
objectos, contendo o nome, apelido e idade dos alunos que têm um certo identificador.
Caso a variável ai unos represente uma tabela numa base-de-dados, esta é automaticamente
acedida. Caso esta variável represente dados num ficheiro XML, a mesma coisa
acontecerá. Se alunos representar uma simples tabela de objectos, tal como foi
apresentado no exemplo original:
var alunos = new Ãlu"hõ[y~ ~ -—-=—
new Aluno { id=l, Nome= "Maria", Apelido= iiit Carvalho"
new AlUno à Id=2 j Nome= 'Pedro", Apel1do= nMartins", Idad&=2S J;,
new Aluno í Id=3, Nome= 'Ana" , Apelido= n Ferreira" Idade=ZÕ J,
new AlUnq -[ Id=4, Notne= 'Maria", Apelido= n Cardoso" , =25. J-,
new Á1 ó iío' { Id=5 , Nome- 'doao", Apel i do=Abreu" ,
então, apenas os dados em memória serão usados. Como se pode ver, independentemente
da fonte de dados, o sistema de execução da LINQ permite tratá-los de forma uniforme e
transparente em C#.
Tipos Anónimos;
Expressões de Consulta;
f rom Especifica uma fonte de dados e uma variável local que representa
cada elemento da colecção.
let
Introduz uma variável local para armazenar os resultados de uma
sub-consulta.
Tabela 11.1 — Palavras-chave que podem ser usadas numa expressão de consulta
Nas próximas secções, iremos ver cada uma destas expressões com mais detalhe. No
entanto, para o leitor que queira ver um número elevado de exemplos, sugerimos o
seguinte apontador ("Visual C# Developer Center-101 LINQ Samples"}:
http://msdn.microsoft.com/en-iis/vcsharp/aa336746.aspx
var alunoQuery =
| f ròm aluno i ri alunos _ - - - -- - - ..- |
where" aluno.Id == id
sel.ect new { aluno.Nome, ai uno. Apelido, aluno.idade };
ai unos encontra-se nesta condição, uma vez que representa uma tabela.
É ainda possível especificar mais do que um f rom na mesma expressão de pesquisa. Por
exemplo, admitindo que possuíamos uma outra tabela com as avaliações dos alunos:
class Avaliação
{
public int Id { get; set; } // Identificador do aluno
1 public int[] Notas { get; set; } // Notas dos testes
v a r avaliações = n e w AvaliacaofJ - . . .
' new^V/lTa-cao { Id=l, Notas=new int[] {12, 13, 14, 13} }> '-,.'., , f
• new^A^a-liatao { ld=2, Notas=new int[], {15, 14, 16, l?}. }, ', ; ' V - : . ;
neW y\'v|liacao { id=3, Notas^new int[] {10, 12, 14, lèí >, •"'-.'. '
new Avéfecao { id=4, Notas=new ,int[] {15, 18, 17, 181 - 3-, y.. ; - ; , - ,
,- neWfA^W^cao { id=5, Notas=new int[] {19, 19, 18, 17Í } í ~< :S* '.
1; - •'- ........ . . ........ . ' ;t ---'w-v
torna-se possível escrever uma expressão que retoma a informação do aluno, associada às
suas notas. O seguinte código:
var alunoNotas =
from aluno i n alunos
from notas In avaliações
where aluno.ld — notas.Id
select new_t aluno^Npme, ai uno. Apelido., aluno.Idade, notas.Notas };
foreach (var aluno i n alunoNotas)
{
Console.writei_ine("{0} {l}", ai uno. Nome, ai uno. Apelido) ;
foreach (var nota i n ai uno.Notas)
console.Write("{0} ", nota);
Console.WriteLineQ ;
J . . . . ... . ..
imprime as notas de cada aluno:
Maria Carvalho
12 13 14 13
Pedro Martins
15 14 16 15
Ana Ferreira
10 12 14 16
Maria Cardoso
15 18 17 18
João Abreu
:19 19 18 17
Na expressão de consulta:
var "alunoNbta"s "="
; from aluno i n alunos :
from notas i n avaliações
where aluno.ld == notas.Id
[ __select__new. { alunq.Npme, aluno.Apelido^ notas.Notas };
indica-se, como fonte de informação, duas colecções (alunos e avaliações). Em
seguida, garante-se que ao considerar cada elemento presente em cada uma das colecções,
os seus identificadores são idênticos (aluno.ld == notas.Id). Finalmente, emite-se um
novo tipo anónimo contendo o nome, o apelido e as notas correspondentes a cada aluno.
Um ponto importante é que dentro de uma expressão from é possível referenciar dados de
outro from. Por exemplo, a seguinte expressão:
var boasNotãs" =
from aluno i n alunos
from notas i n avaliações _ _ _
T "from nota" i n notas.Notas" ~ """ "" " ~ ' " ~
l _ where. nota > 15
• where aluno.ld == notas.id " " " " "
' select new { aluno.Nome, aluno.ApelidOj nota }; ;
permite obter uma colecção com todos os alunos que tiram notas superiores a 15 valores
em alguma avaliação. Neste caso, está a tornar-se directamente visível na variável nota, a
classificação associada a cada elemento presente em notas.Notas. Por outro lado,
notas.Notas representa cada elemento que terá de ser iterado em avaliações. Vale a
pena estudar esta expressão com alguma atenção. O resultado da execução deste código:
forêach "£?yar"boas i n "boasNõtasJ " ~""" ' "~ " „ • [ ' . . '
_ conso]e;WnteUneC"{0} _{l}\t{2}"_ í boas..Nome, .boas^Apelido, boas:."nota)
será
pedno Martins "" 16""" ~" ' > ".
Ana Ferrei rã 16
Maria Cardoso 18 :
Maria Ca.rdoso
Maria Cardoso
Do.ao'
3'oao
3oao Abreu
João Abreu
Como iremos ver adiante, existe uma outra expressão chamada group que permite
agrupar resultados, de acordo com um certo critério (por exemplo, calcular uma média de
notas, agrupando-as por idades). A expressão where pode aparecer antes ou depois de
uma cláusula group, dependendo se o objectivo é efectuar a filtragem dos elementos
antes ou depois de eles serem agrupados.
Os exemplos anteriores permitiram-nos, já, ver duas formas importantes de sei ect. Em:
var aTunosSenior = ~ " "" "' " "
from aluno i n alunos
where alunç.Idade > 23
í ~ select aluno: - - - . . . . . . . . ... - ~l
são retomados directamente todos os elementos aluno, depois de devidamente filtrados.
Não existe qualquer definição de novos tipos de dados. No entanto, em:
var" aluhoNotâs =
from aluno i n alunçs
from notas i n avaliações
where aluno.Id == notas.id
select new { aluno-Nome, aluno.Apelido, notas.Notas };
é criado um novo tipo de dados anónimo, contendo três campos. Ambas as formas de
utilização são perfeitamente lícitas. É ainda lícito emitir um tipo de dados pré-existente,
utilizando os campos disponíveis na expressão de pesquisa. Por exemplo:
var nomescompletos =
from aluno i n alunos
select ai uno.Nome + " " + ai uno.Apelido;
irá criar uma lista de alunos com o seus nomes completos.
4O6 © FCA - Editora de Informática
INTRODUÇÃO À LINQ
" Numa expressão de consulta, select permite especificar quais são os dados
A RETER (elementos) a ser produzidos como resultado da pesquisa. Por exemplo:
. . . select aluno;
LINQ - select ... select new { ai uno. Nome, ai uno. Idade };
~ Um select pode emitir, tanto os dados da consulta, após filtrados, como
outros tipos de dados. Neste último caso, especifica-se um tipo de dados
pré-existentes a partir dos campos da consulta, ou cria-se uni novo tipo de
dados anónimo, contendo os campos de interesse.
1 1 . l .4 EXPRESSÃO GROUP
A expressão group permite agrupar elementos da pesquisa, de acordo com um certo
critério. Esses elementos podem ser previamente filtrados ou não. Vejamos um exemplo
simples:
var, animosidade' =
fróm aluno tn alunos
group aluno by aluno. idade;
irá retornar uma colecção correspondente aos alunos agrupados pela idade. Cada elemento
da colecção terá uma chave (a idade), contendo uma lista de alunos correspondendo a essa
idade. O seguinte código:
foreach- (var alunosPorídade In alúnõsidade)
corisoTè.Wri.teLineC"idade:
. {0}
Consol e. wríteLlne ("===—=====—=======================") ;
fdreéCch (vâr a] uno ~in alúnospòriclade)
Console. WríteLiheC" {0} {!}", aluno. Nome, aluno. Apelido) ;
Consol e. WriteLineQ ;
.} ' _ ..... .. ................ __________
irá imprimir os alunos nesse agrupamento:
Idade: 25
Maria -Carvalho
Maria Cardoso
aoao Ab-reu
Idade': 23
• Peâr,o .Martins
Idade: 20
Ana Ferreira
Note-se que foi necessário utilizar dois foreach encadeados. O primeiro está a iterar
todas as idades encontradas. O segundo está a iterar todos os alunos correspondentes a
uma certa idade. Para aceder a cada uma das idades, utiliza-se a propriedade Key. Neste
caso: alunosPorldade.Key.
Vejamos mais um exemplo, este um pouco mais complexo. É possível especificar mais
do que uma chave para realizar o agrupamento. Para tal, é necessário utilizar um tipo
anónimo. O seguinte código:
vãr álunosNomeldáde - " " "" "" ......... ..... "" '
from aluno i n .alunos __ _ _ _ _ _ _ _ _ '
j gfpup.laTunõ. __by MW riaJujTo.í^ _II_ ITl"!_".Jl'.'_r.7 ~ ]
foreach (var alunosPorNomeldade i n alunosNomeldade)
{
Console. WriteLine("Nome: {0} " , alunosPorNomeldade. Key) ;
Console.WriteLi ne ("====================================") ;
foreach Cvar aluno i n alunosPorNomeldade)
Console. writel_ine(" {0} {!}", aluno. Nome, aluno. Apelido) ;
Console.WriteLineQ ;
'l .. ........................... .... . ... _ ........ . ..... .. ........ .
agrupa os alunos que têm simultaneamente a mesma idade e o rnesmo primeiro nome. O
resultado é:
Nome: {"idade" = 25, Nome = Maria } ""'
Maria Carvalho
Maria Cardoso
Nome: { idade = 23, Nome = Pedro }
Pedro Martins
Nome: { idade = 20, Nome = Ana }
Ana Ferrei rã
Nome: { idade = 25, Nome = João }
João Abreu
Em termos de compilação, a expressão group é convertida numa chamada ao método
GroupByQ (Enumerable.GroupByQ) do espaço de nomes system. Linq.
1 1 . 1 .5 EXPRESSÃO iisrro
Em muitos casos, ao usar-se LINQ, é necessário encadear pesquisas ou realizar
sub-pesquisas. A palavra-chave into permite definir uma variável temporária onde
resultados parciais podem ser armazenados. Tal é útil quando necessitamos, mais tarde, de
os utilizar numa outra expressão sei ect, group ou joi n, ou mesmo quando é necessário
realizar filtragens intermédias.
A título de exemplo, a seguinte expressão permite agrupar os alunos por idade, filtrando,
no entanto, todos os grupos que tenham menos de 3 pessoas:
-var a-lunosldade = '.........."
fronralurio in alunos _ -
group aluno by ai uno. Idade íntçTa] un_b_sN_QGrupp
, where --alunosNoGrupo.countQ >= 3
i select alunpsNoGruppj ________ ______ ...... _ _ ....... ,.
Obviamente, é possível combinar expressões criando consultas mais complexas. No próximo
exemplo, os alunos são agrupados por idade, considerando-se, apenas, os grupos que têm
uma representatividade superior ou igual a 10%. Após terem sido criados os grupos,
apenas se obtém o resumo de cada grupo, criando um tipo abstracto contendo a idade
(chave do grupo), total de pessoas com essa idade e a percentagem correspondente.
A/ar é^taiti^ticaldades"^' ~ "" ..... ' "" """ ....... " .- *
n alunos ..' X , ' /'
by ai uno. Idade into grupo - , .
!countO >= 0.10*alunos.CountO , ' . •
seTecfe-iTew
Idade = _grypp.-!<£y,
© FCA - Editora de Informática 4O9
C#3.5
surge:
:ídade -r %
•25 3 60%
,23 1 20%
2Q 1 20%
A utilização de into em conjunto com group é apenas necessária, quando se quer
executar operações de consulta adicionais sobre cada grupo.
obtém-se uma colecção contendo, para cada aluno, as suas notas correspondentes. Isto,
apesar dessa informação estar armazenada em duas tabelas diferentes (alunos e
aval i acoes). A este tipo de operação chama-se vima. junção.
Estritamente falando, a expressão de consulta acima não deve, regra geral, ser escrita
desta forma. Deve ser escrita usando uma expressão joi n:
var . . .
from aluno i n_ alunos ___ _ _ ____ ' **'*'"'_'_"
[ " jòin nota 'J n" avaTiacQ|s _pjn aljJQpI^Id ecfuàTs~ nota', í ã \. ,........" _ _
selecÇ new/{ aluno, Nome, aluno. Apelido, alunò.ldade, nota. Notas };
Man'à "Cardoso" , ~ - .-
15 18 17 18 ,,,-•-
•aoap Abreu '' v^*"
:i9 19.18..17 „. • . _ - i . . . - „_ . '
Embora o resultado final seja idêntico ao da primeira expressão, esta última é tipicamente
muito mais eficiente. O compilador sabe explicitamente que se está a cruzar dados de
duas fontes diferentes. Note-se que o formato de uma expressão j o i n é:
A fim de ilustrar os dois tipos de junção que ainda não vimos (junção de grupo e junção
externa esquerda), consideremos as seguintes colecções:
vãr alunos = new~ATúrió[] urión
new'. Al uno Notne= 'Maria", Apel i do="carval no" , Idade=25, Ic
ldDisc=l},
new Aluno Nome= 'Pedro", Apelido="Martins", 3:dàde=23, ic
new Alunô.{ld=3, Nome= 'Ana" , Apelido="Ferrei rã" , ldade=20J Ic
new Aluno, Nome= "Maria", Apelido="Cardoso", id,ade=25J ÍdDisc=3},
Ic
new Ali u no Nome= 'João", A P e Ifl^jP^^htl^-U^' ' , Idã.de=25} Xe
XdDisc=3},
new Aluno Nome= 'Rita", Apellíffi^^^pQZ11 , ldade=35, Ic
ldDisc=9}
Suponhamos que queremos obter a lista de alunos inscritos a cada uma das disciplinas.
Para isso, usamos uma junção de grupo:
^vãr_alundsPprDlscjpJTna_=_ _"__ _'1~_'~_" _""~ l "~"I _ " ! " " _ _ i.
frõm"cíTsc'Tn "disciplinas
join aluno in alunos on disc.Id equals aluno.Idoisc into alunoNaDisc
sei ect. new í pisclpJJaaf=di>SjC.J^pmeJ_Al_unos=a_]unpNaplsç_iJ. - _
JVaV.disc i n alunosporoisciplina) . , . •
L •-
, dl se. Di sei pi i na) ; '-
"(var. aluno in disc. Alunos) "t
{0} {!}", aluno. Nome, aluno.ApelidoO';
No exemplo acima, existe ainda um aspecto curioso: a aluna Rita Queiroz, que se encontra
inscrita numa disciplina desconhecida, não surge nos resultados. Consideremos, então, uma
expressão de consulta que tenta listar todos os alunos e as disciplinas correspondentes:
iyar álunopiscfplina = _ " __ "_ ' _ "__ l
" "frõm"aluno i"ri""alunòs" " ~ " "
join disc i n disciplinas on aluno.ldoisc equals disc.ld
select new {_ aluno. Nome, ai uno. Apelido, _Disciplina=disc.Npme };
:foreach (var aluno i n alunooisciplina)
;{Console.WriteLine("{0} {1} \ {2}",
ai uno.Nome, ai uno.Apeli do, ai uno.Di sei pi i na);
J ...... _ . . . . .
O resultado é:
'Maria "Carvalho "Matemática"
Pedro Martins Física
•Ana Ferreira Fisica
Maria Cardoso Quimica
3oao Abreu Quimica
Ou seja, Rita Queiroz não aparece. Isso deve-se ao facto de estarmos a usar uma
equijunçao. Para se conseguir que a Rita surja na listagem, tem de se utilizar uma junção
externa esquerda. Este tipo de junções garante que mesmo que não exista um elemento na
segunda fonte de dados (fonte direita), todos os elementos da primeira fonte de dados
(fonte esquerda) são mostrados.
Pode-se utilizar os tradicionais n u l l e O s quando ta] faça sentido, ou outro valor que se
queira. Regressando ao exemplo anterior, a junção externa esquerda poderia ser escrita da
seguinte forma:
Vá r aluhoDisciplina =
from aluno In alunos
join disc In disciplinas on aluno^ldoisc equals disc.Id into alunopisc
from ficha irí alunóDisc.DèfàultífEmptyC
new Discip]nna { id=-lA Nome = "—piscioVina Desconhecida--" })
select new {"aiuno.Nome, aluno.Apelido, plsciplinã-ficha.Nome };
Examinemos esta expressão com cuidado. Após ter-se feito a junção de grupo, cujo
resultado é colocado em ai unoDi se, é feita uma nova consulta que a utiliza como fonte.
Nessa consulta, ao fazer-se alunoDisc.oefaultlfEmptyC.. O sobre a fonte de dados,
todas as entradas vazias associadas irão ser substituídas pelo objecto que é passado como
parâmetro. Neste caso, uma disciplina com identificador -l e um nome desconhecido. Ou
seja, no fundo, funciona da mesma forma que uma equijunção mas em que especificamos
um valor a usar, caso não seja encontrado nenhum do lado direito. O resultado da
impressão de alunooisciplina será:
Maria Carvalho Matemática
Pedro Martins F1s1ca
'Ana Ferreira Fisica
Maria Cardoso Química
João Abreu Quimica
Rita Queiroz --Disciplina Desconhecida--
Como se pode ver, Rita Queiroz já se encontra na listagem.
Para terminar, existem ainda dois pontos importantes a considerar. Em primeiro lugar, ao
realizar-se junções, as chaves podem ser compostas. Isto é, a chave pode consistir num
conjunto de atributos que, como um todo, identifica univocamente um elemento. Por
exemplo, admitindo que todas as pessoas são univocamente identificadas por um par
[nome, apelido], sendo esses elementos parte de alunos e de avaliações, uma
equijunção simples poderia ser expressa por:
vár alunoNotas =
from aluno i n alunos
join nota i n avaliações
on riew { aluno.Nome, aluno.ApèTidò } |
equals new equals { nota.Nqmej nota.Apelido } . ._ _ |
select new { alúno._Nome.,_ ai uno ..Apelido,_ ai uno. Idade, nota. Notas }; . ;
Quando se usa chaves compostas, têm de ser definidos tipos anónimos que caracterizam
essas chaves. As propriedades associadas são comparadas uma-a»uma, pela ordem em que
estão definidas. Necessitam também de ter o mesmo nome, caso este exista.
O segundo ponto importante é que é possível fazer junções sobre um número arbitrário de
fontes de dados. Tudo o que é necessário, é colocar diversas expressões join. E também
possível realizar operações que não são equijunções. Por exemplo, se fosse necessário
classificar qualitativamente os alunos em termos das suas notas, havendo intervalos para
© FCA - Editora de Informática
C#3.5
"Muito Bom", "Bom", "Suficiente" e "Fraco", numa tabela separada, isso não seria
possível, usando uma equíjunção. Quando tal acontece, é necessário escrever a expressão
de consulta em termos de f rom, operações where e groupby, tal como foi discutido nos
primeiros exemplos. A expressão j cri n apenas existe de forma ao compilador poder
optimizar as equijunções e junções de grupo, que são os casos mais frequentes.
Em termos de compilação, a cláusula jcrin, quando não é seguida pela cláusula into, é
convertida numa chamada ao método Join Q ( E n u m e r a b l e . u o i n O ) do espaço de nomes
System.Linq. Quando é seguida pela cláusula Into, é convertida numa chamada ao
método G r o u p u o i n Q ( E n u m e r a b l e . G r o u p J o i n O ) do espaço de nomes System. Li nq.
Consideremos uma expressão de consulta que imprime a média final das notas de cada
aluno:
ívar notasFinais =
416 © FCA - Editora de Informática
INTRODUÇÃO À LINQ
Imaginemos, agora, que queremos ver, não apenas a média final, mas também as notas
classificadas de forma qualitativa. Existe uma tabela que, para cada intervalo de médias,
diz se o aluno é insuficiente, suficiente, bom, muito bom ou excelente:
var escalaQualitativa = new intervalo[J
new Intervalo { classe= 'Excelente", Inf=18, Sup=20 },
new Intervalo { classe= 'Muito Bom", Inf=17, Sup=17 },
new Intervalo { classe= "Bom", Inf=14, Sup=15 },
new Intervalo { classe= 'suficiente", Inf=10, Sup=13 },
new Intervalo { classe= 'insuficiente", Inf=0, Sup=9 }
Nesta definição, intervalo é uma classe trivial, possuindo apenas três campos: a classe a
que o aluno pertence, o limite inferior da classificação associada e o limite superior da
classificação da classe.
Embora seja possível alterar a expressão de consulta anterior, para formatar os alunos, de
acordo corn a tabela qualitativa, tal não é trivial. No entanto, usando a expressão l et, tal é
relativamente simples:
var notasFlnals =
from notas i n avaliações
l et media = Math.Round(notas .Notas .AverageQ)
from intervalo in escalaQualitativa
where (media >= intervalo.lnf) && (media <= intervalç.sup)
let registo = new {l^notas^Id,, Me_di>=mediaJ çlasse==intervalg.Classe}.
© FCA - Editora de Informática 41 "7
C#3.5
surge:
Nome Apelido Medi a Classe
Maria Carvalho 13 suficiente
Pedro Marti n s 15 Bom
:Ana Ferreira 13 suficiente
Maria Cardoso 17 Muito Bom
João Abreu 18 Excelente
Embora a clausula l et seja bastante útil e, muitas vezes, permita escrever mais facilmente
expressões de consulta, esta deve ser usada com alguma contenção. Em muitos casos, este
tipo de expressões implica uma determinada ordem de execução e algumas restrições nas
operações que estão a ser feitas. Assim, torna-se algo mais difícil ao compilador,
optimizar as expressões para que executem o mais eficientemente possível. Tal poderá
não ser muito importante quando se trabalha com colecções em memória mas, quando se
usa bases-de-dados, a penalidade pode ser grande. As bases de dados possuem
optirnizadores de consultas que, ao utilizar-se variáveis l et, podem ter o seu papel
bastante dificultado.
- Numa expressão de consulta, l et permite criar uma variável local que pode
ARHTER ser usada como fonte de consultas subsequentes. Por exemplo:
let media = alunos.Notas.AverageQ
L NQ - let _ 1jma Yez defino Q valor de uma variável, o mesmo não pode ser alterado.
Imaginemos, agora, que queremos obter a média de idades de todos os alunos presentes
nesta colecção. Uma abordagem possível seria iterar ao longo de todos os elementos da
colecção, acumulando as idades num contador, dividindo o total pelo número de alunos.
No entanto, em LINQ, é possível escrever simplesmente:
double medialdades =
Cfrom aluno i n .alunos select aluno, idade) .AverageQ ; . :
O método de extensão AverageQ permite calcular directamente uma média, a partir do
resultado de uma expressão de consulta (ou dos elementos de uma colecção).
Obviamente, estes métodos de extensão podem ser também usados dentro de expressões
de consulta. Ai reside o seu poder. Por exemplo, suponhamos que dada a tabela de alunos,
pretendemos saber quantos alunos existem por idade. Para tal, basta escrever:
var alunosPoridade =
from aluno i n alunos
group aluno by aluno. Idade into gruposldade
s_elect new { JEdade-grupps.Id_ade..Key^ jotal=gruppsidad_e..ÇpuntD
\ Alunos").;
consoTèvWríteLi nè ("========—=====") ;
r- grupo In alunosporldade)
- , . .
console. WríteLine("{0} \ {!}", grupo. idade, grupo. Total) ;
} .......... . _ _ . . . .
© FCA - Editora de Informática 41 9
C#3.5
Isto é, dado o conjunto de alunos, estamos a agrupá-los por idade. Após termos obtido os
grupos, estamos a criar um resultado contendo os pares [idade, número de pessoas com.
essa idade}. O resultado da execução será:
[idade " "Alunos
25 3
123 l
: 20 l
;35 . . . l .„
Dando um exemplo de uma consulta ligeiramente mais complexa, o código:
var resumoNÕtas =
from aluno In alunos
join nota i n avaliações on aluno.Id equals nota.ld
orderby aluno.Nome
select new
Nome=aluno.Nome, Apeli do=aluno.Apel1 do,
Max=nota.Notas.Max(), Min=nota.Notas.Min(),
Medi a=nota.Notas.Ave rage C)
ji
Console.WriteLine("Nome \ Apelido \ Máximo \ Mínimo \ Média");
Console.WriteLine("===========================-============—========='');
foreach (var aluno i n resumoNotas)
Console.wntel_ine("{0} \ {1} \ {2} \ {3} \ {4:F1}"(
aluno.Nome, aluno.Apelido, aluno.Max, aluno.Min, ;
ai uno.Medi a); :
l .. . .
permite obter uma listagem de alunos. Esta listagem mostra, para cada um deles, a nota
máxima, mínima e média. A listagem é ordenada pelo primeiro nome dos alunos:
Nome Apelido" Máximo " Mínimo Média
Ana Ferreira 16 10 13.0
goao Abreu 19 17 18.3
Maria Carvalho 14 12 13.0
Maria Cardoso 18 15 17.0
!pedro Marti ns 16 14 15.0
Uma outra operação bastante útil é a countQ. Sem parâmetros, permite contar o número
de elementos de uma colecção. No entanto, se levar um parâmetro, este permite
especificar uma condição para os elementos serem incluídos na contagem. Por exemplo:
Tnt boásMedfas = "
(from notasAluno in avaliações select notasAluno.Notas.AverageQ)
.CountCmedia =>. media > 17.0 ? true : false);
encontra o número de alunos que tiveram média superior a 17 valores. E de notar o uso de
uma expressão lambda, o que é bastante comum em chamadas whereQ e countO- Como
se pode ver, com uma sintaxe extremamente concisa, consegue-se exprimir operações
algo complexas.
Uma outra operação comum consiste em obter os elementos distintos de uma colecção.
Para tal, utiliza-se o método Di s ti nct Q. Por exemplo:
var apeTidos = (from .aTuno in alunos~seTect aiuncKApelido) .DistíriçtO;
pemiite obter uma lista dos apelidos únicos dos alunos.
A tabela seguinte mostra as principais operações existentes, mas agrupadas por categoria.
Novamente, recomenda-se uma exploração aprofundada das mesmas.
© FCA - Editora de Informática 421
C#3.5
CATEGORIA OPERAÇÕES
Filtragem OfType, Where
Projecção Select, SelectMany
Partição Skip, Skipwhile, Take, Takewhile
Junção GroupJoin, 3oin
Concatenaçao Concat
Ordenamento OrderBy, orderByoescending, Reverse, Thensy,
ThenByoescendi ng :
LINQ para LINQ para LINQ para LINQ para LINQ para
Objectos DataSets SQL Entidades XML
LINQ para SQL. Permite escrever expressões de consulta que têm subjacentes
dados presentes numa base de dados relacional. Actualmente, apenas é suportado
Microsoft SQL Server. No entanto, outras empresas encontram-se a escrever
adaptadores para as suas bases de dados;
LINQ para XML. Permite escrever expressões de pesquisa que têm subjacentes
dados que se encontram armazenados em ficheiros XML.
Quando se usa LINQ para SQL, é necessário mapear as tabelas e relações existentes na
base de dados para classes, na linguagem de programação. Isso pode ser feito de quatro
formas diferentes:
Nesta secção, iremos ilustrar estas quatro formas de interagir com a base de dados,
realizando uma consulta muito simples. Dados todos os clientes presentes na base de
dados NorthWind, encontrar aqueles cujo nome começa pela letra "A":
NòrthWrid nw = . . . // A classe NoTthWnd representa á"basè-dé-dãdos
var"'custò'râerfíaniês"= ~ "~" ~" " ~
from customer in nw.customers
where customer.CompanyName.StartsWlth("A")
seleçt eustomer,CqmpanyName;
foreach (var customer In customerNames)
Çonsole..Wr1teLineC"{0}_") _custqmer) ;
O resultado será:
Ana TrujilIo Emparedados y héladós
António Moreno Taquería
; Around the Horn
Northwnd é uma classe que representa a base de dados, enquanto fonte de dados. Esta
poderá ser gerada manualmente ou automaticamente, das quatro formas diferentes
mencionadas. Ou seja, após sabermos escrever expressões de consulta, tudo o que é
necessário é conseguir ter uma ou mais classes que representem as fontes de dados. Nas
secções seguintes, discutiremos como fazer o mapeamento da base de dados, criando
portanto, a classe Northwnd. A expressão de consulta será sempre a mesma.
No nosso exemplo, para gerar a classe Northwnd, utilizando o ficheiro da base de dados
directo ("NorthWnd.mdf}, tudo o que é necessário fazer é executar o comando:
^^ _* ...... " ~ " ~ _ ."..._. ...... . "... . .
Isto criará um ficheiro "nwind.cs", que contém todo o código de abstracção necessário para
usar a base de dados, de forma integrada com LINQ. Assim, o código necessário para
realizar a nossa consulta será apenas:
_ . _ ...... ~~\r custoft
1 1.2.1.2 VlSUALSTUDIO
Sobre o Project Explorei-, clique com o botão do lado direito do rato, adicionando
um novo item (Add-^New Item);
var Gustomertfames =
from customer In nw.customers ' ; ' '
where.cus;tomer. CompanyName. StartsWl th (''A1') : > r ..
select' customer.CompanyName; - ,: t, .:
foreach.Çvar customer i n customerNames) • •-=. ;.'.
Console. WriteLineCíQ}",....customer);
É de notar que NorthwndDataContext não leva nenhum parâmetro de entrada. Neste
caso, a string de ligação é guardada no ficheiro "app.config", também gerado
automaticamente. O ficheiro pode ser alterado sempre que necessário.
Uma outra forma possível de interagir com a base de dados é criando, manualmente, classes
que representam as entidades (tabelas) presentes, assim como as suas relações. Em seguida,
pode usar-se a classe oatacontext, do espaço de nomes system.Data. Linq, para que seja
feito o carregamento e mapeamento dos objectos no programa.
: [Column]
: public int Phone { get; set; }
0 atributo Tabl e permite especificar, a nível da base de dados à qual a tabela a classe se
refere. O atributo Column permite especificar que certas propriedades representam
colunas na tabela associada. É de notar que no atributo Tabl e usou-se
Name="customers". Isto acontece porque o nome da classe é diferente do nome da tabela.
Sempre que tal sucede, é possível especificar explicitamente o mapeamento a efectuar.
Caso contrário, o sistema procura automaticamente tabelas e colunas cora os nomes
presentes no código.
Tal como foi mencionado anteriormente, é necessário usar Datacontext para ir buscar os
dados à base de dados e gerir as ligações. Existem diversas formas de o fazer, com
diferentes implicações em termos de código. Por uma questão de coerência com os
exemplos que temos vindo a apresentar, iremos encapsular Datacontext em termos da
classe Northwnd, que teremos de criar. Note-se que esta é uma solução adequada neste
caso simples, podendo haver outras mais adequadas em casos mais complexos (múltiplas
tabelas, classes, etc.).
~clãss Nõrthwrid : Datacontext
1 public NorthWnd(string connectionString) :
base(connectionString)
A classe Northwnd deriva de Datacontext, para que seja fácil carregar dados. Possui
apenas um construtor, que corresponde à string de ligação necessária para aceder à base
de dados. Obviamente, esse trabalho será delegado na classe-base: Datacontext. Possui,
ainda, uma propriedade pública que permite obter os elementos da tabela customers.
Assim, retorna Table<customer>, delegando esse trabalho no método GetTableQ. Este
método é herdado de Datacontext, permitindo, genericamente, obter qualquer tabela
presente na base de dados. A tabela a obter é especificada pela parametrização
428 © FCA - Editora de Informática
INTRODUÇÃO À LINQ
surgira:
;AnaTruj~illoEmparedados y hélãdõs "
António Moreno Taqueria
Arpund the Horn _. ......
A listagem seguinte apresenta o código completo deste exemplo.
/*
* programa que ilustra o mapeamento directo de campos
* numa base-de-dados.
*/
using System;
usi ng System.collecti ons.Generi c;
using System.Linq;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using system.Text;
[Table(Name = "customers")]
class customer
{
[Column]
public string CustomerID { get; set; }
[column]
public string CompanyName { get; set; }
[Column]
public int Phone { get; set; }
Tabela 11.4 — Principais atributos usados para mapear bases de dados em classes
Note-se que não é indicado qualquer tabela correspondente a esta classe ou as colunas
que serão usadas. Para ilustrar que não é necessário ter o mesmo nome nas colunas da
tabela da base de dados e na classe correspondente, modificámos o nome das
propriedades de customer. A forma como é feito o mapeamento da base de dados para
esta classe é especificada num ficheiro XML:
(<7xmT v e r s ~ i õ n = ' ' i r O ' i " " " ~ ~
<Database Name="NorthWnd"
xmlns="http://schemas.mi crosoft.com/li nqtosql /mappi ng/2007">
<Table Name="customers" Member="customers">
<Type Name="customer">
<Column Name="CustomerID" Member="ID" />
<Column Name="CompanyName" Member="Name" />
<Column Name="Phone" Member="Phone" /> ;
</Type> :
</Table> !
A título de exemplo, o código seguinte encontra a empresa cujo nome é "Around the
Horn" actualizando-o para "Around the Horn -~ The Chain Store":
•NorthwndbBDãtaContext "nw~= new NorthWndòBDãtaCbntéxtOj~ "~ " " " :
customer myCompany = !
(from customer in nw.Customers !
i where customer.CompanyName == "Around the Horn"
: select customer).singleQ; j
i
'mycompany.CompanyName = "Around the Horn — The Chain Store"; ;
'nw._Subnritchanges.Oj,. .... _ ;
A única parte eventualmente menos evidente deste exemplo diz respeito ao uso do
método de extensão s i n g l e Q . Este obtém a instância seleccionada, fazendo uma
conversão explícita para o tipo de dados subjacente (neste caso, Customer). Isto
permite-nos actualizar o nome da empresa directamente, o que não aconteceria se
myCompany tivesse sido declarada, usando a palavra-chave var. Se tal acontecesse, a
variável poderia representar uma colecção, que teria de ser iterada manualmente. Tal é
ilustrado no seguinte exemplo, que coloca em maiúsculas, o nome de todas as empresas
presentes na tabela Custotners:
TíorthWndDBDataContèxt nw =™new" NbrthWndDBDãtaCõntextO ; " :
i j . . ' ' . • ' . • • ' . . . . ; .
çonarnes = ., ^ - . '
i n n w . Customers . • ' . - . ' . '
cústomer; ,'"'- ;
,' i , - ' ' • ' • • . : - *':-1; •
foreadi (g/á r' company In compames}
còjnp^nSc.companyName = company. companyName. ToUppérO ; :
Embora não o discutamos aqui, é ainda possível especificar o que acontece quando não é
possível realizar as actualizações, especificar actualizações que abranjam várias tabelas,
assim como a utilização explícita de transacções.
Todos os elementos (tags) XML são representados pela classe XElement. Esta classe
possui, ainda, atributos que permitem carregar e gravar ficheiros XML. Por exemplo,
escrevendo:
XElement catálogo = XETeme.ht.LoadC"cataio~go_cds.xnil") ;'
Cgnsole.WnteLineCcatalggp);
o ficheiro "catalogo_cds.xmr é carregado em memória e impresso. O resultado impresso
no ecrã é exactamente o que é mostrado na listagem 11.2, com a excepção da primeira
linha3. O primeiro elemento do ficheiro, chamado elemento raiz, corresponde à tag
catalogo.
Isto porque cada elemento titulo é constituído, não só pelas tags correspondentes, mas
também pelo texto e, eventualmente, atributos e nodos descendentes. Assim, para obter
apenas o texto, é necessário usar o atributo vai ue do elemento:
+ O/ar- titúl o in';"ti'túl.ós')'
e WnireLii
3 Estritamente falando, a primeira linha não é um elemento mas sim urna directiva XML, correspondendo ao
prólogo do ficheiro.
© FCA - Editora de Informática 435
C#3.5
O resultado será:
Scfeairn ríg Fl èlàs of Som"c Lòve
Uh Huh Her
•The Mj. r ror consplr.acy
Na verdade, uma forma mais eficaz de escrever este exemplo teria sido:
i vá r títulos '= ""
, from titulo In catalogo.DescendantsC"t1tulo")
'• select titulo.value;
Embora titulo não seja filho directo de catalogo, é um descendente deste. O método
oescendentsQ permite seleccionar um conjunto de elementos com um certo nome em
qualquer parte de um ficheiro XML. Assim, é possível pesquisar directamente qualquer
conjunto de nodos presente num ficheiro deste tipo, sem grande esforço.
Este pormenor das conversões explícitas é bastante importante porque, em muitos casos,
sem conversões explícitas, não é possível escrever as expressões de consulta. Por
exemplo, imaginemos que os elementos cd possuíam ainda dois campos: nvendldos,
representado o número de CDs já vendidos, e preço, representando o preço unitário de
cada CD. Para ser possível encontrar os CDs cujo volume de vendas excedesse, por
exemplo, 1.000.000€, as conversões explícitas seriam essenciais:
ivãr grãndesvéndas = "' " "" " (
! from cd in catalogo.DescendantsÇ"cd")__ _ . _ _ :
where " "" " ~ " -" ' ~ ~ "~ "1
(1nt)c_d^lemejitr;nYe^
"selèct cd; '_ _ _.. _. . _. i
O último aspecto que iremos ver é a facilidade com que é possível criar árvores e
ficheiros XML, usando a classe XE! ement e LINQ.
Para criar uma pequena árvore XML em memória contendo um conjunto de CDs, basta
usar as classes XEl ement e XAtt ri bute. Por exemplo:
XEl ement catai ògcf= ~ ;
new XEl ementC catalogo", ;
; new XElementC"cd" , j
i new XAttribute("id", "0001"), !
new XElement("titulo", "screaming Fields of Sonic Love") ,
new XElementC"artista", "Sonic Youth") , !
new XElementCano", "1995")), ;
; new XElementC"cd" , !
new XAttributeC"id", "0002"),
new XElement("titulo", "Uh Huh Her") , ;
new XElementC"artista", "P3 Harvey") ,
i new XElementC"ano", "2004")), :
new XEl ementC cd", i
new XAttnbuteC"id", "0003"), ;
new XElementC"titulo n , "The Mirror conspi racy") , ;
new XElementC"artista", "Thievery Corporation") ,
new XElementC n ano n , "2000")));
•catai ogp . Save("cat_alogp_cds2 . _xml") ;._ _ ......... ... ..... _ ... ... ...... .i
cria em memória o ficheiro XML que temos estado a usar e guarda-o para disco. Tudo
isto de forma extremamente simples.
Para perceber todo o potencial desta funcionalidade, suponhamos que temos uma
colecção de objectos do tipo CD. CD é uma classe definida da seguinte forma:
icflass' C D -.. — . . ;
:çatalogp._SayeCIlçatalo30^cds3..xml.").i.. . . . . ;
Este código merece ser examinado com cuidado. Na mesrna expressão de pesquisa,
estamos a combinar, directamente, elementos criados no momento (por exemplo, o
elemento catai ogo) com objectos gerados a partir de uma expressão de consulta de uma
lista de objectos (List<CD> catai ogocos), gerando e gravando um ficheiro XML como
resultado. Este nível de versatilidade e flexibilidade na manipulação de fontes de dados}
quer sejam objectos, bases de dados ou XML é um dos fortes da LINQ.
/*
* Programa que ilustra a geração e manipulação directa de XML.
*/
using System;
using System.Linq;
using System.Xml.Ling;
usi ng System.Collecti ons.Generi c;
class CD
public string id { get; set; }
public string Titulo { get; set; }
public string Artista { get; set; }
public int Ano { get; set; }
class Exemplocapll_2
static void Main(string[] args)
// Cria uma lista de cos correspondendo ao catálogo
List<CD> cataiogoCDs = new List<CD>();
// Adiciona alguns CDS à lista de objectos
cataiogoCDs.AddCnew CDQ {
Id = "0001", Ano ^ 1995,
Titulo = "Screaming Fields of Sonic Love",
Artista = "Sonic Youth" });
cataiogoCDs.AddCnew CD() {
Id = "0002", Ano - 2004,
Titulo = "uh Huh Her",
Artista = "PJ Harvey" });
catalogoCDs.AddCnew CD() {
Id = "0003", Ano ^ 2000,
Nos últimos onze capítulos, foi coberto imenso terreno. Em particular, na primeira parte
do livro, a linguagem C# foi amplamente explorada. Na segunda parte, foi dada uma
visão geral dos APIs fundamentais da plataforma .NET.
Para terminar, iremos situar o leitor nos API que foram explorados e no que mais ainda
existe para explorar. Embora o objectivo deste livro não seja centrar-se na plataforma
.NET, mas sim na linguagem C#, achamos que é importante o leitor ficar com uma ideia
clara do que existe disponível. A figura seguinte apresenta uma visão de alto nível dos
API existentes. Esta figura é semelhante à apresentada no primeiro capítulo, mas
focando-se nas interfaces de programação.
Acesso à rede
(ASP.NET, Web
Services, Web Forms)
12.1. l COMMONLANGUAGERaN77ME(CLPb
Relativamente ao Common Langiiage Runtime, ainda existe muito para explorar. Entre
outras coisas:
12.1.2 BAS£CLASSlJBRARr(BCL)
Neste livro, foi coberta uma boa parte da Base Class Libraiy. No entanto, o leitor poderá
ainda encontrar muitas classes úteis. Classes que permitem, desde gerar números
aleatórios, a processar texto, até manipular horas e datas. Existe, ainda, um outro conjunto
de classes, conjuntamente chamadas de Globalization, que permitem criar programas
adaptados a executarem em diferentes partes do mundo.
12.1.4 ACESSOÀREDE
Neste livro, foi dada urna panorâmica geral da forma como se pode aceder a serviços na
Internet. No entanto, existe ainda muito para explorar. Em particular, os web sei-vices
merecem um tratamento mais profundo do que foi dado neste livro. Para além disso,
existe ainda a questão da criação de páginas web dinâmicas, utilizando o API ASP.NET,
assim como a manipulação dessas páginas como formulários, os chamados Web F o rins.
Na plataforma .NET, existem dois API que permitem ao programa, criar programas
gráficos: Windows Forms e GDI+. O Windows Fonns permite criar programas baseados
em janelas, diálogos e controlos. O GDI+ permite desenhar em janelas e manipular
objectos gráficos (por exemplo, imagens). Na prática, trabalhar com janelas é
extremamente fácil e, caso o programador utilize o Visua.lStudio.NET, o trabalho fica
ainda mais facilitado, uma vez que, nesse caso, tudo é feito de forma visual.
12.2 CONCLUSÃO
Para terminar, e num acto de revolta contra livros que "simplesmente terminam" num
capítulo como qualquer outro, gostaríamos de agradecer ao leitor, por nos ter
acompanhado ao longo desta viagem. Esperamos que tenha sido interessante.
Regra geral, deve-se utilizar a notação da linguagem Pascal ao dar nome a tipos de dados,
métodos e espaços de nomes. A primeira letra de uma palavra deve ser em maiúscula,
sendo as restantes em minúsculas. Se a variável, ou o tipo de dados, é composta por
várias palavras, cada uma delas deve começar por maiúscula. As palavras não devem ser
separadas por sublinhado (_). Por exemplo, os seguintes nomes de classes e variáveis são
válidos: Empregado, NomeDaPessoa, VelocidadeMaxima.
No caso das interfaces, aplica-se a notação Pascal, só que o nome da interface deverá
começar sempre pela letra /maiúscula. Por exemplo: iLe-itorCD e icollectlon.
1 Esta foi a única convenção que violámos ao longo do livro. Ao longo do livro, utilizámos nomes em
maiúsculas separados por sublinhado, para constantes. Por exemplo, SALARIO_MAXIMO e
RESOLUCAO_MAXIMA. Dado que as enumerações funcionam como constantes, este formato foi
também aplicado nesse caso. É opinião dos autores, que esta forma de codificar permite manter uma
melhor perspectiva sobre elementos modificáveis e não modificáveis e quando são feitas atribuições de
variáveis a valores por omissão. No entanto, fica o alerta para o facto de esta nomenclatura não ser seguida
na plataforma .NET. No entanto, convenções de codificação não são especificações de linguagens. O
programador pode (e deve) utilizar o seu discernimento para decidir quando deve ou não aplicá-las.
© FCA - Editora de Informática 445
C#3.5
A variante da notação em que a primeira letra da primeira palavra começada por minúscula
e as seguintes obedecem à notação Pascal chama-se notação Camel, Por exemplo: nome,
nomeDaPessoa, veloddadeMaxIma. Esta notação deve ser utilizada em nomes de
parâmetros de métodos. Por exemplo:
jpQbTic'cTãss""Pessoa ~" 77 Nome "de ^cna"sseT""ridtãçãb"~Pãscal "~~
j prlvate strlng Nome; // Variável de Instância: notação Camel
publlc Pessoa(stn'ng nome) // Parâmetro de entrada: notação Camel
Nome = nome;
Outra situação em que se pode utilizar notação Camel é ao dar nome a variáveis
automáticas. No entanto, também é admissível utilizar a notação Pascal para esses casos.
Neste livro, optamos pelo primeiro caso.
Base - 50, 92
Base Class Library • Ver BCL
BCL • 3,442
/checked • 138 BinaryFormaíter • 318, 319
/doe • 245 BinaryReader • 308, 309, 315
/domain-384 BinaryWriter-308,309,315
/out • 66 BítArray-278,282
/password • 384 Bool-16,60
/r-66 Boolean • 60
/t-66 Boxing/unboxing • 60
Amsafe - 221 Break-26,31
/username • 384 BufferedStream-309
Button • 142
Byte • 12, 60
A
Abort • 335
Abstract'98,-100
AcceptTcpClient • 392
ADO.NET-3,442 Caracteristicas • l
AllowMul tiple-172
CH- • l, 2 ,5, 27, 39, 41, 45, 75, 87, 105,111, foreach • 187
131, 135, 213, 221, 224, 225, 233, 240, hashtable • ver hashtable
241, 249, 276, 446 icollection • 276, 277
Cadeias de caracteres • 9, 16, 17, Ver também ienumerable- 187, 189
stríng iteradores • 187
formatação • 266 list • ver Jist
formatações definidas pelo programador - 268 queue • ver queue
operações • 18, 21 SortedDictionary • ver SortedDictionary
String • ver string SortedList • ver SortedList
StringBuílder • Ver StringBuilder stack • ver Stack
Capturas • ver expressões regulares yield • 196
Caracteres • 16 Collections • 190
sequência de escape • 17 Comentários • 10
Case • 26 documentação XML • 244
Cast • Ver Conversões explícitas fim de linha- 11
Catch • 123 múltiplas linhas • 10
Char-16,60 Common Language Runtime • Ver CLR
Checked • 137 Common Language Specifícaíion • Ver CLS
Class • 47 Common Type System • Ver CTS
base • 50 CompareTo • 292
Classes • 47 Componentes
abstractas • 97 atributos • ver Atributos
construtores • ver construtores definição • 141
conversões • ver conversões entre tipos eventos • ver Eventos
destrutores • ver destrutores métodos • 141
inicialização automática de campos • 74 Propriedades • ver propriedades
inicialização directa de campos • 81 Composição • 48
membros estáticos • 68 Console • 9
membros estáticos readonly • 80 leitura • 260
modificadores de acesso • 64 Const • 19, 67
níveis de acesso • 63 Constantes • 19, 67, 68
seladas • 96 Construtores - 47, 72
utilização genérica de mis • 76 estáticos • 78
Click • 143 por omissão • 73
Clone • 258 utilização de vários • 75
Glose - 235 Context • 387
CLR • 2, 442 Continue-31
boxíng/unboxing • 60 Controlo de fluxo • 24-32
chamada de construtores • 79 break-26, 31
destrutores • 233, 234 case • 26
métodos virtuais • 94 continue • 31
sistema de tipos - 57 default • 26
tratamento de excepções • 130 do-while • 27
CLS-4, 171 for • 28
CLSCompliant- 171 foreach • 29
CodeBehind • 385 goto-27, 31
Código hash • 253 if-else • 24
Coleccções quebra de ciclos • 30
LinkedList • ver LinkedList switch • 26
Colecções while • 27
ArrayList • ver ArrayList Conversões
BitArray • ver BitArray de e para cadeias de caracteres - 261
dictionary • ver Dictionary definidas pelo utilizador • 214
enumeradores • 187, 198 entre classes • 59
Else • 24 False - 16
EnableSession • 389 Ficheiros • Ver também Streams
Encapsulamento • 47 de texto-311
Enum- 112 gestão do sistema de ficheiros - 301
Enumerações • 112 hierarquia de classes • 301
Enumeradores • 187, 190, 198 leitura e escrita • 307
Equals-251 File • 301
Eratóstenes • cer crivo de Erastóstenes FileAccess • 310
Error-261 FileInfo-301
FileMode-310
© FCA - Editora de Informática 449
C#3.5
ICloneable - 257
thrcad • 337 V
timeslice • 333
tíraer • 367
ThreadStart • 333 Value • 145
Throw-132,134 Value types • 257
Timer • 367 teste de igualdade • 256
Tipos Variáveis • 19
anónimos • 179 declaração • 11
anuláveis • 217 Virtual - 54, 93, 100
classes -Ver Classes Visual Basic- 1,4,5,34,446
conversões • Ver Conversões entre tipos Void • 8, S2
enumerações • Ver Enumerações
estruturas • Ver Estruturas
inferência • ver inferência automática de tipos W
lógico • 15, 16
numéricos • 12, 13 Wait • 349
parcialmente definidos • 114 WaitOne - 346
ToString- 61 Web Forras-385, 443
Transrnission Transport Protocol • ver TCP Web Service Description Language • ver WSDL
True- 16 Web services • 4, 379
Try - 123 @WebMethod-381
Type • 108 pplication • 387
Typeof-107,108, 175 clientes • 383
CodeBehind • 385
configuração e instalação - 384
U criação • 381
dados disponíveis a um web servíce • 387
UDP • 396 Disco - 386
UdpCIient • 397 espaços de nomes • 386
Ulnt • 13, 60 informação de aplicação • 387, 388
UInti6-60 informação de sessão • 387, 388
UInt32 • 60 session • 387
UInt64 • 60 thread safeness • 389
Ulong -13, 60 WebMethod-381
Unboxing • Ver boxing/unboxing WebService-381,387
Unchecked • 137 Web.config • 386
Underflow • 138 WebClient-369
Unicode-16, 17,314 WebException • 384
UnicodeEncoding • 314 WebMethod-381
UTF7Encoding-314 WebRequest • 373
UTF-8-314 WebResponse • 373
UnicodeEncoding • 314 WebService-381
Uniform Resource Identifier • Ver URI Where • ver linq
Unsafe - 220 While • 27
URI • 371, 377, 375, 337, 371 Windows Forms • 3,165, 443
classe Uri • 377 Write-311
manipulação • 377 WriteLine- 9, 250,311
UriBuilder-377 número arbitrário de parâmetros • 12, 229
User•388 WSDL-380, 386
User Datagram Protocol • ver UDP wsdl.exe - 383, 384
Ushort-12,60
Using-8, 116, 118,238
UTF7Encoding-314
UTF-8-314
X XmISeriiiIizer-318,326
XinlElement • 328
XSD • 327, 328
XML-308, 327 xsd.exe - 328
documentação de código • 244
linq • 434
relação com SOAP • 380 y
schema • 328
seríalização de objectos • VerSerializaçSo de
objectos • formato XML yield • 196
|FCA
ISBN: 978-972-722-557-6
N°Págs.:488 Conteúdos: Evolução dos equipamentos Windows CE e Windows Mobile; • A plataforma
.NET e a Compact Framework 3.5; • Criação de um projecto Windows Mobile; • Programação
orientada a objectos; • O Visual Studio e o desenho de interfaces; • Teste, debugging e gestão
de erros; • Empacotamento, distribuição e instalação; • Persistência de dados com o SQL Server
Compact 3.5; • Web Services; Tópicos avançados; • SDK Windows Mobile 6.0 .
A plataforma .NET tem sido adoptada pela comunidade de desenvolvimento Web desde o seu
lançamento, em 2002. De forma a fornecer um melhor desempenho, flexibilidade e redução no
trabalho de codificação, a Microsoft lança agora o ASP.NET 3.5. Este livro, com vários
exemplos práticos, apresenta as principais características relacionadas com a construção de
aplicações Web através do ASP.NET 3.5. Inicia com a apresentação da framework que serve de
suporte ao desenvolvimento de páginas (Web forms e ASP.NET server controis simples) e
introduz gradualmente todas as novas funcíonalidades disponibilizadas.
Esta obra tem como objectivo ensinar o programador que se está a iniciar na plataforma
ASP.NET, sendo também uma ferramenta indispensável para o programador conhecedor da
framework ASP.NET que pretende fazer a transição para a nova versão.
ISBN: 978-972-722-615-3
N"l'ágs.:8l6 Conteúdos: Validação de dados e scripts no lado cliente • Configuração de aplicações e
tratamento de erros • Controlos data source e controlos data bound • Serviços, segurança e
perfis de utilizadores • User controis, master pages, themes e skins * Localização de aplicações
• Ciclo de vida de uma página • Handlers e módulos • ASP.NET AJAX.
ou na n/Distribuidora L1DEL
SAIBA MAIS SOBRE NÓS
EM WWW.FGA.PT
VISUAL BASIC Este livro cobre, de uma forma clara e acessível, as técnicas de programação em Visual Basic
2008 m*wwm associadas à tecnologia .NET. As matérias são apresentadas passo a passo e reforçadas com
uma vasta componente prática, composta por exercícios resolvidos e propostos. Expondo os
temas de forma objectiva e aliando a teoria à prática, é uma obra útil tanto para os que estão a
começar a dar os primeiros passos na programação em Visual Basic, como para os que já
possuem conhecimentos nesta área.
Inclui noções gerais de algoritmia e de usabilidade, destinadas a quem nunca programou. No
final de cada capítulo, o leitor encontra teses de consolidação que lhe permitem avaliar os
conhecimentos adquiridos.
Conteúdos: Introdução à programação • Princípios do modelo de programação orientada a objectos *
ISBN: 973-972-722-294-0 Desenho de interfaces gráficas (Windows Forms e Windows Presentation Foundation) • Gestão e
N" Págs.: 448 manipulação de bases de dados relacionais (SQL Server e Access) com ADO.NET * Desenvolvimento
Completo de um projecto baseado num caso real.
Esta obra tem como objectivo fornecer uma competência sólida no desenvolvimento de programas de
média e elevada complexidade e um conhecimento profundo sobre estruturas de dados avançadas e
algoritmos complexos, usando a linguagem de programação C e aplicando o paradigma da
programação modular. Assim, utiliza uma metodologia que dá particular ênfase à decomposição
funcional das soluções, através da implementação de tipos abstractos de dados. Para atingir este
objectivo, ela está organizada em quatro grandes temas: O estudo das principais estruturas de dados
dinâmicas; O estudo das principais classes de algoritmos, tendo ern consideração a sua complexidade;
O estudo da implementação dos diferentes tipos de memórias; O estudo do tipo abstracto de dados
grafo (Graph), com especial destaque para a implementação dinâmica baseada em listas ligadas, e dos
seus algoritmos mais importantes.
ISBN: 578-972-722-295-7
N° Págs.: 520 Conteúdos: Recursividade • Estruturas de Dados Dinâmicas • Programação Modular • Memórias
Associativas • Memórias de Acesso Aleatório • Pesquisa, Selecção e Ordenação * Filas e Pilhas •
Grafos.
j fca@fcs.pt
INTELIGÊNCIA ARTIFICIAL - Fundamentos e Aplicações 2a Edição Rev, e Aum.
Tecnologias de Informação
A Análise Inteligente de Dados (AID) aborda todo um conjunto de algoritmos computacionais que
permitem processar e analisar dados em bruto, de modo a extrair informação útil. Este novo campo
de investigação define-se na intersecção de áreas como a Estatística, a Inteligência Artificial, a
Aprendizagem Automática, as Bases de Dados e os Sistemas de Informação. Este livro aborda a
AID, com especial destaque para a aprendizagem supervisionada. No início são introduzidos os
conceitos básicos, seguindo-se uma descrição detalhada de diversos tipos de algoritmos
supervisionados, tais como: Árvores de Decisão e Regressão, Regras de Classificação e Regressão,
Modelos Lineares e Redes Neuronais Artificiais. Para além da descrição de cada algoritmo, é
apresentada uma implementação do mesmo em Java, sendo também apresentados diversos
exemplos de aplicação. Por fim, explica-se como se podem avaliar e comparar algoritmos de AID,
descrevendo-se também quais os tópicos avançados nesta área. E um livro interesse para alunos e
profissionais das áreas das TI e Comunicação.
Conteúdo- Introdução e conceitos básicos sobre a Análise Inteligente de Dados (AID) -
Al " Stmo de aprendizagem supervisionada - Aplicações da AID em problemas do mundo real •
AvTaçTe comparação^ Algoritmos de AID - Tópicos avançados: Comunas de Modelos
Sistemas Híbridos, Selecção de Modelos, Meta-Aprend.zagem e Aprendizagem Dinâmica
Implementação em Java de diversos algoritmos apresentados.
cnologias
Informação
Este livro é destinado a todos os profissionais, investigadores e estudantes universitários que adoptem a
linguagem C# para o desenvolvimento de aplicações. Esta nova edição trata de forma completa a versão
3.0 da linguagem C# e a versão 3.5 da plataforma .NET.
Hernâni Pedroso - Mestre em Engenharia Informática pela Universidade de Coimbra. Desenvolveu a sua
actividade profissional principalmente na empresa Criticai Software, S. A., em soTtware para sistemas
críticos. Foi um dos principais arquitectos do sistema WMPI (Windows Mvssage Passin» Interface). A sua
investigação teve um enfoque especial nas áreas de sistemas distribuídos e paralelos, tolerância a talhas
e computação de elevado desempenho.
ISBN 978-972-722^03-6