Explorar E-books
Categorias
Explorar Audiolivros
Categorias
Explorar Revistas
Categorias
Explorar Documentos
Categorias
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