Anda di halaman 1dari 480

Abordagem completa da linguagem C# 3.0 e .NET 3.

5
Inclui execução concorrente, acesso à rede, e LINQ
DA INFORMÁTICA
Martins
ED.)

XA MÁGICA - O LINUX EM

_<^ .&*~ Oc° . fena Nunes


C° ^ .<* v^FICE 2007 PARA TODOS NOS
t*. f ' r

e TECNOLOGIAS INTERACTIVAS

CS3 CURSO COMPLETO


Ferreira
2007 Depressa & Bem

v ^WERPOINT 2007 Fundamental


3 jvlaria José Sousa
PROGRAMAÇÃO COM EXCEL PARA ECONOMIA E
GESTÃO (2aED.ACT. E AUM.)
Adelaide Carvalho
PROGRAMAÇÃO DE SISTEMAS DISTRIBUÍDOS
EM JAVA
Jorge Cardoso
PROGRAMAÇÃO PARA DISPOSITIVOS MOVEIS
EM WINDOWS MOBILE 6
Joaquim Alves
EXCEL 2007 MACROS &. VBA Curso neto Ricardo Queirós
Henrique Loureiro REVIT ARCHITECTURE Curso Completo
EXERCÍCIOS DE ACCESS 2007 José Garcia
Carla Jesus SEGURANÇA EM REDES INFORMÁTICAS (2* ED.
EXERCÍCIOS DE EXCEL 2007 AUM.)
Paulo Capela Marques André Zúquete
EXERCÍCIOS DE PHOTOSHOP CS3 &CS2 VISUAL BASIC 2008 Curso Completo
Miguel Linhares Henrique Loureiro
FLASH CS3 Curso Completo WINDOWS SERVER 2008 Curso Completo
Pedro Cid Ferreira António Rosa
FLASH CS3 Depressa & Bem WINDOWS VISTA Fundamental (2a ED. ACTO -
Heider Oliveira SP1
GESTÃO DE PROJECTOS COM O MICROSOFT Carla Jesus
PROJECT 2007 WORD 2007 Domine a 110%
Rui Feio Isabel Vaz
GESTÃO DE PROJECTOS DE SOFTWARE (3a ED. WORD 2007 Guia de Consulta Rápida
ACTO Joaquim Alves
António Miguel

DlRUA-SE AO SEU FORNECEDOR HABITUAL OU

CONSULTE-NOS POR EMAIL: livrarialx@Iidel.pt


Paulo Marques
Hernâni Pedroso
Ricardo Figueira

FCA - Editora de Informática


R.D. Estefânia, 183-1° Esq° - 1000-154 Lisboa
Tel: 21 353 27 35 (Departamento Editorial)
E-mail: fca@fca.pt url:www.fca.pt
DISTRIBUIÇÃO

Lidei — edições técnicas, [da

SEDE: R. D. Estefânia, 183, R/C Dto., 1049-057 LISBOA


Internet: 21 354 14 18 - livrarialx@Iidcl.pt / Revenda: 21 351 14 43 - revenda@Iidel.pt
FormaçÍIo/Marketing:21 351 1448-formacao@Iidel.pt 7markeling@Iidel.pt
Ens, Línguas/Exportação: 21 351 I442-depinlemacional@lidel.pt
Fax:213577827-21 3522684

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

Copyright © Janeiro 2009


FCA - Editora de Informática, Lda.
ISBN: 978-972-722-403-6

Capa: José M. Ferrão - Look-Alieac!

Impressão e acabamento: Tipografia Lousanense, Lda. - Lous3


Depósito Legal N.° 285437/08

FCA - Marca Registada de FCA - Editora de mformólica, Lda.

FUNAMENTAL fí/77 - Marcas Registadas de FCA - Editora de Informática, Lda.

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.

Gostaria de agradecer à FCA todo o apoio prestado e a preserverança demonstrada ao


esperarem, pacientemente, que esta nova edição estivesse concluída. O trabalho de uma
editora é quase parental, compreendendo desculpas, adiamentos sucessivos, e tratando de
dar os incentivos correctos para que o trabalho chegue a bom porto.

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

© FCA - Editora de Informática


PREFACIO

A PLATAFORMA .NET E A LINGUAGEM C#


A importância assumida pela plataforma .NET é enorme, quer pela sua abrangência, quer
pela profundidade tecnológica que lhe está associada. Esta nova plataforma de
desenvolvimento de sofhvare irá ter um impacto enorme na indústria de sofhvare e, em
particular, na produtividade dos programadores. Ao quebrar as barreiras de integração
entre várias linguagens de programação e ao criar novos cenários para a integração de
aplicações e dispositivos com base em web sennces, a plataforma .NET altera, de forma
radical, o panorama no campo da engenharia de software. A utilização de standards
abertos da indústria permite a interligação com os diferentes sistemas em utilização e
permite que a plataforma .NET se constitua como a base à qual as empresas podem
recorrer para interligar os seus produtos e serviços.

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

© FCA - Editora de Informática XI


ÍNDICE

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 ^=^=^=^=^==^==^=^=^^==^==^^^

4.1.2 Boxing/Unboxing... .60


4.2 Campos de uma classe 63
4.2.1 Níveis de acesso de membros 63
4.2.2 Níveis de acesso de topo ....65
4.2.3 Constantes. 67
4.2.4 Membros estáticos 68
4.3 Construtores 72
4.3.1 InicialJzação por omissão 74
4.3.2 Iniciadores de objectos 75
4.3.3 Utilização de vários construtores 75
4.3.4 Utilização genérica de this ....76
4.3.5 Construtores estáticos........ .....78
4.3.5.1 Membros static readonly 80
4.3.6 Inicialização directa de campos ..81
4.4 Métodos simples 82
4.4.1 Visibilidade de variáveis 83
4.4.2 Overloading ...83
4.4.3 Passagem de parâmetros...... ...86
4.4.3.1 Passagem por referência (RBF) ..87
4.4.3.2 Variáveis de saída (OUT) .....88
4.5 Redefinição de métodos 90
4.5.1 Overriding simples.. ..91
4.5.2 Polimorfismo. ....93
4.5.3 Gestão de versões .94
4.5.4 Classes seladas. ..96
4.5.5 Classes abstractas..... ......97
4.5.6 Modificadores de métodos 100
4.5.6.1 Modificador extern 100
4.5.7 Interfaces..... 101
4.5.7.1 Herança de interfaces 105
4.6 Conversão entre tipos 106
4.6.1 Operador is 107
4.6.2 Operador as 107
4.6.3 Operador typeof .....108
4.7 Estruturas 109
4.8 Enumerações ....112
4.9 Definições parciais 114
4.9.1 Tipos parcialmente definidos 114
4.9.2 Métodos parcialmente definidos 115
4.10 Espaços de nomes 116
4.1Q.I Áliases 118
5 - EXCEPÇÕES 121
5.1 Um primeiro exemplo 121
5.2 Estrutura genérica 129
5.3 Lançamento de excepções 132
5.4 Hierarquia de excepções 135
5.5 Excepções de aritmética 137

Xfv © FCA - Editora de Informática


ÍNDICE

6 - PROGRAMAÇÃO BASEADA EM COMPONENTES 141


6.1 Propriedades 144
6.1.1 Propriedades automáticas............................................................. 146
6.1.2 Propriedades Indexadas 147
6.2 Eventos 150
6.2.1 Delegates 150
6.2.2 Multicast delegates 154
6.2.3 Métodos Anónimos 155
6.2.3.1 Métodos anónimos usando delegates 156
6.2.3.2 Expressões lambda 158
6.2.4 Sistema de eventos na plataforma ,NET 162
6.2.5 Um exemplo utilizando Windows Formi'......................................... 165
6.3 Atributos 167
6.3.1 Alvo dos atributos 170
6.3.2 Definição de novos atributos 171
6.3.3 Obtenção de atributos em tempo de execução 174
7 - TÓPICOS AVANÇADOS 179
7.1 Tipos Anónimos 179
7.2 Expressões de Consulta .180
7.3 Inferência Automática de Tipos 181
7.3.1 Inferência em variáveis locais 181
7.3.2 Inferência em tabelas 183
7.3.3 Inferência em expressões de consulta 184
7.3.4 Inferência em expressões lambda 186
7.4 Enumeradores e Interadores 187
7.4.1 A interface lEnumerable 187
7.4.2 Iteradores 195
7.4.3 Enumeradores genéricos............................. 198
7.5 Genéricos 201
7.5.1 Definição de tipos genéricos ..203
7.5.2 Definição de métodos genéricos..... ..205
7.6 Redefinição de operadores 210
7.6.1 Redefinição simples de operadores .......................210
7.6.2 Conversões definidas pelo utilizador ............................................214
7.7 Tipos Anuláveis 217
7.7.1 Operador de aderência anulo.............................. 219
7.8 Ponteiros 220
7.8.1 Sintaxe 221
7.8.2 Aritmética de ponteiros ...223
7.8.3 Ponteiros e tabelas ...............224
7.8.4 Ponteiros para membros de classes 225
7.9 Métodos com número arbitrário de parâmetros 227
7.10 Métodos de Extensão 230
7.11 Destruição de Objectos 232
7.11.l Sintaxe............. 233
lM.2 Dispose Q Close 235
7.11.3 A interface IDisposable ....238
© FCA - Editora de Informática XV
C#3.5

7.12 Pré-Processamento 240


7.12.1 Directivas #defme e #undef..... ......241
7.12.2 Directivas #if, #elif; #else e #endif 241
7.12.3 Directivas #warning e #error......... 242
7.12.4 Directiva #line... 242
7.12.5 Directivas #region e #endregion.... 243
7.13 Documentação emXML .....244

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

© FCA - Editora de Informática


_^ ÍNDICE

9 - EXECUÇÃO CONCORRENTE 331


9.1 Gestão de threads 333
9.1.1 Controlo de threads. ......................335
9.1.2 A classe Thread 337
9.2 Sincronização 338
9.2.1 O problema da atomicidade. 339
9.2.2 Secções críticas ...............343
9.2.3 A classe Mutex.. 346
9.2.4 Monitores.............. 348
9.2.4.1 Exemplo: produtor/consumidor com buffer finito 350
9.2.5 A classe Semaphore..... 358
9.2.6 Outros objectos de sincronização .......................364
9.2.6.1 Classes AutoResetEvent e ManualResetEvent 364
9.2.6.2 Classe ThreadPool 365
9.2.6.3 Classe ReaderWriterLock.. 365
9.2.6.4 Classe Timer. .367
l O - ACESSO À INTERNET 369
10.1 Acesso a recursos na Internet 369
10.1.1 Classe WebClient ..369
10.1.2 Classes WebRequest e WebResponse...... 373
10.1.3 Classes utilitárias 374
10.1.3.1 Configuração á&proxies 374
10.1.3.2 Resolução de endereços 375
10.1.3.3 Manipulação de URI ................377
10.2 Web seivices 379
10.2.1 Criação de web sejvicês 381
10.2.2 Clientes de web services. .........383
10.2.3 Configuração e instalação 384
10.2.3.1 Questões de configuração.. 384
10.2.3.2 Espaços de nomes 386
10.2.4 Informação disponível a um web sennce ............387
10.3 Utilização do protocolo TCP/IP 391
10.3.l Protocolo TCP ...............391
10.3.1.1 Servidores TCP.... 392
10.3.1.2 Clientes TCP ........396
10.3.2 Protocolo UDP 396
10.3.2.1 Utilização do protocolo UDP ........397
l 1 - INTRODUÇÃO À LINQ 4O l
11.1 Expressões de Consulta 402
11.1.1 Expressão From..... 403
11.1.2 Expressão Where.... 405
11.1.3 Expressão Select... 406
11.1.4 Expressão Group .............407
11.1.5 Expressão Into.... 409
11.1.6 Expressão Orderby 410

© FCA - Editora de Informática xvi i


C#3.5

11.1.7 Expressão Join .411


11.1.8 Expressão Let.... 416
11.1.9 Operações genéricas e de agregação..... 419
11.2 Arquitectura LINQ 422
11.2.1 LÍNK para SQL.... 424
11.2.1. l Ferramenta SqlMetal 425
11.2.1.2 Visual studio .425
11.2.1.3 Mapeamento por atributos........... 427
11.2.1.4 Mapeamento por ficheiros XML.. 431
11.2.1.5 Inserção e actualização de elementos ..................432
11.2.2 LINK para XML 434
12- EXPLORAÇÕES FUTURAS 441
12.1 Interfaces de programação 441
12.1.1 Common Languagr Ritntime(CLK) ....442
12.1.2 BaseClassLibraiy(BCÍJ)....... ...........442
12.1.3 Acesso e manipulação de dados .......442
12.1.4 Acesso à rede.. 443
12.1.5 Programas gráficos..... 443
12.2 Conclusão..... 443
APÊNDICE CONVENÇÕES DE CÓDIGO 445
ÍNDICE REMISSIVO 447

xvni © FCA - Editora de Informática


Introdução ao primeiro programa em C# 9
Estrutura de um programa ........12
Tipos de dados numéricos .......15
Variáveis lógicas, caracteres e cadeias de caracteres 19
Constantes. 20
Expressões..... 24
Controlo do fluxo de execução...... 32
Tabelas simples ,...38
Tabelas multidimensionais e tabelas dentro de outras tabelas... .......42
Conceitos básicos de OOP 55
Funcionamento do GTS ..62
Níveis de acesso dos membros de uma classe 65
Níveis de acesso de topo ....66
Constantes e campos readonly. ....68
Membros estáticos .......72
Construtores 81
Métodos simples..... 90
Herança e polimorfismo .........105
Conversão entre tipos ........109
Estruturas..... .112
Enumerações ...114
Definições parciais 116
Espaços de nomes.. .119
Excepções 136
Excepções de aritmética 139
Propriedades ..149
Delegates e expressões lambda ......161
Eventos 165
Atributos .....177
Tipos anónimos, expressões de consulta e inferência ..187
lEnumerable e lEnumerator. 195
Iteradores... ..200
Genéricos.. ....209
Redefinição de operadores. 214
Conversões definidas pelo utilizador. .....217
Variáveis anuláveis 220
Ponteiros..... 227
Métodos com um número arbitrário de parâmetros.... 230
Métodos de extensão 232
© FCA - Editora de Informática XIX
C#3.5

Destruição de objectos........... 239


Pré-processamento 243
Comentários emXML ...246
Comparação de objectos.. 256
Cópia de objectos 259
A classe system.string , ........265
Formatação de cadeias de caracteres 270
Expressões regulares. ....275
Colecções 299
Acesso ao sistema de ficheiros .......306
Streams -.311
Ficheiros de texto.. 314
Ficheiros binários 318
Serialização de objectos para formato binário ......325
Serialização de objectos em XML ........329
Gestão de threads ......338
Atomicidade e secções críticas ..346
Classe Mutex 348
Monitores ....356
Semáforos...... ." 363
Outros objectos de sincronização .....368
Acesso a recursos Standard da Internet. ....373
Classes utilitárias para o acesso à Internet...... 379
Web Services........'.... 390
Utilização do protocolo TCP ...396
Utilização do protocolo UDP 399
LINQ-frora... 405
LINQ-where... 406
LINQ-select..... .407
LINQ-groirp 409
LINQ-into .410
LINQ-orderby. ...411
LINQ-join... 416
LINQ-Iet... 419
LINQ-agregação 422
LINQ para SQL. 433
LINQ para XML .439

© FCA - Editora de Informática


INTRODUÇÃO
O C#' é uma nova linguagem de programação, proposta pela Microsoft, para o
desenvolvimento de aplicações. Juntamente com o C# foi também introduzida a
plataforma .NET, que constitui um ambiente de execução sobre o qual as aplicações
correm.

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:

Orientada aos componentes: durante os últimos anos, um dos grandes passos


em frente que foram dados, em termos de engenharia de software, foi o conceito
de "componente". Um componente é uma unidade binária de código que pode ser
incluída numa aplicação. Tipicamente, a manipulação de componentes faz-se de
forma visual, por drag-and-drop e, configuração das suas propriedades e
interligações. A ideia é que o trabalho básico, do dia-a-dia de um programador,
deve ser juntar componentes, como se de peças de Lego se tratasse, preenchendo
apenas pequenas secções de código. O C# inclui características de programação e
de desenvolvimento de componentes directamente na linguagem, tornando-a

1 C# lê-se em inglês Csharp.


© FCA - Editora de Informática
C#3.5

muito prática, tanto para a construção de aplicações baseadas em componentes,


como para o desenvolvimento dos próprios componentes;

Robusta e moderna; o C# é uma linguagem orientada aos objectos, possuindo


mecanismos como: garbage collecíion, que liberta o programador da gestão
explícita da memória; excepções, que permitem uma gestão robusta dos erros nos
programas; gestão de versões de módulos, que permite que as classes e os
programas evoluam ao longo do tempo; e introspecção, que permite determinar
os tipos dos objectos dinamicamente e realizar conversões entre eles. Estas
características e muitas outras permitem ao programador, construir aplicações
robustas e de uma forma muito mais segura do que tipicamente acontece com
linguagens como o C-H-;

Familiar: o C# baseia a sua sintaxe na linguagem C-H- e, em certa medida, na


linguagem Java. Hoje em dia existem muitos programadores que já conhecem
essas linguagens, fazendo com que a transição para o C# seja relativamente
pacífica.

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:

Gestão automática de memória: o CLR dispõe de um garbage collector que se


encarrega de limpar os objectos que já não estão a ser utilizados pelas aplicações;

• Segurança: o CLR possui mecanismos que permitem atribuir permissões ao


código, baseadas na sua proveniência e em quem o está a executar. Existem
também mecanismos que permitem garantir que o código a executar é válido e
não irá corromper outros programas que se encontrem a executar no CLR;

• Tradução de código intermédio (IL) para código nativo: ao compilar um


programa na plataforma .NET, tipicamente, este é traduzido de uma linguagem de

2 © FCA - Editora de Informática


INTRODUÇÃO

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;

Carregamento dinâmico de classes: o CLR torna possível carregar, em tempo


de execução, segmentos de código que antes não estavam presentes na máquina
virtual.

C# VB C++ JScript (...)

Common Languagé Specifícation

ASP.NET Windows Forms

Data & XML

Base Class Líbrary (BCL)

Common Languagé Runtime (CLR)


.

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.

Por cima da máquina virtual básica, existe um conjunto de bibliotecas estandardizadas


que permitem às aplicações, tomar partido de um rico conjunto de APIs. Existe um
conjunto de bibliotecas básicas, denominadas por Base Class Libraiy (BCL), que
oferecem recursos como acesso a ficheiros, estruturas de dados básicas (hash tables,
listas, etc.), acesso a recursos de rede e outras. Em seguida, encontram-se bibliotecas que
permitem aceder a bases de dados (ADO.NET) e manipular informação em geral (por
exemplo, em XML). No topo da hierarquia, estão as bibliotecas que permitem efectuar
desenvolvimento web (ASP.NET) e criar interfaces com o utilizador em Windows
(Windows Forms'),

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 é

© FCA - Editora de Informática


C#3.5 ^^^^=^^^=_

executado no Common Langitage Rimtime. A Common Langiiage Specification (CLS)


constitui uma norma que especifica o que é que tem de ser suportado a nível de uma
linguagem de programação para esta ser compatível com a infra-estrutura .NET e com
outras aplicações que se encontram a correr no CLR.

l .2 SOBRE ESTE LJVRO


Voltemos, então, um pouco atrás: porquê apreender uma nova linguagem e porquê
aprender uma nova infira-estrutura de desenvolvimento? Caso o leitor seja um
programador para o ambiente Windows, a resposta é bastante simples: a Microsoft está a
apostar de uma forma muito forte no .NET, sendo este o ambiente de desenvolvimento
que passará a ser utilizado de futuro, na família de sistemas operativos Windows. Uma
resposta mais completa seria: o C# é uma linguagem nova e moderna que visa facilitar o
desenvolvimento de aplicações. Combina as melhores características do C#, do Visual
Basic e mesmo do Java. Quanto à plataforma .NET} trata-se de uma plataforma nova,
bem pensada, que visa obter uma forma simples e unificada de desenvolvimento de
aplicações. Um ponto bastante forte da plataforma .NET é que resolve alguns problemas
que dificultam a programação em Java de uma forma limpa". Ao mesmo tempo, a
plataforma foi pensada para que seja fácil obter interoperabilidade entre aplicações que se
encontram a correr na mesma máquina (através do CLR e utilizando a Common
Langiiage Speciflcation, e, ao mesmo tempo, conseguir interoperabilidade com aplicações
que se encontram a correr noutras máquinas e, em particular, na Internet (via os
chamados web services, que interligam as aplicações servidor, usando protocolos como o
SOAP e o HTTP).

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:

Programação em C#: o C# é a linguagem por excelência do .NET. Neste livro,


começamos por mostrar ao leitor, como programar nesta nova linguagem. Em
particular, é abordada a versão 3.0 da linguagem C#;

Bibliotecas base disponíveis no .NET (BCL): existem imensas bibliotecas


disponíveis na plataforma .NET. Nós cobrimos o conjunto de classes fundamentais

" 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

ao desenvolvimento de qualquer aplicação, seja ela baseada na web, para


Windows ou mesmo para terminais móveis, baseando-nos também na versão 3.5
da plataforma .NET;

• Tópicos avançados: finalmente, cobrimos questões avançadas como


multiprocessamento e sincronização, acesso a recursos de rede e reflexã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.

Esperamos que este livro lhe seja útil.

© FCA - Editora de Informática


w

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#!");
}

Listagem 2.1 — Primeiro programa em C# (ExemploCap2_l.cs)

O objectivo deste programa é muito simples. Ao executar, deveremos ver a mensagem


ro mèú^pp-Tméiro .programa em"c#! ."L.".™!".'". ~~Z.".l'.i _~_ZLL.'_"1~ :~...~1 _I •.:"."":.".". !!"ll_"' .'.

ser escrita no ecrã.

No entanto, antes de executar o programa, é necessário traduzir o programa de C# para


código que a máquina entenda (compilação). O código fonte é escrito num editor de
texto1 e em seguida, corre-se o compilador sobre os ficheiros relevantes, resultando num
executável (.exe). Neste caso em particular, para compilar e executar o programa
t'ExemploCap2_l.cs" faz-se:

Visual Cf 2008 comei ler versipn^3.5_.21Q22.8

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

" Frãméwòrk"version"375.....~ .....


Icopyright CO Microsoft Corporation. Ali rights reserved.
|C : \Ll v ro\exemp1 os>Exempl oCap2_l
|o_meu ..primeiro _RrgaranLa_em. C# !_________________________________ _ .
Vamos, agora, analisar a estrutura deste programa. O C# é uma linguagem quase
totalmente orientada aos objectos, pelo que mesmo um programa simples exige que
estejam presentes alguns elementos de "orientação aos objectos". Neste momento, não é
fundamental conseguir compreender este programa de uma forma completa. Apenas
queremos dar ao leitor uma noção da estrutura de um programa em C#. Os elementos que
agora poderá não perceber, rapidamente se tomarão claros.

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

O corpo do método Mal n C) possui apenas uma linha:


:ÇQnjól é/Wrj ^ "."_.".".-.-."."•
O que esta linha faz é imprimir no ecrã a tão esperada frase. Ao escrevermos
console.wrlteLlneQ, estamos a manipular a classe console, que representa o ecrã e o
teclado, chamando o método wrlteLlneQ sobre a mesma. Isto permite enviar uma frase
(cadeia de caracteres) para o ecrã. Em C#, as cadeias de caracteres (strings) são
representadas entre aspas. Finalmente, é de referir que todas as expressões em C# são
terminadas com ponto e vírgula.

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).

" Os programas são organizados em classes, que encapsulam dados e


AROKR métodos.
_, _ Os métodos representam o local onde é implementada a funcionalidade
Introdução ao r r
primeiro desejada, sobre a forma de um conjunto de instruções.
programa em C# " Os blocos de código são agrupados por chavetas.

" 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.

2.2 UM EXEMPLO COMPLETO


Um programa, ao executar, está continuamente a manipular dados. Na verdade, um
programa manipula muitos tipos diferentes de dados. Cada linguagem de programação, e
o C# não é excepção, fornece um conjunto básico de tipos de dados que permitem
armazenar e representar diferente informação. Uma grande vantagem das linguagens
orientadas aos objectos é permitirem ao programador, estender os tipos de dados
existentes, adicionando novos tipos. Na verdade, um tipo de dados abstracto é
simplesmente informação associada a um conjunto de operações que é possível realizar
sobre a mesma. Finalmente, às diversas expressões que o programador escreve para

© FCA - Editora de Informática 9


C#3.5 ^^^^^^=^^^^^^^====^^=^^==—=====^_

manipular os dados do programa, chama-se fluxo cie execução do programa, existindo


também expressões condicionais, que permitem tomar decisões, baseadas no conteúdo
dos dados do programa.

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;
}

Listagem 2.2 —Imprime no ecrã as raízes quadradas dos múltiplos de 10,


menores ou iguais a 100 (ExemploCap2_2.cs)

O resultado da execução deste programa é:


•c':\Livr'õ\exemp1Õs>Èxèmp1 ocap2_2.exe "" "~ -—-- - ,
!A raiz de 10 é 3.1622776601683795 . i
,A raiz de 20 é 4.47213595499958 i
iA raiz de 30 é 5.4772255750516612
iA raiz de 40 é 6.324555320336759
ÍA raiz de 50 é 7.0710678118654755 ;
A raiz de 60 é 7.745966692414834
A raiz de 70 é 8.3666002653407556
A raiz de 80 é 8.94427190999916 :
A raiz de 90 é 9.4868329805051381
A .raiz de 100_ é^lO _ _ _ _ . _ . _ . _ _.._ . :
0 primeiro ponto a examinar neste programa é a'forma como os comentários são escritos
no código fonte. Tal como em outras linguagens, tudo o que se encontra entre os símbolos
/* e */ é considerado um comentário. A este tipo de comentários, chama-se comentários
de múltiplas linhas, devido a poderem ser utilizados para incluir no código, uma extensão
mais ou menos longa de texto, que pode prolongar-se por várias linhas. No entanto, existe
1O © FCA - Editora de Informática
ELEMENTOS BÁSICOS

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.

Uma linha importante deste programa é a que declara a variável número:


*jnt " " ''~ " '" " ™ ' '* "
Esta linha declara uma variável de nome "numero", cujo tipo é inteiro. Isto é, uma
variável que pode apenas representar números inteiros, sejam estes positivos ou
negativos.

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) ....... " ............. " " - " ~•

representa um ciclo. Tudo o que se encontra entre chavetas é executado enquanto a


condição for verdadeira. Neste caso, enquanto a variável numero for menor ou igual a
100. Este é um exemplo de uma expressão de controlo do fluxo de execução do
programa.

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.

© FCA - Editora de Informática


C#3.5

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) . .;_'

O método WriteLineQ pode levar um número arbitrário de parâmetros. O primeiro


parâmetro representa uma cadeia de caracteres formatada, de acordo corn o que se
pretende que surja no ecrã. Os parâmetros seguintes são as variáveis que deverão ser
substituídas em certas posições da frase, ao ser enviada para o ecrã. O método
writeLineQ irá substituir todas as ocorrências das entidades {n}, em que n é um
número, pelo parâmetro correspondente que lhe é passado. Assim {0} irá representar a
variável número e {1} a variável raiz.

A última linha do ciclo é uma atribuição simples, que coloca na variável número o valor
corrente somado de 10.

" Existem comentários de linha (//) e de múltiplas linhas (/* . . . */)•


ARETER
~ As variáveis são declaradas escrevendo o tipo, seguido do nome da
Estrutura de um
variável.
programa " Pode-se inicializar as variáveis quando estas são declaradas.
" As variáveis têm de ser inicializadas antes de serem utilizadas, sob pena de
haver um erro de compilação.
" As variáveis podem ser declaradas em qualquer ponto do código.
~ O método console.writeLineO aceita um número variável de parâmetros,
sendo cada parâmetro representado por {n} na cadeia de caracteres enviada
para o ecrã. n representa o número do parâmetro em causa.

2.3 TIPOS DE DADOS

2.3.1 TIPOS ELEMENTARES

2.3.1.1 TIPOS NUMÉRICOS

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.

© FCA - Editora de Informática


ELEMENTOS BÁSICOS

(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.

Tabela 2.1 —Tipos elementares de dados numéricos

Os tipos de dados mais importantes são o i nt e o doubl e. O primeiro é utilizado na


grande maioria das situações em que é necessário declarar uma variável inteira, o
segundo, na generalidade das situações em que é necessário utilizar números reais.

Um outro tipo de dados interessante é o decimal. Este tipo de dados é tipicamente


utilizado em situações em que se esteja a lidar com variáveis que representam dinheiro.
Embora a sua gama não lhe permita valores tão grandes e tão pequenos como o doubl e,
variáveis do tipo decimal são capazes de representar números muito grandes, sem perder
precisão. Os casos monetários são exemplos clássicos deste tipo de situação. Se uma
quantia for representada num doubl e, e se a quantia for muito grande, ao adicionar um
pequeno valor (por exemplo, adicionar alguns cêntimos a alguns milhões de euros),
devido ao factor de escala do doubl e, os cêntimos são perdidos. O decimal foi concebido
para evitar este tipo de problemas, mantendo grande precisão na representação de
números.

2.3.1.2 VAUORES LITERAIS


Quando se faz uma atribuição de um valor a uma variável, chama-se ao valor um valor
literal. Por exemplo, na expressão seguinte:
'int x;;^^ssi./r_."iv^"."i_v"."-.".7Ji.."J-""-. jrizri:./""^' _.i.::....V^M^V-^' .'.
o valor 10, é um valor literal. Ao escrever-se um literal, pode-se indicar o seu valor em
decimal ou em hexadecimal.
íírit x - ICR " '"-/-/- um literal "siníplès e"corisi"dêradb" déciinal "•"' ;
jint y = OxOa; [J, .Um .númerp_Jiexadeclmal_,comesa,copi "Qx" . _ ... .:

© FCA - Editora de Informática 13


C#3.5

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____________________________._ ._. _.....!

2.3.1 .3 CONVERSÕES EMIRETIPOS NUMÉRICOS

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:

jint x = k; // correcto, qualquer valor de um byte "cabe" num int


[ byte .L_f=. .x;___________//_Er_ro]_ __Ne_m.Jtgdos__gs 1 nt são _r_epres_ent:áye1 s _num .byte

Consideremos ainda o seguinte exemplo:


;1rit ~~~ãT= "10';" ..... "" .......
:double x = 2 0 . 0 ;

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

FCA - Editora de Informática


ELEMENTOS BÁSICOS

conversão implícita, é gerado um erro de compilação que alerta o programador para o


problema. Mesmo assim, se o programador achar que a perda de precisão é aceitável ou
que o valor presente na variável pode ser garantidamente convertido, então, poderá fazer
uma conversão explícita. Vejamos o seguinte exemplo:
;çJpubl;e.;a = 2Q,0; ~ ~~ ~ , ! " ~ t ,
int k = a + 1; __ /^^Exrpj.P-_rfisi0.tado^qderÁ_nãq__caDer em_k
Neste caso, o valor l (int) é convertido em double para a soma poder ser efectuada.
A soma resultará ainda num double. Assim, está a ser feita a atribuição de um double a
um int. Claramente, um inteiro não consegue representar todos os valores que um
doubl e consegue, logo, é gerado um erro de compilação.

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.

© FCA - Editora de Informática 15


C#3.5

2.3.2 O TIPO LÓGICO


Um outro tipo de dados muito importante é o tipo lógico — bool, abreviatura de boolean.
Uma variável lógica permite armazenar um valor que representa verdadeiro ou falso.
Embora, estritamente falando, apenas fosse necessário um bit para armazenar um valor
lógico, por uma questão de optimização3, um valor lógico ocupa um byte inteiro. Uma
variável deste tipo apenas pode ter dois valores: true ou false. A tabela 2.2 mostra a
principal Informação sobre este tipo de dados.

NOME li TOTAL DE BYTES |[ GAMA DE VALORESJ j DESCRIÇÃO " l


BOOL. || l " |( true ou false \o que permite armazenar um valor lógico. |

Tabela 2.2 — O tipo elementar bool

Eis una pequeno exemplo:


'bõoT "fimDòP"ro~g"ramã = true;" ""

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.

2.3.3 CARACTERES E CADEIAS DE CARACTERES

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.

\E 1 j TOTAL DE BYTES [ DESCRIÇÃO


í CHAR li 2 ' •" ; Tipo que permite representar um carácter (letra).

Tabela 2.3 — O tipo elementar 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

A tabela 2.4 sumaria as sequências de escape reconhecidas em C#.


SEQUÊNCIA ; DESCRIÇÃO
V ""
• Plica
A" Aspa

\ : Barra para trás


• Valor nulo
"\ •-••
' Som (beep}
Ab - - - - ; Andar para trás (backspacé)
:\ ~ ~ "
' Form feed
An Nova linha
\ Retornar ao início da linha (carriaqe returri)
\ Tabulação horizontal
\ • Tabulação vertical

Tabela 2.4 — Sequências 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.

2J3.3.1 CADEIAS DE CARACTERES


As cadeias de caracteres, ou strings, são representadas pelo tipo string. Na verdade, as
stríngs são objectos e não elementos de um tipo de dados primitivo. Assim, nós iremos
adiar parte da discussão deste tipo de dados para mais tarde, quando já tivermos falado de
programação utilizando objectos.

Pelo facto de as cadeias de caracteres serem tão importantes em qualquer tarefa de


programação, os engenheiros que criaram a linguagem resolveram dar-lhe um tratamento
especial que transcende o disponível para simples objectos. Em C#, um objecto do tipo
string ocupa sempre no mínimo 20 bytes, contendo um conjunto imutável de caracteres.

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"); . . . . . . . . . . .... .,..._ :

O resultado da execução deste código é:


:"íguài.s.ir;;_ " " ' ; * ^ . ; . .. _. _ '.".'""_ ~__~'."'...". ~' *'„•
A tabela 2.5 resume a informação sobre o tipo st ri ng.

NOME \ NÚMERO DE BYTES~~\ DESCRIÇÃO """" ]!

string || 20 ou mais |'• Tipo que permite representar uma cadeia de caracteres.

Tabela 2.5 — O tipo stri ng

Como as cadeias de caracteres são na verdade objectos, é possível executar vários


métodos e operações sobre os mesmos. O código seguinte resume alguns.
istrihg fVasèl = ''A Insustentável Leveza" dó s é r " T
; string frase2 = " foi escrito por Milan Kundera.";

,// A frase3 é_o resultado da junção da frasel e frase2


rjng_7f fas_e3 _= frasel _+" _f rase2 ;"..'." "_'^ ".'_ ".7."""'^ "..-"'l '.'.' ".' _T.. /.".,_ . l

; // O^íéO? 9 íajl^hp da f raseB

y/ Obtém o primeiro carácter (carácter Q)_ da fraseS C A ' )


|chãr ',chrT ".'= frasèSTQ].; '.""""""". " T . " " ; . " . _ . " . _ " . " '."_"„" . " ...2.^'
'// obtém o segmento "A Insustentável" da fraseB
// o primeiro parâmetro de substring é a posição onde começar
// Ç3_PJÊLr!Ll_r de 0) e o segundo, o numero de caracteres a extrair.
ístfihq 'parte"- "f raséBJgubstfihgCOT^IS^ : " '
Mais tarde, estes elementos serão discutidos em maior detalhe. Estes exemplos são
apresentados aqui, pois permitem ao leitor começar, desde já, a experimentar algumas
operações em cadeias de caracteres. No entanto, não é possível discuti-los sem abordar
conceitos como métodos, propriedades e overload de operadores.

18 © FCA - Editora de Informática


ELEMENTOS BÁSICOS

" As variáveis lógicas são representadas pelo tipo booT.


ARETER
~ Uma variável lógica apenas pode ter dois valores: true ou false.
~ Os caracteres são representados pelo tipo char.
Variáveis lógicas,
caracteres e " Os literais de caracteres são especificados usando plicas.
cadeias de
caracteres " As sequências de escape permitem representar caracteres especiais,
começando sempre pelo carácter "\"-
" As sequências de escape mais utilizadas são a mudança de linha \ e a
tabulação horizontal \t.
" As cadeias de caracteres (sfrings] são representadas pelo tipo string.
~ Os literais das cadeias de caracteres são especificados entre aspas.
" As cadeias de caracteres são, na verdade, objectos, aos quais é possível
aplicar diversos métodos e operadores, assim como examinar as suas
propriedades.

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-

Corísol e.WriteLi ne(x);

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

© FCA - Editora de Informática 19


C#3.5

Uma constante tem de ser sempre inicializada na altura da sua declaração e não é possível
mudar o seu valor.

" As constantes são declaradas com a palavra-chave const.


ARETER
" As constantes têm de ser inícialízadas quando são declaradas e nunca
Constantes mudam de valor.
" As constantes podem ser declaradas em qualquer ponto do código.

2.6 EXPRESSÕES E OPERADORES


Sempre que se realiza uma operação matemática entre duas variáveis, está a utilizar-se
um operador. Uma expressão consiste numa sequência de operadores e operandos que
especifica um cálculo. Por exemplo, x=y+2; constitui uma expressão em que é feita a
atribuição do valor resultante da soma da variável y a 2. y e o literal 2 são operandos,
sendo + o operador.

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 __ _._

A tabela 2.6 apresenta os principais operadores existentes, a sua precedência e uma


pequena descrição dos mesmos. A precedência diminui de cima (o mais "forte") para
baixo (o mais "fraco" e último a ser avaliado), de acordo com as categorias apresentadas.
Dentro da mesma categoria, a precedência é definida da esquerda para a direita (isto é,
pela ordem da expressão).
PRECEDÊNCIA 1 ^OPERADOR nn^scRiçXo' """ " ~~\A
Ç expressão ) ]í Parêntesis, permitem agrupar uma expressão. l

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.

2O © FCA - Editora de Informática


ELEMENTOS BÁSICOS

(cont.)

í PRECEDÊNCIA |j OPERADOR | DESCRIÇÃO j


+x | Positivo unário. |
| -x [ Negativo unário, inverte o sinal.
i !x Not, nega o valor de uma variável lógica.
j ~x |i Complemento para um, nega todos os bitsâ& uma variável, |
UNÁRIA Pre-increment, incrementa o valor de uma variável antes de a
++X
i utilizar.
! Pre-decrement, decrementa o valor de uma variável antes de a
-x utilizar.
1 (tipo) x |Í Cast, conversão explícita de uma variável num certo tipo. |
1x * y | Multiplicação.
MULTIPLICATIVA TX 7 y" 11 Divisão.
fx %y I^Resto de divisão.
x + y ] r Soma.
ADITIVA
í^ ; ~y Subtracção,
x « n Deslocamento à esquerda, desloca os bits da variável x em n
biísà esquerda.
DESLOCAMENTO
Deslocamento à direita, desloca os b/isôz variável x em n bits
x » n
à direita,
j x < y || Comparação "menor que".
x > y Comparação "maior que". 1
RELACIONAL x <= y 1 T Comparação "menor que ou igual a".
x => y Comparação "maior que ou igual a".
x is tipo Verifica se uma variável é de um certo tipo.
("x == y |) Igualdade, testa se duas expressões têm o mesmo valor. ]
IGUALDADE
fx != y |j Diferente, testa se duas expressões têm valores diferentes.

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

• l or binário, realiza um ou (or) sobre os btts de duas


l expressões.
x && y 1 "e lógica', testa se ambas as expressões são verdadeiras. ]
OPERAÇÕES nou lógicd', testa se pelo menos uma das expressões é
LÓGICAS x M y verdadeira.
SOBRE
VARIÁVEIS rond ? a • b II Condição com resultado, verifica se a condição é verdadeira;
| caso seja, o resultado é a, senão o resultado será b .
(cont.)

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");

© FCA - Editora de Informática


ELEMENTOS BÁSICOS

•é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) )

Em geral, é aconselhável utilizar parêntesis em expressões complexas, mesmo que não


existam dúvidas no agrupamento das expressões. A razão é simples: do ponto de vista de
quem está a ler, toma-se muito mais claro o que está a acontecer e qual era a intenção do
programador quando escreveu a expressão.

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:

© FCA - Editora de Informática 2.3


C#3.5

;!<;+=:
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 ; )

" Todos os operadores possuem uma precedência associada.


ARETER H
&^ " A precedência corresponde, na maioria das vezes, às noções matemáticas
habituais.
Expressões
~ Em caso de dúvida, utiliza-se parêntesis para agrupar subexpressões.
" Os operadores de comparação de ordem são: <, <=, > e >=.
" Os operadores de igualdade, de desigualdade e de negação são: ==, != e !.
~ Os operadores lógicos de expressões, e lógico e ou lógico, são: && e 1 1 .
" Os operadores de incremento e de decremento têm uma forma em prefixo
(-H-X/—x), que incrementa a variável antes de a utilizar, e uma forma de
sufixo (x++/x—) que utiliza o valor e depois, actualiza a variável.

2.7 CONTROLO DE FLUXO


Em C#, tal como em muitas outras linguagens, existem várias expressões que permitem
controlar o fluxo de execução de um programa. Isto é, é possível executar
condicionalmente um conjunto de instruções, de acordo com o resultado de uma
expressão lógica. De seguida, apresenta-se as principais expressões de controlo de fluxo.

2.7.1 EXPRESSÃO IF-ELSE


A j a nossa conhecida expressão if-else permite executar condicionalmente urn pedaço
de código. A parte do el se é opcional. Vejamos alguns exemplos:
fíf ÇfifiiDeProg'rathaJ\
to.pgr util_i_zar ..est_e, Rrpgí]ajna;!;nl;.
Neste exemplo, caso a variável fimDePrograma seja verdadeira, é escrita no ecrã uma
frase que agradece a utilização do mesmo.

No entanto, usando as chavetas {}, é possível colocar dentro de um i f um bloco de


código a executar e mesmo colocar uma secção else. A secção else é executada se a
expressão lógica for falsa. Eis um exemplo:

; Console.WríteLn,n_e(il<a> é maior que <b>") ; 'f5***"*??

' else ' -r*

console. WritieLrineC"<a> é menor ou igual a <b>") ;


s C = b; '>^
<f. _

24 ) FCA - Editora de Informática


_ ELEMENTOS BÁSICOS

Neste caso, é escrito no ecrã se a é maior ou menor do que b, ficando a variável c com o
maior dos dois valores.

Um caso particular da expressão if-else surge quando queremos testar múltiplas


expressões consecutivas exclusivas. Neste caso, se quisermos ser rigorosos com a
indentação do código, deveria surgir uma "escada", devido aos sucessivos else:
••i f Ca == 1)......'.......' ..... "- ........ " • - • ...... ............................ i

}
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)

j else // <a> não é nem l, nem 2, nem 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

2.7.2 EXPRESSÃO swrrcH


A expressão swl tch representa um i f encadeado do género que acima discutimos. Esta
expressão permite comparar o resultado de um cálculo ou de uma variável, com um
conjunto de opções enumeráveis possíveis.

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.

Vamos ver um pequeno exemplo:


swTtch " C p õ s T c ã o N a M e t a ) " —-- ---- -;

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.

2.7.3 EXPRESSÕES WHJLE E DOWHILE


Tal como foi referido antes, um ciclo whi l e corresponde a um conjunto de instruções que
são executadas repetidamente, enquanto uma certa condição for verdadeira:
int k = p;," " ~ "" """ " "~ " -' .
.' ~ -'í, íft Jp-* j, _ _L • • - .ri «--'
CkV 100)
1 ' ~
) yaloa kd^jj$. é £0}", k); v , -.-» s,

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);....._ _ ........... _______________________ _____

2.7.4 EXPRESSÃO FOR


Sempre que é necessário executar um ciclo controlado por uma variável numérica, que é
actualizada no fim de cada iteração, a expressão de ciclo mais adequada é tipicamente um
ciclo for. Para criar este ciclo, é necessário especificar três partes: uma expressão de
inicialização da variável de controlo; uma condição que se mantém enquanto o ciclo for
executado; e uma expressão de actualização da variável de controlo9. A sintaxe geral é:
for (inicialização; condíçãoDeExecução', actualização] \s um peq

;fò'f TThT T=0 ;"" "i <IO"f

A execução deste código leva a que sejam impressos os valores de O a 9 (inclusive) no


ecrã. A variável i é declarada e inicializada com O na primeira "posição" da expressão do
for. A condição a manter é especificada na segunda posição (o valor de i ser inferior a
10), e a condição de actualização, que é executada sempre no fim do ciclo, é especificada
na terceira posição. Enquanto a condição for verdadeira, o ciclo é executado.

Um ciclo f o r é aproximadamente equivalente a:


'.inicialização l
jwhile (condíçãoDeExecução)

\ '•
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

Jr______' . . ..... _.................._________....._ . _ „ . . .. . . . .___________________.... .: ..... . .„;


representa um ciclo infinito10.

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.

Um ponto importante é que as variáveis ao serem declaradas são apenas conhecidas


dentro do bloco de código onde se encontram. O que isto quer dizer é que, caso seja
declarada uma variável na expressão do for, ou dentro do corpo do ciclo, esta não será
conhecida fora deste. Vejamos o seguinte código:
,fâr""CÍ;'nít""i"?=0;"T<IOT~n++) " ................. ~ ..... ........ ----,- ---
-f - * - - - " • <
•irvt qualdradoj " " • ' ' * ; -/ - •:./•' '
quadradp ~ 1*1 ; . - , . ' •
cdfí!rôle.tifriteL-ine("o quadrado de {0} é {!}", 1, quadrado)-; .• "-.''
} • *' > . • ;
// Err°ç! -<i> e <quadrado> não são conhecidos fora dó ciclo for
!Consoíe.,Wr1teLlneCM1={Q}:, _quadradprs{l}_"_1 1,^ quadradpj)_; --_;.
Como a variável 1 e a variável quadrado são declaradas dentro do ciclo, estas não podem
ser utilizadas fora do mesmo. O mesmo acontece para qualquer outro tipo de ciclo e, na
verdade, sempre que se declara um bloco de código (isto é, código entre chavetas {}).
Sempre que existe um bloco de código, quaisquer variáveis declaradas no seu interior
apenas têm visibilidade dentro do mesmo. A este tipo de variáveis chama-se variáveis
automáticas, uma vez que deixam de existir, sempre que se sai do seu contexto.

2.7.5 EXPRESSÃO FOREACH

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

;int[]' elementos W{TO,"34;'T7rT9"T;


'foreach (int i i n elementos)
; Console.Writel_ine("{0}", n ) ;
J. ... .
A primeira linha declara uma tabela chamada el ementos, contendo os valores 10, 34, 17
e 19. As tabelas serão examinadas em detalhe na próxima secção. Para já, basta
considerar que a variável el ementos está a armazenar uma colecção de valores do tipo
inteiro.

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

conter os valores (tipo) e o nome da variável que os irá armazenar (nomeVariáveí).


estruturaDeDados representa uma variável capaz de armazenar um certo conjunto de
valores. Finalmente, os elementos presentes em estnitwaDeDados têm de poder ser
convertidos, implicitamente, no tipo da variável indicado.

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.

2.7.6 QUEBRA DE CICLOS


Por vezes, é útil conseguir sair directamente de dentro de um ciclo. Por exemplo,
imaginemos que estamos a meio de um ciclo e existe uma condição que, ao tornar-se
falsa, invalida a próxima iteração do ciclo. Ao mesmo tempo, não queremos executar o
código existente até ao final da iteração corrente, uma vez que a condição já não se
aplica. Para resolver este problema, pode ser necessário colocar um ou mais i f de
controlo:
rwhíl è "i
:{

i f (existeErro)
{
fimDociclo = true;
. prpg.rama__yal _abo.rtar_! _"
30 © FCA - Editora de Informática
ELEMENTOS BÁSICOS

// É necessário voltaria testar a condição!


if CopcaoDóutflTzador!== I ã r "&& TexisteErrõ}

_
else . . .

A palavra-chave break permite terminar imediatamente qualquer ciclo, continuando na


expressão imediatamente a seguir ao ciclo. Assim, o exemplo anterior ficaria;
; while (ífimóoCiclo) -- - - - -— - - ........... •.

Í i f (existeErro)

console.WriteLineC"p programa__vai_ abortar!_") ;


f break: II Õ break leva a que o ciclo whiíe seja terminado
r • "
i f (opcaoDoUtilizador!='a')

else

:} . . ._ __._ . _. _. „_. . . ..„


A expressão continue permite continuar directamente para a próxima iteração de um
ciclo, sem executar as instruções seguintes da iteração corrente. Por exemplo, o seguinte
código:
[int 1=0; " — -
;while (1<=100)

if (i%4 == 0) // Múltiplos de quatro não são impressos


1 continue;
! Console.WriteLine("{0}", i);
;>.. . ... .. .. „ _ . _ ___ . . . .

resulta na impressão dos números de l a 100 que não são múltiplos de 4.

É de notar que se pode usar o break e o continue, tanto nos ciclos while e do-while,
como nos ciclos for.

Embora o seu uso seja extremamente desaconselhado, o C# suporta ainda a existência de


elementos goto11. Um goto permite saltar para um local arbitrário de código, violando as
regras de programação estruturada. Um programador consciente não utiliza esta primitiva

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:

while (j <= 20)

[Co n s o l e .w r 1t e_Qn e (' '_À"p_os* os ~ ciei ÒsT1')";.


Neste exemplo, ao descobrir-se que existe um erro dentro de um ciclo encadeado, é
utilizado um goto para sair dos ciclos. O local para onde saltar é identificado por uma
palavra seguida de dois pontos.

A RETER " Para executar condi cionalmente um bloco de código, utiliza-se a


construção
i f (condição)
Controlo do fluxo
de execução
else

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;

Caso se queira que vários valores correspondam ao mesmo bloco de


código num switch, basta colocá-los seguidos.
Os ciclos whi l e e do-whi l e permitem executar repetidamente um bloco de
código, enquanto uma certa condição for verdadeira.
Os ciclos while testam a condição à entrada do ciclo e os do-while, à
saída do ciclo.
As formas dos ciclos whi l e e do-whi l e são respectivamente:
while (condição) do

> > while (condição');

32 © FCA - Editora de Informática


ELEMENTOS BÁSICOS

- Os ciclos for permitem executar um bloco de código, fazendo uma


ARETER variável tomar diversos valores em cada iteração.
A forma de um ciclo for é:
Controlo do fluxo for (inicialização\ actualização)
de execução {
> *"

~ Os ciclos foreach permitem iterar ao longo dos elementos de uma


colecção de valores.
~ A forma de um ciclo foreach é:
foreach {tipo nomeVariável i n estruturaoeDados) {

~ Para terminar imediatamente um ciclo, utiliza-se a palavra-chave break


que leva a que o mesmo seja abortado.
~ Para passar imediatamente para a iteração seguinte de um ciclo, utiliza-se
a palavra-chave confi nue.
~ A palavra-chave goto permite saltar directamente para uma certa instrução
do programa. É fortemente desaconselhado o uso de goto.

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.

2.8.1 TABELAS SIMPLES


Para criar uma tabela simples, que armazene N elementos de um certo tipo
(tipoDaTabela), utiliza-se a seguinte forma:

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^,

// Coloca em total o valor da posição 3 da tabela


double-total = al_tu_ras[3J ; ._ ._

© FCA - Editora de Informática 33


C#3.5

O primeiro elemento de cada tabela é o elemento O, sendo o último N-l, em que N é o


tamanho especificado para a tabela. As tabelas não podem mudar de tamanho12. Uma vez
declarado o seu tamanho, este fica idêntico para todo o sempre. Para se obter o tamanho
de uma tabela já críada; faz-se: nomeoaTabel a. Length.

A figura 2.1 ilustra os principais conceitos associados a uma tabela simples.

doubleG alturas = new double[N]; // Declarar e criar uma tabe'a


ai tu rãs [3] = 1.85; // Colocar um valor numa posicão

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

// Mostra o conteúdo de unma posição da tabela


Console. writeLine("o valor de altura[3] é: {0}", a1turas[3]) ;
// Mostra qual o tamanho da tabela
Console. writeLine("o tamanho da tabela é: {0}", alturas. Length) ;

Figura 2.1 — Principais conceitos associados a tabelas simples

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

;// cria efectivamente a tabela


La]turas_ = _new__dou_bl e [total pepessoas];
Tentar aceder a um elemento de uma tabela antes de esta ter sido efectivamente criada,
leva a um erro de compilação, assim como tentar aceder a um elemento que não existe:
stringlT nomesDè~Pessoás~;~ " " ""
// Erro! A tabela ainda não existe (não foi inicializada)
nomesDePessoas[2] = "Pedro Bizarro";
// Ok
_nomesDepessoas = new st_rincj[10] ;

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

: //Erro! A tabela só leva 10 elementos


;nomesDePessoas[14l _= ".3p|p_ Gabriel";_

Frequentemente, um programador deseja declarar uma tabela e inicializá-la


imediatamente. Isso pode ser conseguido de duas formas:
•"// Cria e inicialfzà uma~tabela "" " " " ..... "
string[] nomesDePessoas = {"António", "José", "Cunha"};

// Uma outra forma parecida de o fazer...


stringll nomes_Depessoas2 = new st_r_tng[] {''António", "^.oséj^ "Cunha"}j

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.

O programa da listagem 2.3 ilustra os conceitos que acabámos de discutir. Neste


programa, é lido do utilizador um conjunto de palavras, uma por linha, sendo as mesmas
armazenadas numa tabela. A tabela pode armazenar no máximo 1000 palavras. Quando o
utilizador escrever "***fím***"5 o programa termina e mostra quais as palavras únicas
que o utilizador introduziu.

/*
* programa que lê palavras do utilizador, uma por linha
* até à palavra "***fim***", e mostra quais as palavras
* únicas introduzidas.
*/
using system;
class PalavrasUnicas

static void MainQ


const string PALAVRA_FIM = "***f-j m ***";
COnst int MAXIMQ_PALAVRAS = 1000;

// Tabela que guarda as palavras únicas


stn"ng[] palavrasunicas = new string[MAXIMO_PALAVRAS] ;
// Total de palavras únicas já introduzidas
int total PalavrasUnicas = 0;

bool fim = false;


do
// Lê uma palavra do utilizador
string palavraLida = Console.ReadLineQ ;
i f ÇpalavraLida -= PALAVRA_FIM)

© FCA - Editora de Informática 35


C#3.5

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++)

i f (pai avrasuni cãs [i] — palavraLida)


encontrada = true;
}
i f ( "encontrada)
i f (total Pai avrasuni cãs — palavrasUnicas. Length)
Console.WriteLineC
"Não há espaço na tabela. Descartando palavra!");
else
pai avrasuni cãs [total Pai avrasuni cãs] = pai avraLi da;
++total Pai avrasuni cãs ;
}
}
} while(!fim) ;

// Lista as palavras únicas


Console. WriteLine("Foram introduzidas {0} palavras únicas:",
total Pai avrasuni cãs) ;
for (int i=0; i<totalPalavrasUnicas; i
Console.WriteLine("\ {0}", palavrasUnicas [i]) ;

Listagem 2.3 — Programa que lista as palavras únicas introduzidas


pelo utilizador (ExemploCap2_3.cs)

Provavelmente, o único ponto novo neste programa, relativamente aos conceitos


introduzidos anteriormente é a expressão:
Jtri.n"g7paTay>aLiciaJ=_ Console.ReadL"ineO^ l' . "._ ._/. ._!.._„ 1/1^ ... 717
Neste caso, estamos a invocar na classe Console o método ReadLineO, que retorna uma frase
(sfting) lida do utilizador. Esta funcionalidade é extremamente útil sempre que se quer ler
alguma informação através do teclado ou mesmo para fazer depuração de erros13.

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];

// copia a tabela original para o inicio da tabela destino


for (1nt_iéO; 1<original .Length; 1++)
destino[ij = original [i]j______ ............. ___

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]; ,

•Arrãy.copy (original _, _ 0 j . .destino., Q, original .Length)..• _........._____._ . _ _ _ . . ;


O primeiro argumento de copyQ é a tabela de onde se vai copiar; o segundo argumento é
a posição a partir da qual se vai copiar; o terceiro parâmetro é a tabela de destino; o
quarto parâmetro é a primeira posição para onde se vai copiar na tabela de destino, sendo
o quinto parâmetro, o número de elementos a copiar. Eis o protótipo do método:

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 .

Quando se chama um programa, colocando palavras à frente do seu nome (cadeias de


caracteres), estas palavras são passadas ao programa em execução. Para conseguir aceder
a estas, em vez de se utilizar o método MainQ "normal", utiliza-se uma variante que
possui como argumento uma tabela de strings:
static Vbid MainCstrfrfgG Tparámetros) !
r • _ '' >• ^fi -^ -, *' i' ^
L vJ > s 4

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.

© FCA - Editora de Informática 37


C#3.5

/*
* 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)

- Para criar uma tabela, utiliza-se a expressão:


ARETEK fipoDaTabela[] nomeoaTabela = new ti poDaTabel a [tamanho];
Tabelas simples ~ Para aceder a um elemento, basta indicar o seu índice entre parêntesis
rectos: nomeoaTabel a [elemento].
~ Para saber o tamanho de uma tabela, faz-se nomeoaTabel a. Length.
" Uma vez definido o tamanho de uma tabela, este será fixo.
~ Para criar uma tabela, inicializando-a automaticamente, faz-se:
tipoDarabelaG nomeoaTabela = {...}; ou
tipooaTabelaG nomeoaTabela = new tipooaTabela[] {,,.};
- Para copiar uma tabela, utiliza-se a expressão:
Array.Copy(origem, indiceOrigem, destino,
incfíceoestino, totalElemCopiar) ;
~ Caso se queiram utilizar os argumentos de linha de comandos,
utiliza-se uma versão do MainO que leva como argumento uma tabela de
strings'. MainCstringG args).

2.8.2 TABELAS MULTIDIMENSIONAIS


Um outro tipo de tabelas muito úteis são as tabelas multidimensionais. Estas tabelas são
quadros de números que podem ter duas, três ou mais dimensões. Por exemplo, a figura
2.2 apresenta uma tabela bidimensionai, de cinco linhas por quatro colunas (5x4). Tal
como no caso das tabelas simples, os índices de cada dimensão começam em O e vão até
N-l, em que N é o número de elementos em cada dimensão.

38 © FCA - Editora de Informática


ELEMENTOS BÁSICOS

índice das colunas


'ó"
12 45 23 16
9 32 87 44
índice das
81 88 90 34
linhas
31 13 48 88
2 69 18 4
Figura 2.2 - Uma tabela bidimensional de cinco linhas por quatro colunas

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)

Int valor = tabelaJ^jSj ; _ //.valor;_ ficará çom__34 ....... . . _ . . .„'_.


Novamente, é possível declarar e inicializar uma tabela simultaneamente. Por exemplo, a
tabela da figura 2.2 pode ser criada e inicializada da seguinte forma:
;int[,] tabela27 = ~ "" - - - - - - - - -~ - -- -

{12, 45, 23, 76},


{ 9, 32, 87, 44}, ;
{81, 88, 90, 34},
{37, 73, 48, 88}, :
{ 2, 69, 18, 4}

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.

2.8.3 TABELAS DENTRO DE TABELAS


Em C#, existe ainda mais um tipo de tabelas, que se chama jagged arrays ou "tabelas
dentro de tabelas". Embora esta estrutura não seja geralmente muito útil, por vezes
permite resolver certos problemas facilmente. Por exemplo, imaginemos que temos de
armazenar os salários de todas as pessoas que pertencem a um certo conjunto de
departamentos. As pessoas estão, portanto, agrupadas em departamentos, variando o
número de pessoas presente em cada departamento. Uma forma simples de resolver este
problema é criar uma tabela que irá armazenar os departamentos. Cada departamento em
si consiste numa tabela que armazena os ordenados das pessoas pertencentes ao mesmo.
A próxima figura ilustra a ideia.
D.P.*™*.
f ~i
0 1 2 3 4 5 6

salários fc-
1 | 1 \ 1 1

1 1 1 \ 1 1

g i ifi
» g 3

y l 3 § S

B 8 ê

Figura 2.3 — Exemplo de uma "tabela dentro de uma tabela"

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

4O © FCA - Editora de Informática


ELEMENTOS BÁSICOS

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];" ' ----- ---- - -... --,

salàrlos tr CO] = 220;


ifó [1] = 250;
s ala ri' os TT [2] = 235j
O truque para realmente entender o que se passa é pensar sempre o que é que é obtido
quando se acede a um certo elemento da tabela. Por exemplo, ao escrever-se
salarios[0] [2] = 235 ;, a expressão salários [0] está a obter a subtabela de índice 0.
Ao aplicar-se [2] à mesma, está a aceder-se ao elemento 2 dessa subtabela.

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:

Ranjç: __/: 2^ _ ... _ •__ .-.^ví >


pois estamos a armazenar seis elementos (seis tabelas) numa tabela bidimensional.

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.

42 © FCA - Editora de Informática


PARTE I
A LINGUAGEM C*
3« CONCEITOS DE

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.

Porquê esta mudança? As linguagens estruturadas têm um modelo de programação simples.


Tipicamente, o programador pensa numa ou mais estruturas de dados que lhe modele o
problema e, em seguida, desenvolve um conjunto de operações (funções) que actuam
sobre essas estruturas de dados (figura 3.1).

Operação F() Operação G() Operação H() Operação l()

Estruturas de Dados

i Programa - Estruturas de Dados + Algoritmos j

Figura 3.1 — Modelo da programação estruturada

Embora este modelo de desenvolvimento funcione bem para pequenos programas,


existem sérios problemas quando a dimensão dos sistemas começa a aumentar.
O problema é que uma vez que todas as operações têm acesso a todos os dados, uma
pequena modificação num dos módulos pode ter implicações em todo o programa.
À medida que os programas crescem, toma-se muito difícil manter o código. É simples de
© FCA - Editora de Informática
C#3.5

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

Objecto C i Programa = Objectos + Relações

Figura 3.2 — Modelo da programação orientada aos objectos

Uma questão bastante complexa é a forma como se consegue chegar a um conjunto de


objectos e relações que modelem o problema correctamente. O objectivo deste livro não é
ensinar ao leitor todo este processo. O tema é demasiado extenso e complexo para o
discutirmos em profundidade aqui. Caso o leitor não tenha experiência em design
orientado ao objecto, recomendamos o livro de Grady Booch - Object-Oriented Ánalysis
and Design with Applications. Não é realmente possível aprender a programar bem no
paradigma de orientação aos objectos, lendo simplesmente um livro que ensina a sintaxe
usada numa linguagem. Embora isso seja mais ou menos possível numa linguagem
estruturada, os conceitos envolvidos em orientação aos objectos são muito mais elaborados.
E usual dizer-se que para aprender OOP começa-se por ler um livro que ensine a sintaxe
de uma linguagem OOP, em seguida lê-se um livro sobre design OOP e, finalmente, só a
experiência pode ajudar o programador.

46 © FCA - Editora de Informática


CONCEITOS DE ORIENTAÇÃO AOS OBJECTOS

No entanto, antes de começarmos a discutir a sintaxe do C# em detalhe, vamos discutir


um pouco dos principais conceitos associados à OOP.

3.1 CONCEITOS BÁSICOS


A programação orientada aos objectos assenta em três conceitos básicos fundamentais:
encapsulamento de informação, composição/herança e polimorfismo. Iremos examinar
cada um deles. No entanto, caso o leitor não consiga perceber todos os conceitos, não se
preocupe. Mais à frente, iremos discutir em detalhe a sintaxe utilizada. Nesta altura, o que
importa é ficar com as noções básicas sobre estes três pilares fundamentais. Todos os
conceitos aqui abordados serão largamente examinados em capítulos posteriores.

3.2 ENCAPSULAMENTO DE INFORMAÇÃO


Tal como foi dito antes, um dos pontos fulcrais da OOP é o esconder as estruturas de
dados dentro de certas entidades (objectos), aos quais são associadas funções (métodos)
que manipulam essas estruturas de dados. As estruturas de dados não devem ser visíveis
para outros objectos, apenas a sua interface (isto é, os seus métodos ou funções).

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);
} /.;:' ' . :

Um Empregado tem internamente armazenado um nome e uma idade. E de notar que


antes da declaração das variáveis Nome e idade encontra-se a palavra-chave private. O
que isto quer dizer é que apenas esta classe pode utilizar estas variáveis. Nenhuma outra
classe pode aceder às variáveis. A informação é "escondida" dentro da sua classe.

Esta classe possui também um construtor Empregado(string nomeDaPessoa, int


idadeDaPessoa) e um método MostralnformacaoQ . Ambos são public. Sempre que
uma entidade é declarada como public, qualquer outra lhe pode aceder. Sempre que
© FCA - Editora de Informática 47
C#3.5 _

uma entidade é declarada como p ri vate, apenas os elementos pertencentes à mesma


classe lhe têm acesso.

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<

Ao executar este segmento de código, surgirá no ecrã:

É de notar que não é válido escrever expressões como:

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.

3.3 COMPOSIÇÃO ^ E HERANÇA »


Quando um programador está a desenhar uma aplicação orientada aos objectos, começa
por tentar encontrar classes. Cada classe tem uma determinada responsabilidade e
representa uma entidade concreta do mundo real. Uma classe pode ter no seu interior
objectos de outras classes ou relações para estes. Por exemplo, podemos ter na classe
Empregado um objecto do tipo casa e um objecto do tipo Telemovel (figura 3.3). Por sua
vez, cada uma destas classes irá possuir os seus dados e métodos. A este tipo de relação
chama-se composição, sendo a relação mais típica do design orientado aos objectos.

48 © FCA - Editora de Informática


CONCEITOS DE ORIENTAÇÃO AOS OBJECTOS

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.

Figura 3.3 - Relação de composição

No entanto, existe um outro tipo de relação bastante comum e muito importante, é a


relação de herança. Consideremos, ainda, o exemplo da classe Empregado. Imaginemos
agora que, numa aplicação que, utilize esta classe, surge uma nova classe que representa o
patrão da empresa. Isto é, existe uma classe Patrão. Tal como um empregado normal, o
patrão possui um nome e uma idade. No entanto, possui ainda uma característica que é ter
um certo número de acções da empresa.

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

Figura 3.4 — Relação de herança

© FCA - Editora de Informática 49


C#3.5

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.

Vejamos como é representada esta relação:


cTass Patrão : Empregado" - - - - - - - - - - .

private int NumeroAccoes; // Número de acções da empresa

public PatrãoCstrlng nomeDoPatrao, int idadeDoPatrao, int nAccoes)


: base(nomeDopatrao, idadeDoPatrao)
: { NumeroAccoes = nAccoes;
. >
// Mostra o número de acções do patrão
public void MostraAccoesQ
Console.WriteLine("o número de acções é: {O}", NumeroAccoes);

. } . . . . . . .
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).

Um ponto extremamente relevante é que um objecto patrão também é um objecto


Empregado, possuindo todos os métodos que este tem. Assim, o seguinte código é
perfeitamente válido:
Patrão donoDaÉmpresa = new Patrão C"Manuel Marques", 61, 1000000);
donoDaEmpresa.MostralnformacaoQ ;
idonoDaEmpresa. MostraAccoesQ ;

5O © FCA - Editora de Informática


CONCEITOS DE ORIENTAÇÃO AOS OBJECTOS

É 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. IO. BufferedStream System.lO.MemoryStream

System.lO.FileStream System.Net.Sockets.NetworkSíream

Figura 3.5 — Exemplo de uma hierarquia de classes

© FCA - Editora de Informática 51


C#3.5 ^^^=^=_

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.

* programa que Ilustra o conceito de polimorfismo.


*/
using System;

// classe base, comum a todos os empregados


class Empregado

private string Nome;


public Empregado(string nomeDaPessoa)

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)
{

// Nova implementação da funcionalidade "MostraFuncao"


public override void MostraFuncaoQ

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) ;

Listagem 3.1 — Programa que ilustra o conceito de polimorfismo


(ExemploCap3_l.cs)

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".

Suponhamos que temos o seguinte código:


patrão, chefe = new >atrao("MahueT"Marques") ;
chefer-MostraNomeO;
chefeVMÔXtraFUncaoO;
iEmprè"gactç emp = chefe;
emp.MostraFlincaoO; ..... „_
Qual deverá ser o resultado da execução? Obviamente, que a linha chefe.
.MostraFuncaoO ; levará a que seja escrito "Patrão", no entanto, quando se cria uma
referência Empregado emp com o valor de chefe e se chama MostraFuncaoO sobre esta,
o que acontecerá?

Em primeiro lugar, a conversão de Patrão em Empregado é possível. É sempre possível


converter (ou utilizar) uma classe base em vez da classe derivada. Isso deve-se ao facto
de uma relação de herança ser uma relação é um. A classe patrão possui todos os
elementos que a classe Empregado possui.

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.

Voltemos, agora, ao exemplo da listagem 3.1. Na classe principal do programa, é criada


uma pequena tabela com os trabalhadores da empresa (trabalhadores), onde se
encontram dois empregados normais e um patrão. Em seguida, é executado o seguinte
ciclo:
for (int i=0; i<traba1hadores.Length; i++)
trabalhado rés [i] .MostraNomeQ ;
trabalhado rés [1] .MostraFuncaoQ ;
Console.writeLineQ;
} . _. . . . . . . ....
Aqui, vemos o poder do polimorfismo em acção. Os empregados (quer sejam normais ou
patrões) são tratados de forma uniforme, sendo colocados numa tabela. No entanto, ao
chamar o método MostraFuncaoQ, no caso dos empregados "normais", é mostrado
"Empregado". No caso dos patrões, é mostrado "Patrão". O resultado da execução do
programa é o seguinte:
zé Maria ""
Empregado
António Carlos
Empregado
José António
Patrão
O polimorfismo permite que os objectos mantenham a sua identidade apesar de serem
tratados, usando classes mais genéricas (isto é, classes mais acima na hierarquia de
derivação). Isso é extremamente poderoso. Por exemplo, neste exemplo, seria trivial
estendê-lo por forma a que houvesse um método de cálculo de salário, que no caso dos
empregados teria uma certa implementação e no caso dos patrões, uma outra.

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.

54 © FCA - Editora de Informática


CONCEITOS DE ORIENTAÇÃO AOS OBJECTOS

~ A programação orientada aos objectos (OOP) baseia-se em três princípios


ARETEK básicos: encapsulamento, composição/herança e polimorfismo.
Conceitos
" Encapsulamento refere-se ao facto de as estruturas de dados serem entidades
básicos de OOP privadas de cada classe, devendo ser apenas acedidas por elementos da
própria classe.
~ Composição e herança são os tipos de relações que podem existir entre
objectos (e classes). Composição corresponde a relações do tipo contém,
herança corresponde a relações do tipo é um.
" Polimorfismo refere-se à capacidade de diferentes objectos se comportarem
de forma diferente quando o mesmo método é invocado neles. Isto, apesar
de ser utilizada uma referência para uma classe base para fazer a chamada.
" Uma classe representa uma família de objectos, enquanto um objecto
representa uma instância dessa família (ex: Empregado é uma classe
enquanto a entidade que representa "José António" é um objecto da classe
Empregado).
" Diferentes instâncias de uma classe têm variáveis diferentes. As instâncias
são criadas com o operador new.
~ Ao criar um novo objecto, o construtor da classe é chamado. O construtor
tem o mesmo nome que a classe e não tem valor de retorno.
" Se um elemento de uma classe é declarado p ri vate, então é visível apenas
para elementos dessa classe.
~ Se um elemento de uma classe é declarado public, então é visível para o
exterior da classe.
" Os dados da classe devem, regra geral, ser privados. Apenas o menor
número possível de métodos da classe deve ser público.
~ Uma relação de composição surge quando uma classe tem de conter um
objecto de uma outra classe (exemplo: "automóvel contém motor").
~ Uma relação de herança surge quando um objecto também é uma instância
de uma outra classe mais geral (exemplo: "automóvel é um veículo").
~ Uma relação de herança indica-se por:
class Classeoerivada : ClasseBase { ... }
" É sempre possível utilizar uma referência de uma classe mais acima na
hierarquia de derivação, para um objecto de uma classe derivada dessa.
~ Para existir polimorfismo, os métodos na classe base têm de ser declarados
vi rtual e nas classes derivadas têm de ser declarados override.

© FCA - Editora de Informática 55


4. 1 O SISTEMA DE TIPOS DO CLR

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:

a variável vai representa directamente o valor em memória. A figura 4.1 ilustra a


diferença. Enquanto emp é uma referência para uma zona de memória que contém o
objecto, vai é o elemento em si.

objecto do tipo Empregado

"Alexandre Marques"

variável do tipo int

vai

Figura 4.1 — Diferença entre uma referência para um


objecto e uma variável elementar

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

O ponto importante a reter, é que ao utilizar objectos não se está a manipular


directamente o objecto, mas apenas uma referência para este. Assim, é, por exemplo,
possível colocar duas referências a apontar para o mesmo objecto, permitindo ambas
manipulá-lo. Numa variável de um tipo elementar, ao utilizar a variável, está-se realmente
a manipular o seu valor.

Vejamos um exemplo concreto. Imaginemos que a classe Empregado possui um método


que permite mudar o nome do empregado. Consideremos agora o seguinte código:
[Empregado empl = new Empregadò("Alexahdre Marques");
'Empregado emp2 = empl; -
empl.MudaNome("Alexandre oliveira Marques"); :
íempl.MostraNomeQ; ;
emp_2.MostraNomeO; __ '.
Ao correr este código, irá surgir duas vezes o nome "Alexandre Oliveira Marques", uma
vez que tanto empl como emp2 se referem ao mesmo objecto, tendo, portanto, acesso aos
mesmos dados. A figura seguinte ilustra o que se passa nesta situação.

objecto do tipo Empregado

empl- "Alexandre Marques"

emp2

Figura 4.2 — Uso de referências para o mesmo objecto em memória

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");

:emp2 = empl; _._... ._ ;


No momento em que é feita a atribuição emp2=empl, o sistema detecta que já não existe
nenhuma referência para o objecto anteriormente referenciado por emp2 e liberta esse
objecto. Uma das formas de fazer garbage collection é exactamente essa: manter um
contador indicando o número de referências existentes para cada objecto e, periodicamente,
limpar os que já não são referenciados.

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

a variável emp desaparece automaticamente após a execução do método. Logo, o objecto


criado deixa de ter referências a apontar para ele, desaparecendo também.

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.)?

O C# possui um mecanismo elegante chamado boxing/unboxing que permite aos tipos


elementares serem tratados como objectos, quando necessário. De facto, no CLR existe
uma classe3 para cada tipo elementar. Sempre que é necessário utilizar um tipo elementar
como sendo um objecto, o CLR encarrega-se de encapsular o tipo elementar num objecto
que o represente. Quando esse objecto já não é necessário, o CLR retira o tipo elementar
do seu interior. A tabela 4.1 mostra essa correspondência entre tipos e classes. Em .NET
chama-se à arquitectura de tipos CTS (Common Type System}.
:j NOME EM Cif C LÃS S E/ ESTRUTURA CORRESPONDENTE
:| string System. String
;j sbyte System. SByte
1 bYte System. B v te
ij short System. Intl6
.j ushort System. Ulntl6
'1 int System. Int32
.[ uint System. UTptB 2
|| ulong System. UInt64
;[ long System. Int64
1 char__ System. Char
;| float ! j System. Si ngl e
:| double : j System. Double
;| bool j| System. Bool ean
,| decimal [ System. Decimal \a 4.1 — Os tipos ele

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

Vejamos um exemplo concreto:


int vai = 1 0 ; " " " "
objectr.. obj''= vai; .
consbT'e":w,ri'teL-ineC"p..ya]pr
Quando é criada a referência obj para o tipo elementar vai, a plataforma encarrega-se de
encapsular o inteiro dentro de um objecto do tipo system.int32. A partir desse
momento, é possível utilizar o número como se de urn objecto se tratasse. Neste caso,
podemos ver que chamamos o método Tostri ng C) na referência, resultando numa stríng
com o conteúdo "10".

É 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.

Figura 4.3 — Processo de boxing j unboxing

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:

O processo de boxing/unboxing é utilizado extensivamente na plataforma .NET. Por


exemplo, quando é feito um Console. WriteLineQ ;, o segundo parâmetro do método

© FCA - Editora de Informática 61


C#3.5

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.

ARETER ~ No processo de boxinglunboxing, os valores são sempre copiados, não


sendo possível modificar a variável original utilizando uma referência
Funcionamento resultante desse processo.
doCTS " É possível tratar qualquer tipo de dados como sendo um objecto.
" Quando é necessário considerar um tipo elementar como objecto, o CLR
encapsula o tipo num objecto de uma classe apropriada.
" System.object é a classe primordial a partir da qual todas as outras
herdam.
~ Quando são criados objectos, apenas é possível ficar com uma referência
para os mesmos.
" Pode existir mais do que uma referência a apontar para um objecto. Sempre
que se modifica o objecto a partir de uma referência, todas as outras vêem a
alteração.
" Sempre que se faz uma atribuição com referências, apenas a referência é
copiada.
• Sempre que se faz uma atribuição com tipos elementares, o valor da
variável em si é copiado.
~ A palavra-chave object representa uma referência universal, correspondendo
à classe System. ob j ect.
" Quando se faz uma atribuição de uma referência de uma classe derivada
para uma classe base, a conversão é implícita. Por exemplo:
object obj = empregado;
" Quando se faz uma atribuição de uma referência de uma classe base para
uma classe derivada, a conversão tem de se explícita. Por exemplo:
Empregado p = (Empregado) obj;
" Sempre que um objecto não é apontado por nenhuma referência, o garbage
collector elimina-o.
" É sempre possível fazer com que uma referência deixe de apontar para
qualquer objecto, colocando-a a null.

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

4.2 CAMPOS DE UMA CLASSE


4.2. l NÍVEIS DE ACESSO DE MEMBROS
Tal como foi anteriormente referido, as classes representam as entidades básicas de
programação numa linguagem orientada aos objectos. Uma classe encapsula um certo
conjunto de dados e operações associadas a esses dados. Quando se declara um campo ou
um método dentro de uma classe, este pode ter diversos níveis de protecção. Vejamos a
classe ponto:
: class Ponto
:{
public double x; :
public double y;

public Ponto(double xi, double yi) '.

; X = XÍ ; ;
y = yi ; ,

public double Distancia(Ponto p) \n Math

. }

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;

-} . ..... . . . . . _ _- __„. .. _ . <


então, as declarações seguintes seriam inválidas:
•pi.x = 120.0; " ' " "
;pl.y = 32.0; ....... . ... - .. _ . . ;
© FCA - Editora de Informática 63
C#3.5

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

protected O membro só é visível na classe onde foi declarado e em classes derivadas


desta.
0 membro só é visível dentro da classe onde está declarado. (Nem as
p ri vate
classes derivadas lhe têm acesso,,}
O membro só é visível dentro da sua unidade de compilação corrente
internai (assembly}. Funciona como um public só que apenas para o módulo
corrente.
protected 0 membro só é visível na classe onde foi declarado e em classes derivadas
internai desta, desde que dentro da mesma unidade de compilação (assembly}.

Tabela 4.2 — Modificadores de acesso

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.

Em C#, o código é compilado a nível de entidades chamadas assemblies. Um assembly


representa um módulo bem definido que contém não só o código, como meta-informação
sobre esse código (quais os tipos de dados lá contidos). Para além de código e de
metainformação, um assembly contém ainda todos os recursos necessários para executar
esse código, sejam estes imagens, tabelas de strings ou outros. O que importa recordar é

64 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

que um assembly representa um módulo de software bem identificado, sendo tipicamente


encapsulado numa DLL (Dynamic Link Llbraryf.

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.

ARETER rn - Quando se declara um membro de uma classe, é possível especificar o tipo


de visibilidade que esse membro tem fora da classe.
Níveis de acesso - Membros p ri vate são apenas visíveis dentro da própria classe.
dos membros de - Membros protected são visíveis dentro da classe e em classes derivadas.
uma classe
" Membros publ i c são completamente visíveis dentro e fora da classe.
'Membros internai são como os membros public, mas apenas visíveis
dentro do módulo (assembly') corrente.
~ Membros protected internai são como os membros protected, mas
apenas visíveis dentro do módulo (assembly} corrente.

4.2.2 NÍVEIS DE ACESSO DE TOPO


Na secção anterior, estivemos a discutir os níveis de acesso existentes para os membros
das classes. No entanto, as classes em si também têm níveis de acesso. Uma classe pode
ser declarada com um de dois níveis de acesso: internai ou public. Quando não se
especifica nada (como temos feito nos exemplos até agora), o nível de protecção é
i nternal. Isto quer dizer que a classe declarada só é visível dentro do assembly corrente.

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 í ; '* •

^public ponto(double xi, double yi) :': :


r-=^ Y.Í • - - . - . • • . - - ' • - " . • :
í^r^A J- j,.. . ; „ -.

Distancia(Ponto p)

5 É também possível criar assemblies multi-fí cheiro.


© FCA - Editora de Informática 65
C#3.5

return' Cy~p.,yO*Cy-p.~y)D ;
'T. f -is. - • . . - ' • • .

podemos utilizá-la em qualquer módulo ou programa que queiramos. Isto é ilustrado na


figura 4.4.

esc Ir:AssemblyPonto.dll Teste.cs esc /tilibrary /out:Assemb[yPonto.dlI Ponto,es

using system; using System;


ciass Teste pubtic class Ponto
public static void MainO private double x;
private double y;
Ponto pi = new PontoflO.O, 20.05; Para a classe Ponto
Ponto p2 = new PontD£l4.0. 12.0); public PontoCdouble xi, double yi)
ser visível fora do
Console.writeLineC"dCpl,p2) = ÍO}", x = xi;
pl.Distancia(pZ)j; assembly onde foi y « yi;
compilado, tem de
ser declarada como public double DistanciaCPonto p)
public
return Math.SqrtCÇx-p.x)*Cx-p.x3

Teste.exe AssemblyPonto.dll

Figura 4.4 - Utilização de classes públicas em módulos diferentes

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.

" Quando se declara um tipo de topo (uma classe ou entidade semelhante),


ARETER também se pode especificar a sua visibilidade para fora do módulo corrente
(assembly).
Níveis de acesso
de topo ~ Tipos sem nenhuma especificação ou com a especificação internai, apenas
são visíveis dentro do assembly corrente. Exemplo:
internai class Xpto { ... }
~ Tipos com a especificação p u b l i c são visíveis fora do assembly corrente.
Exemplo:
public class Xpto { ... }

66 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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 :

Cpnsole;WriteLine("o valor de Pi é: {0}", Matemática. PI) ;


} ;
É de notar que quando se declara o valor de uma constante, esta tem de ser imediatamente
inicializada. O C# possui ainda um outro tipo de constantes, que não possuem esta
restrição: os campos readonly.

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:

6 Na verdade, esta classe existe na plataforma .NET e charna-se System. Math.


© FCA - Editora de Informática 67
C#3.5

:pCi6Tfc" class impressora"


! p ri vate readonly int MAX_RES;
public lmpressora(int dpi)
MAX_RES = dpi;

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.

4.2.4 MEMBROS ESTÁTICOS


Até agora, temos visto que cada instância de cada classe possui as suas próprias variáveis.
Por exemplo, ao criar dois objectos do tipo Empregado, cada um deles representa uma
entidade individual com as suas próprias variáveis de instância:
fBnpr7êga"dõ~êm"pI~^" new" Êmprégãdò"C"pèdro' Nunes1'}; " """ i
= new Emprega_dq.C'Antónip .Bernardes_ n }i _ _ ._ i
O objecto apontado por empl possui uma referência para uma cadeia de caracteres
(string} que coutem "Pedro Nunes" e o objecto apontado por emp2 possui uma referência
para uma outra cadeia de caracteres que contém "António Bernardes". Apesar de na
classe as referências estarem declaradas com o mesmo nome (Nome):
: cláss Empregado™ ' "" """ '" " ----- ---
• private string Nome; i
; public Empregado(stn"ng nomeDaPessoa) j
: Nome = nomeDaPessoa;

68 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

public void MostraNomeQ


' Consol e.WriteLineC^O}", Nome); ;
j. _ . ._._ , _ :
num caso e noutro, Nome representa dados diferentes.

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Ç) ;

© FCA - Editora de Informática 69


C#3.5

emp2.MostraQ ;
Empregado.AlteraDi rector("3osé António");
empl.MostraQ ;
empZ.MostraQ ;

Listagem 4,1 — Exemplo que ilustra a utilização de membros estáticos (ExemploCap4_JL.cs)

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.

Normalmente, utiliza-se métodos estáticos quando se quer actuar sobre informação


respeitante a uma classe como um todo ou, quando se quer especificar uma operação que
não está associada a objectos particulares de uma classe. Math.sqrtQ constitui um
exemplo deste último caso. sqrtQ é estático porque não necessita de urna instância da
classe Math para ser utilizado. Ao mesmo tempo, pelo facto de ser estático, permite-nos
escrever expressões úteis como:
- •--•• - • - ;

© FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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;
}

Isto representa o "início do fim". Quando o programador começa a acrescentar métodos,


também os irá declarar como static. Ao definir novas classes, irá deparar-se com o
mesmo problema, acrescido da questão da acessibilidade. Tipicamente, nesta altura, o
programador passa todas as entidades a publ i c.

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:

© FCA - Editora de Informática


C#3.5

cTàss Teste ~". - - .

public static void Mal n O !


í " ' '• - Í
Teste programa = new TesteQ; í
programa.Menuprincipal Q; í

private void Menuprincipal O j


// onde é feito todo o trabalho real i
.L _ _.. ._„_ _. í
Relativamente aos membros estáticos, existe ainda uma nota final que é importante ter em
mente. Uma constante (membro declarado como const) funciona como um membro
estático. Isto é, existe apenas uma variável (constante) para todas as instâncias da classe.
No entanto, apesar de uma constante ser logicamente (e na prática) estática, é ilegal
declara-la como stati c. Isto é, o seguinte exemplo resulta num erro de compilação:
felãss "MãtérnãtTca "~ " """ • • • " • • • • - --— — - --,
statlc const double PI = 3.1415926535; // Não compila!

~ As variáveis estáticas de uma classe são partilhadas por todas as instâncias


ARETEI da classe.
Membros ~ Uma variável estática é declarada, usando a palavra-chave stati c
estáticos (Exemplo: stati c int ordenadosase;).
~ Os métodos também podem ser declarados como estáticos. Neste caso, o
método não se aplica a um objecto em particular, mas à classe como um
todo.
~ Para invocar um método estático, utiliza-se a notação:
NomeDaclasse.Metodo(...);
~ Um método estático não pode aceder a variáveis de instância de uaia classe,
apenas a variáveis estáticas.
~ As constantes (const) funcionam como membros estáticos. Isto é, apenas
existe uma variável para toda a classe.

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.

© FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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.

Quando se constrói uma classe, pode-se especificar um ou mais construtores. O único


requisito é que tenham assinaturas diferentes (isto é, a lista de parâmetros que lhe são
passados seja diferente). Por exemplo, a classe anterior pode ser declarada com dois
construtores:
: cias s Empregado .....
private stnng Nome;
private Int Idade;

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

© FCA - Editora de Informática "73


C#3.5

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.

4.3.1 INICIALJZAÇÃO POR OMISSÃO

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?

O que acontece em C# é que as variáveis de instância de uma classe são sempre


inicializadas com um valor por omissão. Esse valor é O para tipos numéricos elementares
e n u l l para referências. Os valores lógicos são inicializados a false e os char com o
carácter de código 0. As tabelas, assim como as cadeias de caracteres são também
inicializadas a null 8 .

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

Ao contrário de linguagens como o C e o C+4- em que as variáveis não inicializadas


podem conter qualquer valor, o facto de as variáveis serem automaticamente inicializadas
traz algum determinismo à resolução dos problemas existentes, devido à não inicialização
explícita de variáveis.

4.3.2 INICIADORES DE OBJECTOS


Os iniciadores de objectos permitem atribuir valores a quaisquer campos acessíveis, ou
propriedades de urn objecto, sem se ter de explicitamente invocar um construtor. Os
iniciadores de objectos consistem numa sequência de membros a inicializar, definidos
entre chavetas { }, onde cada uma das propriedades está separada por vírgulas. Para que
cada membro seja inicializado, deverá ser especificado o nome do mesmo, atribuir-Lhe o
sinal igual (=), seguido da sua expressão, objecto ou colecção. O seguinte exemplo mostra
a sua utilização, utilizando como código a classe Empregado:
class Empregado
public string Nome;
public int Idade;

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.

4.3.3 UTILIZAÇÃO DE VÁRIOS CONSTRUTORES

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.

Em C#, é possível transferir (ou utilizar) outros construtores a partir de um construtor.


0 extracto de código seguinte ilustra esta situação.
rclass Empregado "" ~"~ '
{ :
: p n vate stnng Nome; ;
1 private int Idade; -,
\c EmpregadoCstring nomeoapessoa) i

© FCA - Editora de Informática 7S


C#3.5

;" Nome =" hòmèDaPessoa;

; public Empregado(string nomeoaPessoa, int idadeDaPessoa)


l' *•£$*,:
"í" thi s (nomeDaPessoa) ^ESS^SB^^I"^"
""
i idade = IdadeDaPessoa;

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.

Repare-se que, ao fazer:


•Ãnípregado ~p2J=new Empré"g_ádoÇ"Joaq;uini ÂntõnTo", "42) ;_ " "_ ""
é invocado o construtor Empregado (string nomeoapessoa, int idadeoaPessoa). Por
sua vez, este construtor transfere o controlo para o construtor Empregado(stríng
nomeoapessoa), passando-lhe como parâmetro o nome da pessoa. Finalmente, o controlo
regressa, sendo executado o corpo do construtor, onde é feita a inicialização de idade.

4.3.4 UTILIZAÇÃO GENÉRICA DE THIS


A palavra-chave this tem uma utilização mais genérica do que simplesmente representar
chamadas a outros construtores durante a inicialização de um objecto. Durante a execução
de um método, this representa o objecto corrente, no ponto do código onde a execução
está a decorrer. Isto é especialmente útil em duas situações: durante a inicialização, em
construtores e em comparações com outros objectos.

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;

; public Empregado(string Nome)


// A variável de instância Nome é escondida, representando Nome a
i .._ _.//..yan"á,ye"l que é passada como parâmetro _

76 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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; ;

public Empregado(string Nome)


___ ___ __
thTsVNome = Nòmel......" 77 a variável dê instância Nome "toma
// o valor da variável nome passada
parâmetro^
...... " „__

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 ~~

j public bool igual(Empregado outro)

[TL jnfLXíMy^^"Qyt"cõJl" J "11ZL"~l"~Tr_"!


" " return true;
else
return Nome.Equals(outro.Nome);
'1.

9 Consultar o apêndice, sobre convenções de codificação.


© FCA - Editora de Informática 77
C#3.5

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.

4.3.5 CONSTRUTORES ESTÁTICOS


Consideremos, agora, a questão da inicialjzação dos membros estáticos de uma classe.
Como sabemos, um membro estático não está associado a nenhuma instância em particular,
representando uma variável comum a todos os objectos da classe. Assim, não faz muito
sentido fazer a inicialização dos membros estáticos dentro de um construtor normal da
classe. De facto, existe um construtor especial, chamado construtor estático, que permite
fazer este tipo de inicialização.

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

// Construtor estático da classe


statfc Emprègadò"O "
Director = "<desconhec1do>";

11 ...._.. _..„„ ....._. =


Para declarar um construtor estático, basta colocar statlc antes de um construtor com o
nome da classe e sem parâmetros.

Os construtores estáticos são chamados automaticamente pelo CLR quando a classe


correspondente é utilizada pela primeira vez. Estes construtores nunca são chamados
explicitamente pelo programador. Isto deve-se ao facto de representarem inicialização
estática dos membros das classes, devendo ocorrer antes de as classes poderem ser
utilizadas em código escrito pelo programador. Isto tem duas consequências directas:

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;

© FCA - Editora de Informática 79


C#3.5

public class ExemploCap4_2


publlc static void Mal n C)
Console.writeLine("Programa a executar!");
ClasseSimples objl = new classeSimplesQ ;
ClasseSimples obj2 = new classeSimplesQ;
ClasseSimples obj3 - new ClasseSimplesQ ;

Listagem 4.2- Exemplo que ilustra a utilização de construtores estáticos (ExemploCap4_2.cs)

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;

80 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

4.3.6 INICIALIZAÇÃO DIRECTA DE CAMPOS


Finalmente, convém referir, que é possível fazer a inicialização directa de variáveis e
constantes, sem utilizar explicitamente o construtor. Por exemplo, é possível escrever:
fcTãss"Empregado" ~ ~~ ~ ~^t J-;
1í ' -*'U'--"'-
U private string-Nome = ""; f ^T*^' / j
í? private int "Idade = 0; J \x3Tv~.
r
i •> j
** *
V* "v *J
" " - • »

í 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.

Note-se que, apesar de se utilizarem inicializações directas, os construtores existentes


continuam a ser chamados.

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.

- Utilizando a palavra-chave this, é possível transferir o fluxo de execução


AREHTKR de ura construtor para outro, antes de o corpo do construtor ser executado.
thi s é utilizado como sendo uma chamada a um método. Exemplo:
Construtores class Xpto
{
p n vate int x;
XptoO : this(-l)
>

XptoCint valor)
x = valor;
>

- Dentro de ura método, a palavra-chave this representa uma referência para


o próprio objecto.

© FCA - Editora de Informática 81


C#3.5

Os construtores estáticos são declarados como um construtor sem parâmetros,


ARETHR utilizando a palavra stati c. Exemplo:
class Xpto
Construtores
static XptoC)

Uni construtor estático é executado uma única vez, permitindo efectuar a


inicialização de uma classe como um todo.
Os construtores estáticos são executados antes da classe ser utilizada pela
primeira vez. Não é possível determinar a priori quando é que um
construtor estático é chamado.
Os campos stati c readonly representam constantes de tempo de execução,
associadas a uma classe como um todo.
Os construtores permitem inicializar o estado de cada objecto de uma
classe.
Só se pode efectuar a inicialização de um campo stati c readonly num
construtor estático.
É possível efectuar a inicialização directa das variáveis de uma classe
fazendo uma atribuição quando estas são declaradas.
E também possível fazer inicialização directa de variáveis, usando
inicializadores de objectos.
Um construtor tem de ser declarado com o nome da classe e sem valor de
retorno.
Uma classe pode ter mais do que um construtor, tendo estes de diferir nos
parâmetros que possuem.
Se uma classe for declarada sem nenhum construtor, então, irá possuir um
"construtor por omissão", sem parâmetros.
Caso sejam declarados um ou mais construtores, o compilador já não emite
o código correspondente ao construtor por omissão. Se se tentar utilizá-lo,
isso resulta num erro de compilação.
Todas as variáveis de uma classe são inicializadas com valores por omissão.
O para tipos numéricos e caracteres, false para valores lógicos e null para
referências.

4.4 MÉTODOS SIMPLES


Sempre que se declara um método em C#, é necessário indicar o seu valor de retorno.
Este valor de retomo pode ser de qualquer tipo de dados ou, caso o método não retorne
nenhum valor, void. Dentro do método, para terminar a sua execução, resultando num
valor, utiliza-se a palavra-chave return seguida do valor a retornar. É de notar, que a
utilização do return leva à terminação imediata do método.

82 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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).

Um método declarado sem o modificador static representa um método de instância. Ou


seja, é sempre chamado, utilizando um objecto concreto da classe em questão.

Nesta secção, iremos examinar em pormenor as questões associadas à declaração e


utilização de métodos.

4.4.1 VISIBILIDADE DE VARIÁVEIS


Como já foi referido, sempre que se declara uma variável dentro de um método, a
variável é criada numa zona de memória chamada stack. Quando o método termina, a
variável deixa automaticamente de existir.

É 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; . --

O mesmo se aplica a variáveis passadas como parâmetro de entrada. Essas variáveis


actuam como variáveis locais, sendo as variáveis de instância com o mesmo nome,
escondidas. No entanto, não se pode declarar variáveis locais com o mesmo nome dos
parâmetros de entrada de um método.

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

© FCA - Editora de Informática 83


C#3.5

acontece, os métodos são distinguidos pela sua assinatura, isto é, pelos tipos dos parâmetros
que lhes são passados.

Por exemplo, consideremos os métodos MaxQ, da classe Matemática, que calculam o


máximo dos valores que lhes são passados:
ipublic" class"Matemática " " "~" ' '" " "" "~
il !
' // Calcula o máximo entre dois valores ;
public static int Max(int a, int b)
if (a > b) \n a;
e! se i
return b;

// Calcula o valor máximo de uma tabela de valores '•


public static int Max(int[] valores) ;
int max = valores[0];
foreach (int vai i n valores) '••
{
; i f (vai > max)
; max = vai; \n max;

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.

Para utilizar estes métodos, basta chamá-los com os parâmetros correspondentes:


:iht mãxl~=~ Matemática.MaxO-O, 20); " "// Resultarem" maxl=2'0" .
;int[] tabela = { 21, 10, 32, 12 };
int max2 =.Matemática. Max (tabela).; ._ _//jie.su] ta_ em. max2=32
E de notar, que neste caso, os métodos Max C) são estáticos, sendo utilizados através da
classe Matemati ca. No entanto, o mesmo princípio aplica-se a métodos de instância.

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

pubTTc cias s" Pbhtb "


private double x;
private double y;
public Ponto(double x, double y)
this.x = x;
this.y = y;

// calcula a distância a outro ponto passado como parâmetro


public double Distancia(ponto p)
return Math.Sqrt((x-p.x)*(x~p.x) + (y-p.y)*(y~p.y));

// Calcula a distância às coordenadas de outro ponto


public double Distancia(int x, int y)
return Distancia(new ponto(x, y)); i
} í

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

Idouble dl = a.Distancia(b); // Distância de a a b l


; double d2 = a.Distancia(2, 5); // Distância de a a (2,5) j
:console.WriteLine("{0}", dl); // Escreve os resultados j

Como se pode ver, é possível a coexistência de ambos os métodos, lado a lado, sendo
distinguidos pelos parâmetros de entrada.

Na verdade, até é possível coexistirem métodos estáticos e métodos de instância com o


mesmo nome. Por exemplo, nesta classe, pode-se declarar um método estático
Distancia O que utiliza como parâmetros dois pontos:
ipu6Yi"c"cTass "Tonto" ~~~ ~™ ..... "~" " i

// Calcula estaticamente a distância entre dois pontos


public static double oistancia(Ponto pi, ponto p2)
return pl.Distancia(p2) ;
l_________.....___________....._ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ __________......______________;Í
}

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

© FCA - Editora de Informática 85


C#3.5 _

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.

4.4.3 PASSAGEM DE PARÂMETROS


Em situações vulgares, as variáveis são sempre passadas por cópia12 para dentro dos
métodos. Isto quer dizer que ao chamar um método, o valor da variável de entrada de ura
método é copiado, estando o método sempre a modificar uma cópia e não a variável
original. Mesmo quando um método possui como parâmetro de entrada uma referência
para um objecto, o que é passado na invocação do método é urna cópia da referência, não
a referência original.

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 " " ..... " " ~ ...... "

// calcula a distância do ponto à origem


' public void DistanciaOrigemCdouble resultado)
resultado = Math.sqrt(x*x + y*y) ;

•y.,. . . ......... . ....... ..


utilizando-o da seguinte forma:
Ponto ""p " ....... = new "PòTitóClO.O, O/o);
double resultado = 0.0;
p,DistanciaOric|em(resultado) ;
Console. writeLineCÍQ}", resultado^) ; _
A ideia seria, que no final da execução, o resultado ficasse armazenado na variável
resultado. Embora aos programadores de Java isto possa parecer estranho, em várias

12 Também chamada passagem por valor.


86 © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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.

4.4.3.1 PASSAGEM POR REFERÊNCIA (REF)


É possível, embora na maior parte das vezes não seja necessário, passar uma variável por
referência para dentro de um método. Para isso, basta marcar a variável e o parâmetro
correspondente com a palavra-chave ref. Considerando o exemplo anterior, isto seria
feito da seguinte forma: na classe Ponto, o parâmetro resultado passava a ser declarado
como ref double resultado:
public class "Ponto

| public ',yoid "PlstãncfaprigemCref .~dou'bje~


rê&tfitado - Math.sqrt(x*x + y*y) ;
• } - '•->•: - -
Quando se invoca o método, também é necessário marcar a variável passada com a
palavra-chave ref:
.doubJJe rasultãdo ="0.0";
p-. piftanGÍaorigem(refi resultado) ; _ _ ...... _. ..... _
Assim, ao executar este código, a variável resultado fica com o valor 10, tal como seria
de esperar.

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

i public void colocasimétrico(Ponto outro) ," -, ,:


omtrtj#x - -x; ~ ,
ou€ro^y = -y; -

publnc void MostraO '


: _ Console.write_LineC"_C{QI,_{l}3"^x,_ y);_ ._ .. _ _ . ;

© FCA - Editora de Informática 87


C#3.5

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.

4.4.3.2 VARIÁVEIS DE SAÍDA (our)


Uma das principais utilidades de utilizar passagem por referência é retomar vários valores
de um método. Como já vimos, cada método apenas pode ter um valor de retorno. No
entanto, existem situações em que é necessário (ou conveniente) retornar vários valores.
Por exemplo, suponhamos que temos um método chamado Encontram" nMaxQ na classe
Matemati ca. Este método é suposto encontrar o valor mínimo e o valor máximo de uma
tabela, passada como parâmetro. Uma forma simples de resolver o problema de retornar
ambos os valores, é considerar que existem duas variáveis (mi n e max), que são passadas
por referência e que, no final da execução do método, terão os valores pretendidos:
jpuBTi c" cTass MatematTcã ' ~ " "" "" ~ ~~ ~ " " '" }
"~pubTT^stãtTc^òTd~Êncõ^
ref int min,
ref int max)
"c ""
min = vai o rés [0] ;
~~ " """" " "
max = vai ores[0];
foreach (int vai in valores)
i f (vai < min)
mi n = vai;
if (vai > max)
max = vai;
}

Este método, assim declarado, resolve o problema do retorno de várias variáveis. No


entanto, se ao utilizar este método o programador escrever:
:int[]""vãTdres''="T'^13T3Zri7T"4'5"r7"67" 5647"Z7 ITT; """" " j
'int min; >
:int max; j
i i
jMatematica.EncontraMinMax(valores, ref min, ref max); l

88 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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.

No exemplo acima, o código do método tomaria a seguinte forma:


ipuBlicTíflasâ MatématTca"
'
___„_ ....... ___ . puJL J.Qt .!BÍILi._oUt _lnt,jn_a_x)
| min - vai o rés [0] ;
max « vai o rés [0] ;
t A •>
l foreach (int vai i n valores)
l * -i f_
v (vai < mi n)
mi n = vai ;
i %3f<CVâl > max)
l max1 = vai ;
t j "

E a sua utilização também necessitaria de utilizar a palavra-chave out:


,-írrfc mi-n , l ..... ~ ~~ :

>.-- -.

// BrjAnghaXos valores mínimo e máximo na tabela valores.


EncontraMinMaxCvalores. out m i n , out max) ; __

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 .

© FCA - Editora de Informática


C#3.5

" Um método é declarado com um tipo de acesso (p ri vate, protected,


ARETER public, etc.), um valor de retorno (ou void, caso não retorne nada) e um
conjunto de parâmetros.
Métodos Simples
" Métodos declarados com a palavra-chave static são métodos estáticos, que
se aplicam a uma classe como um todo. Estes métodos não têm acesso às
variáveis de instância.
~ Os métodos estáticos não necessitam de um objecto da classe para serem
chamados. Basta utilizar o nome da classe (exemplo: Nomeoaclasse.
.MetodoQ;).
~ Podem ser declarados vários métodos com o mesmo nome, desde que
difiram nos parâmetros que constituem a sua assinatura. A este processo
dá-se o nome de overloadmg.
~ As variáveis passadas como parâmetros escondem as variáveis de instância
com o mesmo nome. Sempre que é necessário distinguir entre ambas,
utiliza-se a palavra-chave this. Ao escrever this.NomeDavanavel, é
acedida a variável de instância.
~ Normalmente, as variáveis são passadas por cópia para dentro dos métodos.
Caso se utilizem os modificadores ref ou out, as variáveis são passadas por
referência, sendo possível modificar os seus valores originais.
" O modificador ref permite passar a um método, uma referência para uma
certa variável que, tanto pode ser utilizada como variável de entrada, como
de saída.
" O modificador out marca um parâmetro como variável de saída, pertencendo
a variável ao código que chamou o método. Essa variável não tem de estar
inicializada.

4.5 REDERNIÇÃO DE MÉTODOS


Como foi referido no capítulo 3, a herança é um dos pilares nos quais a programação
orientada aos objectos se apoia. Desenhar classes que são derivadas de outras constitui
um processo de refinamento em que se está a pensar numa futura reutilização de código e
na expansibilidade do sistema como um todo. A possibilidade de escrever classes
derivadas de classes já existentes permite ao programador aproveitar uma grande parte do
trabalho já realizado por outras pessoas. Por exemplo, um programador que esteja a
desenvolver uma interface gráfica pode perfeitamente criar uma nova classe, derivada de
uma classe de sistema que represente uma janela vazia. O programador pode, então,
acrescentar os elementos que lhe faltam, obtendo a interface pretendida.

Ao contrário do que acontece em outras linguagens, em C# apenas existe herança


simples. Isto é, quando é especificada uma classe, esta apenas pode derivar de uma única
classe. Embora isto possa parecer limitativo, a história mostra que o uso de herança
múltipla sempre trouxe mais problemas do que os que resolveu. Ao mesmo tempo, a
utilização do mecanismo de interfaces, que iremos examinar brevemente, permite obter a

9O © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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

A classe patrão possui implicitamente todos os campos e métodos existentes na classe


Empregado, para além dos que são definidos em patrão.

Quando se fala de herança, o ponto central associado à mesma é a questão da definição e


utilização de métodos. Vejamos com mais atenção a classe Empregado:
clãss Empregado
private stnng Nome;
private int Idade;
i // Construtor: inicializa os elementos internos de um objecto
public Empregado(string nomeDaPessoa, int idadeoaPessoa) :
• {
'• Nome = nomeoapessoa;
Idade = idadeDaPessoa;
! > ;
// Mostra a informação sobre a pessoa
l public void MostrainformacaoO
; console. Writel_ine("{0} tem {1} anos", Nome, Idade);
1 } :
Imaginemos, agora, que queremos redefinir o método MostrainformacaoQ na classe
patrão , para mostrar informação, não só referente à parte Empregado, mas também para
incluir informação relativa a Patrão em si. A este processo chama-se overriding de
métodos. Uma forma de conseguir isto é com a seguinte implementação:
leias s Patrão f Empregado" ""
:{_ _ __ _.......____________ _ ..... _„.....___________ ______________________ ....... ......________
© FCA - Editora de Informática 91
C#3.5

//" NumércTãcçõés do patrão


public Patrao(string nomeDoPatrao, int idadeDoPatrao, int nAccoes)
: baseCnomeooPatrao, idadeooPatrao)
NumeroAccoes = nAccoes;

// Mostra a informação sobre o patrão


public new void MostralnformacaoQ
í
// Mostra informação sobre a pessoa j
base.MostralnformacaoQ;
// Mostra informação especifica do patrão l
Console.Writel_ineÇ"o número de acções é: {0}", NumeroAccoes);
il_
f >

_.. 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.

O segundo ponto importante é que MostrainformacaoQ é declarado usando a


palavra-chave new:
fpuBl "fclíevFvõi d"Mo"strãín"fõ?mã"cãòT)" " ......

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.

Até aqui tudo parece bem. Se fizermos:


'p^1fr!i^irMã7íuêinfiãT^

surge:

92 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADAAOS OBJECTOS

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

Ao executar este segmento de código veremos:

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.

Em C#, quando não se coloca o modificador virtual na declaração de um método,


qualquer override desse método leva a que o método chamado seja o correspondente ao
tipo da referência utilizado. Isto é, não existe polimorfismo. Os métodos e as referências
para as classes valem por si mesmo.

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

Vamos, então, rever o exemplo anterior. Na classe Empregado, MostralnformacaoQ


deveria ter sido declarado como virtual:
c~vi rtuáT~võTcr MostraInfõ"rmacãõ~O " ------- -">
'-T ' ' -"' * '
i ;- ? —'• < • • • ,j- -.•"•'••' ' í
J_J" j '^ ___ :_ ._ _______ \, porque esta classe

derivadas. Na classe base, MostrainformacaoO deve mostrar informação sobre o


empregado, mas nas classes derivadas deve mostrar informação mais pormenorizada
sobre o objecto em questão.

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~-

© FCA - Editora de Informática 93


C#3.5

Os métodos virtuais implicam uma pequena penalização em termos de perfonnance, uma


vez que o CLR tem de determinar, em tempo de execução, qual o verdadeiro tipo do
objecto que está a ser chamado. No entanto, os métodos virtuais (isto é, quando se utiliza
polimorfismo) constituem um mecanismo muito poderoso de expansibilidade de classes.

4.5.3 GESTÃO DE VERSÕES


Por vezes, ao utilizar classes base e classes derivadas, surgem diversos problemas de
integração. Estes problemas são tanto mais frequentes quantos mais programadores utilizem
um certo conjunto de classes. O C# dispõe de alguns mecanismos que permitem evitar ou,
pelo menos, alertar o programador para este tipo de problemas.

Para melhor compreender esta questão, consideremos o seguinte exemplo. Existe um


programador que escreve uma classe A. Entretanto, existe um outro programador que
utiliza essa classe, criando uma classe derivada B. A classe B possui um método não
presente em A - o método F Q:
jcTáss~Ã~ " "'// Escrita "pelo primei ró programaciòr " "" :

i> '' . " -. '' ;


idass B : A // Escrita pelo, segundo programador
; public void F()

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

94 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS O B j ECTO s

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.

Resumindo: como fornia de protecção contra a introdução de novos métodos com a


mesma assinatura dos que estão presentes em classes derivadas, o compilador garante que
as invocações são estaticamente definidas. Caso existam métodos em classes derivadas
com a mesma assinatura dos métodos presentes em classes base, o compilador requer que
se indique explicitam ente, nas classes derivadas, se se trata de uma nova implementação
(new), que deve esconder a implementação antiga, ou de um ovem" de de um método
virtual. Quando se trata de um método declarado como virtual numa classe base, ao
colocar esse método como new na classe derivada, está a dar-se uma nova definição desse
método.

Esta última parte é algo complicada, mas bastante importante. Consideremos as seguintes
classes:
'class A " "~ " "" " " " :

, public virtual void F()


Console.WriteLine("A.FO");
}

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.

© FCA - Editora de Informática 9S


C#3.5

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

96 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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 }

jclass Derivada : Base


public override sealed void F() // F não pode mais ser modificado

Note-se que não faz sentido, colocar directamente um método como selado. Isto é, o
equivalente a:
jcTãss Teste ~ • --— -- - !

Kí public sealed void F() // Erro de compilação


!
;

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.

4.5.5 CLJVSSES ABSTRACTAS

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.

© FCA - Editora de Informática 97


C#3.5 _

Vejamos um exemplo em particular. Consideremos, ainda, o exemplo das classes


Empregado-patrao, mas com uma pequena variação. À partida, a cJasse Empregado
regista informações sobre um certo empregado. No entanto, os "empregados reais" da
empresa corresponderão sempre a objectos de classes derivadas de Empregado. Por
exemplo, irão existir classes como secretaria, operário, patrão e outras. A classe
Empregado representa apenas o máximo denominador comum entre elas.

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;

public EmpregadoCstrlng nomeDaPessoa, Int idadeoapessoa)

this.Nome - nomeDaPessoa;
this. Idade = idadeDaPessoa;

public abstract decimal CalculaOrdenadoQ ;

Nesta declaração, há dois pontos importantes. Primeiro, a classe em si tem de ser


declarada como abstracta, uma vez que contém métodos abstractos:
[abstract
' " c l.......
a s s Empregado
"~~ ......... " ~ "" ....... ]

O segundo ponto refere-se à declaração do método cal cul aordenado em si:


•pubVi c abstract " "decímãl._Calcula.ofdehado.Q; .. ..'... l . .
cal cul aordenado () é um método que retorna um decimal e não possui parâmetros de
entrada. Ao ser declarado como abstract, não é especificada a sua implementação,
sendo esse trabalho deixado a cargo de quem implementa classes derivadas de Empregado.

É de notar que, em conjunto com métodos abstractos, podem perfeitamente coexistir


métodos normais, não abstractos.

Nas classes derivadas de Empregado, é, então, especificada a forma como é calculado o


ordenado:
i cias s" 'operário : Empregado"" " " " ~"
; private decimal ordenadoMinimo;

: public OperarioCstring nomeDaPessoa, int idadeoaPessoa,


'_„ . _ decimal prdenadoMinimpJ)

98 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

~ : 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.

Relativamente à classe Patrão, um patrão recebe um valor correspondente a 1% das


vendas correntes da empresa:
: dáss patrão : "Empregado" "' "" " " " " """
; public PatraoCstring nomeDaPessoa, int idadeoapessoa)
: base(nomeDaPessoa, idadeoaPessoa)
; í

public override decimal CalculaOrdenadoQ


return 0.01*Empresa.VendascorrentesQ; '
}

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ã; ;

Ldecimal salário = _emp._calculaOrden.adoOj _ // çorrectg_!_


Finalmente, importa ainda referir, que é possível declarar uma classe como sendo
abstracta, sem que esta possua algum método abstracto. Nesse caso, é simplesmente
impossível criar instâncias dessa classe, sendo apenas possível, criar instâncias de classes
derivadas.

© FCA - Editora de Informática 99


C#3.5

4.5.6 MODIFICADORES DE MÉTODOS


Vamos, então, resumir os modificadores que se podem aplicar a métodos. A tabela 4.4
mostra toda a informação anteriormente discutida. De todos estes modificadores, o único
que ainda não foi examinado foi o modificador extern.
| MODIFICADOR SIGNIFICADO
public, ;
protected, í
protected internai, • Definem a visibilidade do método.
internai,
p ri y ate
0 método é aplicável a uma classe como um todo e não a uma
stati c
instância da mesma (método estático).
abstract 0 método é definido numa classe derivada (método abstracto).
0 método poderá ser redefinido numa classe derivada, sendo o
vi rtual objecto correcto determinado em tempo de execução (método
virtual).
0 método está a alterar a definição de um método virtual de uma
override
classe base (método virtual).

new 0 método constitui uma nova implementação de um método presente


numa classe base.
0 método constitui uma nova implementação de um método presente
sealed override numa classe base e não poderá voltar a ser modificado (overrided)
._ _ „ .. ... . . . (método selado).
extern O método encontra-se definido num outro local. i

Tabela 4.4 — Modificadores aplicáveis a métodos

4.5.6. t MODIFICADOR EXTERN


0 modificador extern permite declarar a existência de um método, estando a sua
implementação definida num outro local. Assim, por exemplo, ao fazer:
[cTass Tes"fê ~"~ ~~ —— —• -
1 public extern static void MetodoExternoQ ;
!!__ _
está-se a indicar ao compilador que existe um método, MetodoExternoQ, com a
assinatura dada, que não se encontra implementado nesta declaração. No entanto,
desejamos fazer utilização do mesmo.

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.

Neste exemplo em particular, se MetodoExternoQ se encontrasse no ficheiro


"ModuloAuxiliar.dll", isso poderia ser especificado da seguinte forma:
i"niport System.Runtfme.íhteropServTcès'; ~~ " - - •- ,

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;

public CD(string audio, string video)


this.FaixaAudio = audio; :
this.FaixaVideo = video;
.1" :
© FCA - Editora de Informática 1 Ol
C#3.5

public string AudioQ


return FaixaAudio;

public string vi deo C)


return 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)

i console.writeLine("PC CD Power Player");


j console.writeLine("Audio: {0}", cdATocar.Audio());
: if (cdATocar.video() != null)
; Console.WriteLine("video: {0}", cdATocar.video()); ;

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 - .. . .

public void TocaCD(CD cdATocar)


console.WriteLine("Aparelhagem a tocar: {0}", cdATocar.Audi o());

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>");

O nome das interfaces deve começar pela letra I, maiúscula,


l O2 © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

:Còmputador pç = "hew "Cbmp"utãdor"O ;


:Apare1 hagem stereo = new ApareihagemQ ;

!ii_eitorCD leitorAlvo = pç;


'leitorAlvo.TocaCD(top20);

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

class Aparelhagem : iLeitorCassetes, IRadio ;

} "" .. . . . _ . _________... ._......_ . ________ ......... _....... ....... .... „ . „


O ponto a reter é que as interfaces representam funcionalidades que uma classe suporta.
Isto é, um certo conjunto de métodos que essa classe deve implementar. Outra questão
importante é que é possível declarar referências para esses objectos, utilizando como tipo
a interface em si, tal como fizemos em:
:lLèitòrCD leitorÃlyp~j= Jpcj__.~' ; ;_" ./_" '_'__". ..... "_. .""'V "."_""__'" .'I". "." ". . ". ' •
Na listagem 4.3, é apresentado o código completo do exemplo discutido.

/*
* Exemplo que Ilustra a utilização de Interfaces
*/
using system;
// classe que representa um CD
ri^ c c CD
class rn

private string FaixaAudio;


private string Faixavideo;
public coCstring faixaAudio, string faixavideo)

this.FaixaAudio = faixaAudio;
this.Faixavideo = faixavideo;
\c strlng AudloQ

© FCA - Editora de Informática 1 O3


C#3.5

return FaixaAUdio;

public string vi deo Q


retu rn Fai xaVi deo ;

// interface que permite tocar um CD


interface ILeitorCD
{
void Tocaco(CD cdATocar) ;

// Um computador é também um leitor de CD


class Computador : ILeitorCD

public void TocaCD(CD cdATocar)


Console. WriteLine("PC CD Power Player");
Console. writeLine("Áudio: {0}", cdATocar. AudioQ) ;
i f (cdATocar. Vi deo Q != null)
Console. WriteLine("video: {0}", cdATocar. videoQ) ;

// Uma aparelhagem também é um leitor de CD


class Aparelhagem : ILeitorCD
public void TocaCD(CD cdATocar)
Console. WriteLine("Aparelhagem a tocar: {0}",
cdATocar. AudioQ) ;
}

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) ____ _ __ _ __ __

1 O4 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

leitorAlvo.TocaCD(top20);
else
Console.Writel_1ne("D-isposifivo desconhecido");

Listagem 4.3 — Exemplo que ilustra a utilização de interfaces (ExemploCap4_3.cs)

4.5.7.1 HERANÇA DE INTKRFACES


Tendo em conta o que já dissemos sobre interfaces, é natural pensar nestas como um
género de herança. De facto, declarar uma interface é quase como estar a declarar uma
classe puramente abstracta, isto é, que apenas contém métodos abstractos15. No entanto,
ao contrário do que acontece numa classe abstracta, numa interface não é possível
declarar quaisquer tipos de campos no seu interior e é possível às classes herdarem de
mais do que uma interface.

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.

• Quando se declara um método numa classe derivada com a mesma assinatura


ARETER de um método de uma classe base, estamos na presença de overriding.
Herança e ~ Caso o método da classe base não tenha sido declarado com a palavra-chave
polimorfismo vi rtuaT, as chamadas são directas ao objecto associado à referência
utilizada.
" Sempre que se utiliza vi rtual, isto é, métodos virtuais, o CLR descobre qual
é a classe mais derivada que a referência ern questão suporta (isto é, o
verdadeiro tipo do objecto) e, só então, faz a chamada ao método.
~ Quando se está numa classe derivada, a palavra-chave base refere-se aos
elementos da classe base.

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

" Sempre que se faz um "overridé verdadeiro", envolvendo polimorfismo,


ARETHR numa classe derivada, o método em questão tem de ser marcado com. a
palavra-chave override.
Herança e
polimorfismo " Caso se esteja a criar uma nova implementação de um método numa classe
derivada, que já existe numa classe base, mas não relacionado (um "falso
overridé"}, é necessário declarar o método com a palavra-chave "new".
" A palavra-chave sealed declara uma classe selada (exemplo:
sealed class Empregado {...}), implicando que não é possível criar
classes derivadas da mesma.
" A palavra-chave abstract permite declarar classes e métodos abstractos.
Isto é, métodos que na classe em causa ainda não têm implementação, sendo
esta feita numa classe derivada.
" Uma interface representa um conjunto de métodos que têm de ser
implementados por uma classe. Por exemplo:
Interface ILeitorCD { void tocaCDQ; }
~ Uma classe que implemente uma (ou mais) interface(s) utiliza a notação de
herança para fazer o overridé dos métodos da(s) interface(s).
~ É possível utilizar uma referência do tipo da interface para um dado objecto.
No entanto, não é possível instanciar um objecto usando a interface em si.
~ Uma interface pode herdar de uma, ou mais, outras interfaces. Nesse caso, a
classe que implementar a interface terá de providenciar a implementação de
todos os métodos associados às várias interfaces em questão.

4.6 CONVERSÃO ENTRE TIPOS


Agora que já discutimos os aspectos mais importantes relativos à programação orientada
aos objectos (isto é, classes e interfaces), vamos ver algumas questões importantes sobre
conversões entre tipos.

Como vimos anteriormente, se tivermos um objecto de um certo tipo, é sempre possível


convertê-lo directamente num objecto de um tipo que seja mais geral. Por exemplo, se
tivermos um objecto do tipo patrão que é derivado de uma classe Empregado, é possível
converter directamente o objecto patrão num objecto do tipo Empregado:
Patrão bfgBoss = new" Pâtfào(MMà~nueT~Mãrques"7 61) ;" " " " "" '
^Empregado emp = btgBoss;
De facto, isto até acontece implicitamente, nomeadamente quando existem chamadas de
métodos que utilizam parâmetros mais genéricos.

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_~". """..."..

1O6 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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- .

© FCA - Editora de Informática l O7


C#3.5

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;

4.6.3 OPERADOR TYPEOF


Embora neste momento não fosse absolutamente necessário falar do operador typeof,
este operador, juntamente com os operadores Is e as formam o núcleo da chamada
Runtime Type Identification (identificação de tipos em tempo de execução) e do tópico de
reflexão. Neste livro, não iremos cobrir, de forma profunda, este tema, no entanto, não
podíamos deixar de o mencionar. Reflexão consiste em descobrir, em tempo de execução,
quais os tipos presentes no sistema e suas associações, assim como permitir a
manipulação desses tipos.

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.

Ao executar o seguinte código:


^éthodlnfõIJ^metbabsDaStrTng
jforeach ^Meth_odlnfq methpd_ i

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.

Os mecanismos de reflexão são bastante poderosos e permitem muita flexibilidade. Por


exemplo, é possível chamar métodos "por nome" em objectos dos quais apenas é
conhecida uma referência ou, descobrir todo o conjunto de dependências entre classes e
objectos. No entanto, o tópico, como um todo, excede em muito o âmbito do livro. O
leitor interessado deverá consultar a documentação presente no MSDN.

ARETER ~ As conversões entre referências para classes acima (mais gerais) na


hierarquia de derivação são sempre implícitas.

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)

~ O operador as permite converter uma referência para um objecto numa


referência diferente, caso essa conversão seja possível. Caso não seja, a
referência alvo fica com o valor nul l . Por exemplo:
Patrão pat = emp as Patrão;
~ O operador typeof permite identificar o tipo de um certo objecto. Por
exemplo:
Type -info = typeof (string) ;

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".

Para responder a este tipo de necessidades, em C# existe o conceito de estrutura. Uma


estrutura é simplesmente um tipo de dados composto (por exemplo, dois inteiros). Para
definir a estrutura ponto faz-se:
fstrúct: Ponto""' ' — • - - . - - -- -- - -,
K !
í public int x; i
l public int y; •

Uma estrutura utiliza-se exactamente como uma classe normal. Isto é, para criar um
ponto, basta fazer:

Podendo-se aceder aos seus elementos:


|p"."}T=TQ";
iP..y_=__20j
Tal como nas classes, as estruturas podem possuir construtores e também podem ter
métodos que manipulam os elementos da estrutura. A principal diferença reside no facto
de as estruturas existirem no stack. Isso faz com que copiar uma estrutura ou construir a
mesma seja muito mais eficiente do que nas suas versões baseadas em classes.

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.

11O © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

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;

> ' ' " < .

ponto pi = new ponto O'; ' // Válido (x e y com valor oyt


203j / / _ p k j __como sena d
Como dissemos anteriormente, não é possível modificar o construtor por omissão.
Quando é reservado espaço no stack para uma estrutura, os valores presentes para a
estrutura são sempre os de omissão. Caso se tente "enganar" o compilador, especificando
uma inicialização directa de campos, isso resulta num erro de compilação:
strucfr Ponto • ..... ~~ ..... ..... """" .""•'.- • : . • ."". " ;
pubvMc Int x = 1; // Erro de comPÍ.lâSâàiiÉ^ÉSivL ííf " ''

}_ ._________1.._________.______"r.'I_. ".'..'.Li!'*__________....... ..... ->: -'.-.i. :


As estruturas têm de começar sempre com um estado limpo, sendo os seus campos
modificáveis apenas após estas estarem propriamente criadas.

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.

Finalmente, para os programadores de C++, é de notar que em C# os campos de uma


estrutura são privados por omissão, tal como nas classes. Isso não acontece em C++.

© FCA - Editora de Informática l l1


C#3.5

~ Uma estrutura representa um pequeno agrupamento de dados relacionados.


" A sintaxe utilizada é semelhante à das classes, mas utilizando a palavra-
Estruturas -chave struct
" As estruturas são tipos de dados de valor, residindo no stack.
~ Não existe herança quando se utilizam estruturas, a não ser dos métodos
pertencentes asystem.object.
" As estruturas têm sempre um construtor sem parâmetros por omissão, não
sendo possível modificá-lo.

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; _

112. © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

case- Estadoci vil .VIUVO:


è^rTso
b&eak;
j *"
Embora, internamente, uma variável do tipo enumeração seja um inteiro, não é possível
fazer conversões implícitas entre inteiros e enumerações. Por exemplo, o seguinte código
é inválido:
=i= -Éstadocfvi V. SOLTEIRO;"" / / E r r o "de" compilação!
ci Vil, estado. = 1; ....... _ ...... // Erro dA Compilação í

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.

Por omissão, o tipo de dados internamente utilizado numa enumeração é um inteiro de 32


bits (i nt). No entanto, é possível utilizar outros tipos de dados. Para isso, basta indicar o
tipo correspondente utilizando dois pontos. Por exemplo, em:
pjjblic enum Permissões": byte " .
T ví M r- '
' = 0x00,
= Ox013 .
= 0x02,
Apagar = 0x04
}J . ......... - ^ .......... . - - - -
uma permissão ocupa apenas um byte, O tipo subjacente tem de ser numérico (byte,
sbyte, short, ushort, int, uint, long ou ulong).

As enumerações são muito úteis, facilitando a leitura do código e, ao mesmo tempo,


garantindo que as conversões que são feitas entre tipos são seguras e possuem valores
válidos. Estas duas razões são, possivelmente, as mais importantes para se utilizar este
tipo de funcionalidade.

© FCA - Editora de Informática


C#3.5

A RETER ~ Uma enumeração é representada internamente por um inteiro e representa


uma variável que pode tomar um conjunto finito de valores (enumerados).
Enumerações - Uma enumeração declara-se usando a palavra-chave enum:
enum TipoDaEnumeracao { valori, Valor2, ..., valorN };
- Cada um dos valores possíveis pode ter o inteiro que lhe corresponde
definido. Exemplo:
enum interruptor { LIGADO = l, DESLIGADO = O };
" As conversões de inteiros em enumeração e vice-versa são sempre feitas
explicitamente (cãsf).
- Por omissão, o tipo subjacente a uma enumeração é um inteiro de 32 bits.
No entanto, é possível especificar um outro tipo base, numérico, utilizando
dois pontos. Por exemplo:
enum Interruptor : byte { LIGADO = 0x01, DESLIGADO = 0x00 };

4.9 DERNIÇÕES PARCIAIS


Até agora, sempre que definimos uma classe ou estrutura, a mesma teve de ser criada no
mesmo ficheiro e por inteiro. No entanto, existem muitas circunstâncias em que é útil
declarar uma parte de um tipo de dados num ficheiro e a restante parte noutro. Por
exemplo, é bastante comum existirem ferramentas de geração automática de código em
que o programador apenas tem de introduzir certos fragmentos de código em ficheiros
automaticamente criados. No entanto, a partir do momento em que o programador altera
um ficheiro gerado, caso seja necessário executar novamente a ferramenta de geração de
código, os fragmentos introduzidos pelo programador irão perder-se. Para resolver este
tipo de situações, a linguagem C# suporta definição parcial de tipos de dados.

4.9.1 TIPOS PARCIALMENTE DEFINIDOS


Um tipo parcialmente definido corresponde a uma classe, estrutura ou interface, em que
os seus membros estão definidos em mais do que um local, potencialmente ao longo de
diversos ficheiros. Para indicar que um tipo é definido parcialmente, utiliza-se a
palavra-chave parti ai. Por exemplo, a classe Empregado poderia estar definida em dois
ficheiros separados: o primeiro contendo a definição dos campos:
; partíãT class Empregado

private string Nome;


private int Idade;

o segundo contendo a definição dos métodos:


partia!" class Empregado - -- - - - - -- -—-
public Empregado(string nomeoaPessoa, int idadeDaPessoa)
Nome = nomeoaPessoa;
JCdade__=_ idadeoapessoa;.
1 14 © FCA - Editora de Informática
PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

; '} " "~


public void MostralnformacaoO
cons.ole.writeLine("{0} tem
; -} "

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.

Regra geral, não é aconselhável utilizar tipos de dados definidos parcialmente em


programação comum. Ditam as regras de boa prática de programação que todas as
definições de um tipo de dados, estrutura ou interface, estejam agrupadas num único
local, correspondendo a um ficheiro unívoco. A utilização de definições parciais deve ser
limitada a situações, em que se está a fazer uso de alguma forma de geração automática
de código e se pretende manter separado o código do programador, do código gerado pela
ferramenta.

4.9.2 MÉTODOS PARCIALMENTTEDERNIDOS


À semelhança do que acontece com as classes parcialmente definidas, também é possível
ter métodos parcialmente definidos. A declaração de um método parcialmente definido
consiste em duas partes: a sua "assinatura" e a sua "implementação". Estas podem estar
na mesma parte da classe parcial, no mesmo ficheiro, ou em partes separadas em
diferentes ficheiros. Ao definir-se um método parcial em diferentes ficheiros, estes devem
ter a mesma assinatura. Os métodos parcialmente implementados são identificados pela
palavra-chave parei ai.

0 código apresentado de seguida mostra um método definido parcialmente. O primeiro


ficheiro contém os campos e assinatura do método MostralnformacaoO.
partia! cTass Empregado "" " ~ ~~ . . . . . .
. p ri vate ^tring Nome;
pritfaté int idade;
1 ,// Assinatura do método MostralnformacaoO, definido noutro f tis h £1 r o
partia] vold MostralnformacaoO; ,.~

O segundo ficheiro contém a implementação:


EmprégácTò "" ' ' '" " " " . - " ' . . ;."-..
i „_//-. Goos^rUtor, da ç] asse

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

públic Èmpreg'ado(string ndmeDaPessoa, int i dadéD.aRessoa)


•fT- • .'. * - ***-•
***-
Nome = nomeDaPessoa;
Idade = idadeDaPessoa;
a«a. •
; // implementação do método MostralnformacaoQ
partia! vold MostralnformacaoO
console.Wn"teLlneC"{0} tem {1} anos"-, ^Noiue, Idade);
} . ££¥£&'.. •?
} ._.. ,:•& • . . i .... •
Os métodos parcialmente definidos permitem a implementação selectiva de funcionalidade.
Outros ficheiros têm a possibilidade de implementar ou não os métodos declarados. Se o
método não é implementado, o compilador remove a sua assinatura e todas as chamadas
ao mesmo, não originando qualquer erro de execução ou compilação. Este tipo de
métodos é principalmente utilizado por ferramentas de geração automática de código.
Estes permitem que o nome e a assinatura de um método seja reservado, potenciando que
exista código que os use, caso seja necessário. Um aspecto importante deste tipo de
métodos é serem implicitamente p ri vate. Não é possível definir modificadores de acesso
para os mesmos, nem declará-los como vi rtual. Da mesma forma, não podem ter valor
de retorno. Ou seja, são necessariamente void. Se tal não acontecesse, se estes viessem a
ser chamados, o compilador não poderia retirar todas as referências aos mesmos, pois
teriam um valor de retorno a ser usado.

ARETER " Para definir classes, estruturas ou interfaces parcialmente, potencialmente


em mais do que um ficheiro, utiliza-se a palavra-chave parti ai. Por
Definições
exemplo:
Parciais partia! class A { private int x; }
partia! class A { private int x; >
~ Quando o código é compilado, têm de estar disponíveis todas as definições
parciais do elemento que se está a definir. O compilador encarrega-se de
juntar todas as definições num único local, gerando um único assembly.
~ Não é possível adicionar membros a um tipo de dados já compilado para
formato binário.

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

os programadores tiver de ser utilizado em conjunto, existe um grave problema. É certo


que se ambos os programadores se conhecem e trabalham na mesma empresa, o problema
consegue-se resolver facilmente mudando o nome de uma das classes. Mas e se as classes
pertencerem a fabricantes diferentes, aos quais não é simplesmente possível pedir para
mudar os nomes das suas classes?

Os espaços de nomes permitem resolver parcialmente esta situação. Vejamos como.


Sempre que é desenvolvida uma biblioteca, todas as suas declarações devem encontrar-se
dentro de um espaço de nomes:
namespace NomeDaorgani~zacaó".NorneDãBib~liòtécã

// O código é colocado aqui!

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.

É de notar, que é possível declarar espaços de nomes dentro de espaços de nomes:


: namespace A

namespace B
// Código
J ___. . . . .... _. ____ . . . . .
No entanto, isso é exactamente equivalente a definir um nome composto:
inamespace A . B
' -.//.código ._ _ __ _ . .. , _. ....

© FCA - Editora de Informática 1 17


C#3.5

Note-se que um espaço de nomes representa um agrupamento lógico em termos de


nomes, não um agrupamento físico. Por exemplo, um assembly corresponde a um
agrupamento físico de diversos tipos de dados e elementos num ficheiro. Relativamente
aos espaços de nomes, é possível declarar classes do mesmo espaço de nomes em
ficheiros diferentes e importar essas diferentes classes para o espaço de nomes global. É
um agrupamento puramente lógico.

4.1O.1 AUASES
A palavra-chave using também permite definir abreviaturas para classes e espaços de
nomes. Para isso, faz-se:

u si n g abreviatura = EspacoDeNomes\s que se quer que Tst passe

csharpcursocompl eto .Teste. Para isso, basta escrever:


jjsing. Tst_= CSharpCurspÇompTeto.J~este; _ ["'__ _ "~ :

Neste caso, em vez de escrevermos:


iCSfiàrpCursdcómplétò".Teste.Pessoa p = " " ~" "" "
new.csharpçurspcompleto.Teste.pessoaQ; _ :
podemos escrever:
Tsf '..Pessoa Vp\"= rièw" Tst.PessoaO 3 ."." . . . . " . . . . ' . " . ' " " _ . . " " _ . " "."~"I 7 777*".
Esta funcionalidade é muito útil, quando estamos em presença de um conflito de nomes
de classes entre classes que estão dentro de espaços de nomes com nomes muito
compridos. Assim, em vez de ser necessário estar a utilizar sempre o nome completo da
classe, basta definir uma abreviatura para o espaço de nomes completo ou, mesmo, para a
classe directamente (embora isso seja menos recomendável). Para definir uma abreviatura
para uma classe, utiliza-se exactamente a mesma sintaxe:
.using" person""=""csharpcursocompleto".Teste.PessoãT

•Pe.r.son j3 = new Person_Qj __ '


Falta ainda referir um último ponto. Caso se utilize o mesmo nome de classe no mesmo
espaço de nomes, embora em ficheiros diferentes, isso resulta num erro de compilação.
Este comportamento é obviamente o que seria de esperar. Os espaços de nomes são
agrupamentos lógicos. Assim, não é possível definir a mesma classe duas vezes no
mesmo "agrupamento". A definição de cada tipo de dados tem de ser única.

118 © FCA - Editora de Informática


PROGRAMAÇÃO ORIENTADA AOS OBJECTOS

~ Um espaço de nomes representa um agrupamento lógico de classes.


~ Para declarar um espaço de nomes, utiliza-se a seguinte notação:
Espaços de namespace NomeDoEspaco { // ... membros }
nomes " Podem existir espaços de nomes dentro de espaços de nomes. O espaço de
nomes completo é constituído pelos vários espaços de nomes separados por
pontos. Por exemplo:
namespace Espaço {
namespace SubEspaco {

representa a mesma coisa que:


namespace Espaço.subEspaco { ... }

A palavra-chave using permite importar os elementos de um espaço de


nomes para o espaço de nomes global.
Em caso de conflito entre nomes de tipos definidos em diferentes espaços
de nomes, é necessário utilizar o nome completo do tipo, incluindo o espaço
de nomes a que pertence.
É possível utilizar a palavra-chave using para definir uma abreviatura para
um certo espaço de nomes. Por exemplo:
using ms = Microsoft.Wln32;

© FCA - Editora de Informática l 19


Ao escrever o código de uma aplicação, o programador tem constantemente de ter em
conta que podem ocorrer situações excepcionais ou mesmo de erro. Por exemplo, pode
esgotar-se o espaço em disco ao escrever para ficheiro, o acesso à rede pode ficar
indisponível quando se está a aceder a um site remoto ou pode mesmo não haver mais
memória disponível para criar um novo objecto.

Virtualmente, todas as linguagens de programação modernas dispõem de formas mais ou


menos sofisticadas de lidar com este tipo de situações. Um dos mecanismos mais
habituais, e bastante poderoso em si, é o sistema de excepções.

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)

© FCA - Editora de Informática 121


C#3.5

// Nome dos ficheiros de origem e destino


string NomeOrigem = args[0];
string NomeDestino = args[l];
// Abre os ficheiros de origem e destino
FileStream origem = new FileStream(NomeOrigem, FileMode.Open);
FileStream destino = new FileStream(NomeDestino, FileMode.Create);
// Define um buffer de cópia (8 kbytes)
const int BUF_SIZE = 8*1024;
byte[] buffer = new byte[BUF_SIZE];
int bytesLidos = 0;
// copia o ficheiro origem para o ficheiro destino
do
{
bytesLidos = origem.Read(buffer, O, BUF_SIZE);
destino.WriteÇbuffer, O, bytesLidos);
} while (bytesLidos > o);

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.

122 © FCA - Editora de Informática


EXCEPÇÕES

O tratamento de erros por excepções assenta no conceito básico de bloco try~catch.


Sempre que pode existir a ocorrência de um erro (ou de uma situação excepcional), o
código em causa é envolvido num bloco try-catch:
try

// Copla o ficheiro origem para o ficheiro destino


do
{
bytesLidos = origem. Read(buffer, O, BUF_SIZE) ;
destino.Write(buffer, O, bytesLidos) ;
} while (bytesLidos > 0);

origem. CloseC) ;
destino. Cl oseQ ;

catch (lOException erro)

"consÒTê.WritêLine"C"ocõrreu um erro na cópia "do fichei ro!\n");


: console.WriteLine("Deta"lhes: " + erro.Message) ; :

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

"f bytesLidos — ongem.Read Cbuffer, 0, BUF_SIZE) ;


destino. writeCbuffer, 0, bytesLidos) ; [~~"^X Excepção lançada em
desti no. WriteC)
} while CbytesLicTos > 0); ^~"N\. CloseC)
sendo; o controlo de
execução transferido
para o bloco catch
desti no. CloseC) ; A
catch ciÒException erro; <r ^s
correspondente.
.
Console.WriteLineC"ocorreu um erro na cópia do ficheiro!\n");
Console.WriteLineC"Detalhes: " + erro.Message);
origem.CloseC) ;
destino.CloseC);

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.

Muitas vezes, associado a um bloco try-catch, existe também um segmento finally.


Um bloco finally representa um conjunto de código que deve ser executado sempre,
quer haja excepções lançadas ou não. Por exemplo, os ficheiros origem e destino são
sempre fechados havendo excepção ou não. O bloco final!y coloca-se após o último
bloco catch (pode existir mais do que um bloco catch):
try
// Copia o ficheiro origem para o ficheiro destino
do
bytesLidos = origem.ReadCbuffer, O, BUF_SIZE);
desti no.Wri te Çbuffe r, O, bytes Li dos);
} while CbytesLidos > 0);
catch ClOException erro)
Console.writeLineC"Ocorreu um erro na cópia do ficheiro!\n");
Console,WriteLineC"Detalhes: " + erro.Message);
fTnally " " " " " ~ ™ - - -
origem.CloseC) ;
destino.CloseC);

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

124 © FCA - Editora de Informática


_ EXCEPÇÕES

FileNotFoundException. Esta excepção é derivada de system.io. lOException, sendo,


portanto, um caso especial de um erro de entrada/saída. É certo que é possível envolver os
vários pedaços de código em blocos try-catch com as excepções correspondentes. No
entanto, isso não é uma boa solução. Uma das grandes vantagens do sistema de excepções
é o de ser possível colocar blocos de código mais ou menos grandes e logicamente
coesos, fazendo o tratamento de erros à parte. E possível especificar vários blocos catch.
Voltemos ao exemplo:

// Abre os ficheiros de origem e destino


FileStream origem = new FileStream(NomeOrigem, FileMode.Open) ;
FileStream destino = new Fi1eStream(NomeDestino , FileMode.Create) ;

// Define um buffer de cópia (8 kbytes)


const int BUF_SIZE = 8*1024;
byte[] buffer = new byte[BUF_SlZE] ;
int bytesLidos =0;
// Copia o ficheiro origem para o ficheiro destino
do
bytesLidos = origem. Read(buffer, O, BUF_SIZE) ;
destino. WriteÇbuffer, O, bytesLidos) ;
} while (bytesLidos > 0);
}......................__. „„_.........................________________________.....________............__ .......... _
catch ( FileNotFoundException erro)_

console. WriteLine("0 seguinte ficheiro não existe: {0}",


erro.FileName) ;
.}..........................„..........„ . _ ..... ...................__________......_______......_............_________
catch (lOException erro)

Console. WriteLine("Ocorreu um erro na cópia do fichei ro!\n") ;


Console. WriteLine( n Detalhes: " + erro.Message) ;
1.. . . .. ........... . ..... ..............._. ........ ... ........ ....... .
Neste caso, existem dois blocos catch. Caso ocorra uma FileNotFoundException, esta é
tratada pelo primeiro bloco. Caso ocorra um outro erro qualquer do tipo lOException,
este é tratado pelo segundo bloco.

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

FCA - Editora de Informática 1 25


C#3.S _

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!).

O sistema de excepções deve apenas ser utilizado em situações "excepcionais" e de erro.


Nunca deve substituir um simples teste, se tal for possível de fazer e não complique a
estrutura do código. Acima de tudo, o mecanismo de excepções não deve ser utilizado
para fazer controlo do fluxo de execução genérico. Ora, é muito comum o utilizador
esquecer-se de colocar um nome na linha de comandos ou, mesmo, chamar o programa
sem argumentos, para ver os argumentos com que este é executado. Assim, faz muito
mais sentido fazer um teste simples ao número de argumentos da linha de comandos do
que propriamente, deixar que seja feito um acesso à tabela de argumentos, levando a uma
situação excepcional:

Console. wri-geJtíJijeOArgumentos inválidos");


: Console. WriteMeCllCopia < original> <destino>");
* ~ •
// Termina Q
'. Environment/àicrKo) ;

Na listagem 5.2, é mostrado o programa completo, incluindo o tratamento de excepções.

© FCA - Editora de Informática


EXCEPÇÕES

Programa que ilustra o conceito de excepções.


Este programa copia um ficheiro origem para um ficheiro destino.
Versão com tratamento de excepções.

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

string Nomeorigem = args[0]; // Nome do ficheiro de origem


string Nomeoestino = args[l]; // Nome do ficheiro de destino

Filestream origem = null;


Filestream destino = null;

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;

// copia o ficheiro origem para o ficheiro destino


do
bytesLidos = origem.Read(buffer. O, BUF_SIZE);
destino.Write(buffer, O, bytesLidos);
} while (bytesLidos > 0);
catch (FileNotFoundException erro)
Console.WriteLine("o ficheiro {0} não foi encontrado!",
erro.FileName);
}
catch (Exception erro)
Console.WriteLine("ocorreu um erro na cópia do ficheiro!\n");
console.WriteLine("Detalhes: " + erro.Message);
>
final! y
// É necessário proteger o fecho dos ficheiros com blocos
// try-catch pois também pode ocorrer uma excepção no seu fecho
try

© FCA - Editora de Informática


C#3.5

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,

1 28 © FCA - Editora de Informática


_ EXCEPÇÕES

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.

5.2 ESTRUTURA GENÉRICA


A estrutura genérica de um bloco try-catch é a seguinte:
try " " " " " ' " "" " ...... "" " " :

: // o código que pode lançar lançar excepções encontra-se aqui

catch (TypeAException excA)


1 // Trata a excepção do tipo TypeAException

catch (TypeBException excB)


// Trata a excepção do tipo TypeBException

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.

Todo este processo de propagação de excepções tem um efeito interessante. A medida


que a excepção vai sendo transferida de bloco em bloco, todas as variáveis declaradas
dentro dos blocos internos vão sendo automaticamente eliminadas. Isto é, o CLR trata de
fazer a limpeza do stack, à medida que vai abortando a execução dos diversos blocos
encadeados.

Vamos examinar, agora, o segundo ponto importante: o mecanismo de excepções


funciona também ao longo de chamadas encadeadas de métodos. Esta é uma extensão
simples, de que o leitor provavelmente estaria à espera. Se ta] mecanismo não existisse
seria muito difícil, senão mesmo impossível, ter uma gestão de erros que fosse utilizável
em desenvolvimento de software em larga escala.

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.

130 © FCA - Editora de Informática


EXCEPÇÕES

"cTáss Teste _-(.


publfc vòfd xQ
try •
""""

.. . . „ . . . . . _ .
catch (Exception e)
// Tratamento da 5. Bloco catch
// excepção encontrado, É feito o
tratamento da excepção.

"public void F()


GÒi 4. F Q é abortado como um
todo por não ter um catch
correspondente.
public void G()
X " "
3. Ocorre uma excepção. G Q é
// ocorre aqui uma excepção abortado como um todo por não ter
um catch correspondente.

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

© FCA - Editora de Informática 131


C#3.5

indicam que em programas bastante grandes, a declaração e o tratamento explícito de


excepções diminui a produtividade dos programadores3.

5.3 LANÇAMENTO DE EXCEPÇÕES


Até agora, vimos, apenas, como se pode apanhar e tratar excepções. Falta, no entanto, ver
como é que estas são originadas em primeiro lugar, isto é, como é que são lançadas.

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");

'// Resulta em "Idade inválida"


'Console.Wr1teLlneC"{0}" J Idadelnvallda.Message};

Normalmente, deve-se criar novas classes a partir de Exception ou, preferencialmente,


de Appl 1 cationException, pois isso permite que sejam criados blocos catch
especialmente preparados para esse tipo de excepções. Um bloco catch que apanhe
simplesmente Exception não possui muitas formas de tratar o erro que ocorreu, pois não
possui grandes detalhes sobre o mesmo.

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.

Vejamos, agora, como fica o método MudaidadeQ da classe Empregado:


: public class Empregado " "" " " ;

private int Idade;

public void Mudaldade(int novaldade) ;

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.

Finalmente, vejamos o código do ponto de vista de quem está a utilizar a classe


Empregado:
Émpregado"~émp = h'ew"Êmpregadõ'C"Ahtohfb' Manuel''OV' " '.

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

134 © FCA - Editora de Informática


^ EXCEPÇÕES

No entanto, tal é considerado uma forma de programação muito má5. As excepções


devem ser utilizadas em acontecimentos especiais, excepcionais e não para controlar o
fluxo de execução normal de um programa.

5.4 HIERARQUIA DE EXCEPÇÕES


Ao contrário do que acontece em C++, onde se pode lançar qualquer tipo de dados como
excepção, em C#, apenas se pode lançar objectos que de uma forma ou de outra derivem
de System.Exception. O diagrama da figura 5.l mostra algumas excepções da hierarquia
de excepções da plataforma .NET.

Excepções especificas da aplicação


que o programador desenvolve

Figura 5.1 — Algumas excepções da hierarquia de excepções da plataforma .NET

Existem imensas excepções definidas nas diferentes bibliotecas da plataforma. Vamos


analisar os três grandes braços da hierarquia de excepções.

As excepções derivadas de systemException representam excepções que ocorrem


devido ao funcionamento interno do runtime da plataforma .NET. Em geral, o
programador não deve tratar estas excepções, embora possa fazê-lo. Estas excepções

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.

As excepções derivadas de lOException representam erros nos dispositivos de entrada e


de saída. Tipicamente, são tratadas pelo programador. Devido ao facto de serem tão
importantes e tão comuns, têm direito a um destaque especial em termos de hierarquia de
classes.

Ao desenvolver as suas aplicações e ao criar novas excepções, o programador deve, regra


geral, derivá-las de Appl 1 cati onExceptl on. Esta é a classe base para uso nas aplicações
comuns, em termos de processamento de excepções.

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.

Message Uma mensagem de texto que descreve a excepção. Esta é a mensagem •


tipicamente passada nos construtores das excepções.
Source 0 nome do objecto (ou aplicação) que deu origem à excepção.
A lista completa de chamadas de métodos que levaram ao erro. Esta
StackTrace propriedade é muito útil, sendo muitas vezes colocada uma linha
Console.Wr1teLlne("{0}", excepção. StackTrace) ;
no bloco catch que apanha a excepção, para efeitos de debuggíng.
Targetsi te | o nome do método que lançou a excepção.

Tabela 5.1 - Propriedades importantes de System. Excepti on

ARETER - O mecanismo de excepções é baseado em blocos try-catch-flnally:


try
// Código que pode lançar excepções
Excepções
catch (TypeAExceptlon a)
// Tratamento de excepções do tipo "A"
catch (TypeBException b}

// Tratamento de excepções do tipo "B"


>
catch

O conceito de "propriedade" será explorado no capftulo dedicado à programação baseada em componentes.


Por agora, importa saber que uma propriedade funciona como uma variável pública, podendo-se obter o
seu valor, assim como modificá-lo.
136 © FCA - Editora de Informática
EXCEPÇÕES

ARETER // Tratamento de qualquer excepção


f-i nal 1 y
Excepções
// Bloco de código que executa incondicionalmente

" 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.

5.5 EXCEPÇÕEB DE ARITMÉTICA


Existem, ainda, duas palavras-chave relacionadas com o sistema de excepções que iremos
agora examinar. Essas palavras-chave são: checked e unchecked.

Suponhamos que temos o seguinte código:


ushort valor = 65535;"
-H-val o r; "
.Console.WriteLi_neC"yalo.r =..{0}"^. valor); _ _._ ._
Como valor é do tipo ushort, apenas pode conter valores até 65535. Ao incrementarmos
o seu valor, irá existir um overflow da variável, regressando a mesma a 0. Ou seja, ao
executar o código anterior, irá surgir o valor O no ecrã.

Este é o comportamento por omissão do CLR. No entanto, existem diversas circunstâncias


em que este comportamento não é desejável, sendo mais importante para o programador
ter a certeza de que os limites das variáveis que está a utilizar não são excedidos. Por

© TCA - Editora de Informática 137


C#3.5

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.

No caso de ser necessário desligar a verificação de excepções, pode-se declarar o código


num bloco unchecked:
lusriõrf "VãTõr '="65535"; *™~' ..... •......•"•" • - — - ...... —..._.„....__ j

j unchecked
K
i ++valor;

.=. £Q}"i. .valor).;


Neste caso, independentemente das opções de compilação utilizadas, não haverá lançamento
de excepções no caso dos limites da variável valor serem excedidos.

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

~ Para evitar o lançamento de excepções devido a violações de limites


numéricos de variáveis, utiliza-se blocos unchecked:
unchecked

É ainda possível aplicar as palavras-chave checked/unchecked a expressões,


bastando utilizar a palavra-chave como um operador, envolvendo a
expressão em parêntesis:
checkedCexpressão) unchecked(expressão)

© FCA - Editora de Informática 139


Como referimos na introdução do livro, um dos avanços mais importantes que ocorreu
durante a última década, em termos de desenvolvimento de sqfhvare, foi a vulgarização
da programação baseada em componentes.

Um componente é uma unidade reutilizável de sqftware, tipicamente, com fronteiras bem


definidas, sendo encapsulado num invólucro binário1. Associado à utilização de
componentes, existem, tipicamente, ambientes visuais que permitem manipulá-los
directamente, quase sem que tenha de ser escrito código foute. Neste tipo de
programação, o código fonte escrito é normalmente uma cola entre os componentes,
implementando uma certa "lógica de negócio" que orquestra as relações e a utilização dos
componentes.

Do ponto de vista de programação, um componente corresponde a uma classe. No entanto,


existem três elementos básicos, muito importantes, que suportam a sua utilização e a
interligação a outros componentes:

Propriedades: uma propriedade representa um certo aspecto do estado de um


componente. Por exemplo, se tivermos um componente que represente um botão
no ecrã, uma propriedade poderá ser o tamanho do botão e outra poderá ser o seu
título. Do ponto de vista de programação, uma propriedade funciona corno sendo
uma variável pública de um objecto, com a diferença de que existe um método
que é chamado quando o seu valor é alterado e existe um outro método que é
chamado quando o seu valor é lido. Regra geral, todo o estado de um componente
deverá ser definido pelo valor das suas propriedades;

Métodos: os métodos representam os habituais métodos das classes. Quando se


chama um método num componente, existe uma certa acção que é realizada nesse
método. Os métodos representam acções que não podem ser manipuladas ou
realizadas visualmente;

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

Eventos: Um evento representa um acontecimento a nível do componente.


Trata-se de uma notificação. Quando o componente lança um evento, existe um
ou mais receptores desse evento que são notificados, sendo um certo pedaço de
código corrido nos receptores do evento. Um componente pode registar-se com
outros componentes para receber eventos e, por sua vez, pode lançar eventos.

Na figura 6.1, pode-se ver um exemplo de utilização de componentes no desenvolvimento


de uma aplicação, utilizando o ambiente VisiialStitdio.NET.
E* S5t» Bifctt »« Qcbug Otft
• já - q H g * -^ a T .'••

Figura 6.1 — Utilização de componentes no VisuaIStudío.NET

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.

142 © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

Be Wt BÍ» &o)Kt S*I Cutug Dite Fgml IB* )&ife. Camully U*


X JJ •", «J • •" - f J . - V > WMJ r *wcw r a,m«ta. ,- JJ -? 3 i-1 ^

Figura 6.2 — Associar de um evento ao carregar do botão

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 exemplo, o VisiialStiidlo.NET encarrega-se de registar o interesse do código em


receber este tipo de notificações. Para obter a funcionalidade descrita, tudo o que o
programador tem de fazer é acrescentar o código necessário ao método criado. Por
exemplo:
textBoxl.Text = "p'.Bptãõ"fpj"cãrrégádòí"7.r ~ .
Para além das propriedades, eventos e métodoSj existe ainda uma funcionalidade da
linguagem, muito útil e importante no contexto da programação baseada em
componentes: os atributos. Um atributo representa uma característica declarativa de um
certo componente. Por exemplo, um certo componente pode "declarar" que necessita de
uma certa funcionalidade de segurança para executar. Ou pode "declarar" que para ser
utilizado, necessita de uma outra biblioteca externa. Os atributos, associados aos
componentes, permitem exactamente isso. É da responsabilidade do ambiente de
execução olhar para os componentes, analisar os seus atributos e criar um contexto de
execução apropriado.

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.

© FCA - Editora de Informática 143


C#3.5 _

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;

public int obtemldadeQ


{ i
return Idade; •
}
public void AlteraldadeCint novaldade)
i f (novaldade < 0)
throw new idadelnvalidaException(novaldade) ;
Idade = novaldade;
}

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);

// A pessoa faz anos, adiciona-lhe mais um ano


emp.AlteraldadeCemp.obtemldadeC) + 1) ;
Embora isto resulte, não é propriamente intuitivo ou elegante. O que nós gostaríamos de
fazer seria algo do género:

l 44 © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

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

' // Onde fica realmente armazenada a Idade


; private int idadeEmpregado; ;
// A propriedade pública, vista externamente
public int idade
get
return IdadeEmpregado;
}
set
i f (value < 0)
throw new idadelnvalidaException(value);
IdadeEmpregado = value;
}
}
J ....... -
Existe uma variável privada chamada idadeEmpregado onde, internamente, é guardada a
idade. Existe, ainda, uma propriedade pública chamada idade, tendo dois métodos
associados: get e set.

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

Assim, toma-se possível escrever expressões como:


empMdãde = 19; // set chamado
:Conso1e.Wr1teLineC"{0}" J emp..Idade); // get chamado,
ou mesmo:
^ernp.idade'= èmp.Idade + 1;
++emp.Idade;
;emp.idade += 1; _ _ :
Este exemplo também deve deixar claro porque é que não se deve declarar variáveis
como públicas. No caso de empregado, se declarássemos idade como sendo
simplesmente um inteiro público, nada impediria outro pedaço de código de colocar
Idade com um valor negativo. Mantendo o encapsulamento de dados e utilizando
propriedades, é possível garantir que certos invariantes da classe nunca são violados,
como, por exemplo, a idade ser maior ou igual a zero.

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

Finalmente, corno seria de esperar, as interfaces também podem especificar propriedades


que devem ser definidas pelas classes que as implementam. Por exemplo, a seguinte
interface especifica três propriedades que devem ser implementadas: uma apenas com
get, uma apenas com set e uma com. get e set.
intèrface~InterfaceSimp1es" . . - . - . . . . .
int PropriedadeA { g e t ; } // Apenas com get
nnt PropriedadeB { s e t ; } // Apenas com set
int Propnedadec { get; set; } // Com ambos

6.1.1 PROPRIEDADES AUTOMÁTICAS


E bastante frequente, numa classe, ser necessário definir propriedades que representam
variáveis simples. Por exemplo, na classe Empregado, Nome será possivelmente uma
propriedade simples, implementada como:

1 4G © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

public class Empregado" .....

p^fv^tè-' string _nome; //Variável subjacente ao nome.


pubiic'stri.rig Nome // propriedade pdblica que o representa

g et" •'£* tíeturn _nome; } " ;1


set { ^tfome " = value; } -•'-:.
' '.

>. '" ' i . ' . . - ...... -.....- .......... . .. •


As propriedades automáticas tornam possível a implementação de propriedades que
representam campos simples de uma forma mais concisa, não sendo necessária nenhuma
lógica adicional. Deixa de ser necessário declarar a variável como privada para que
possamos definir uma propriedade. Para tal, basta indicar que operações esta propriedade
suporta (get, set, ou ambos). No exemplo, ficaria:
public cTass Empregado
j public string Nome { get; set; } í
r?

.y '" ' ... . . . . - ..... •


Quando o compilador encontra um "get;" ou um "set;", cria automaticamente as
variáveis privadas correspondentes e implementa as propriedades públicas g et/s et.
Torna-se assim possível escrever:
emp.NÕmH T ^- J I António Manuel "j_ . ; _ _ . / / emp e "do", tipo _Émpregado :
No caso de necessitar de uma propriedade automática apenas de leitura, bastará declarar o
operador set como privado:
public" class Empregado
Í public string Nome { get; private set; "} // Apenas de leitura pública)

6.1.2 PROPRIEDADES INDEXADAS


Existe um tipo especial de propriedades, chamado propriedades indexadas, que por vezes
são muito úteis.

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:

© FCA - Editora de Informática 1 47


C#3.5

(int i=0; i<empregados.Length; Í-H-)

Empregado emp = empregados[i] ;


console. WriteLine("{0}", emp. Idade) ;

Isto é possível, definindo as seguintes propriedades na classe ListaEmpregados:


;cl ass * Li st~aEmpr~égãdos

private Empregado[] Lista;

public Empregado this[int index]


get l
i
return Lista[index] ; :

s et
{
Lista[index] = value;

: } ;

Neste caso, o nome da propriedade é declarado com a palavra-chave this, colocando,


entre parêntesis, um inteiro que representa o índice do elemento a aceder. Tal como nas
propriedades normais, é também possível declarar apenas o get ou o set, fazendo com
que a propriedade seja apenas de leitura ou apenas de escrita, respectivamente. Neste
caso, optámos por não fazer verificações da validade do índice de acesso, porque, caso
seja inválido, o acesso à tabela em causa irá gerar automaticamente uma excepção. Um
facto muito curioso das propriedades indexadas é que se pode passar como parâmetro à
propriedade, algo que não seja um inteiro ou até mais do que um parâmetro3. Podemos,
por exemplo, considerar que queremos aceder a um empregado por nome. Isto é; ao fazer:
;Êmp_regadp_emp;= empregado"s[ n Mahu " '_ ~ " ~"_'_2........"
o objecto retornado corresponde a "Manuel Marques", independentemente da fornia
como ele se encontra armazenado em ListaEmpregados. Isto pode ser conseguido com o
seguinte código:
Empregãdõ^thnslstring" nome] . - . - . . _. . _._ ,,
: qet
foreach (Empregado emp i n Lista)
i.f

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.

14S © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

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.

" As propriedades funcionam como variáveis de uma classe.


ARETER
" Uma propriedade declara-se como se fosse um campo normal, mas
adicionado de um corpo. Nesse corpo, existe pelo menos um dos seguintes
Propriedades
métodos: get e set. O get é invocado quando se está a obter o valor da
propriedade; o set é invocado quando se está a modificar o valor da
propriedade. O método set possui uma variável implícita (value) que
contém o novo valor da propriedade.
" A estrutura de uma propriedade é:
publlc TipooaPropridade NomeDaPropriedade
get { // obtém o valor da propriedade }
set { // Modifica o valor da propriedade }
}
" É possível definir propriedades que representam campos simples. Nesse
caso, não é necessário escrever nenhum código para obter e/ou alterar o
valor da propriedade. Basta apenas indicar que a propriedade existe, com as
palavras get e set:
public TlpoDaProprldade NomeDaPropriedade { get; set; >
" Uma propriedade não é necessariamente pública. Pode ter qualquer nível de
acesso. Urna propriedade também pode ser estática, virtual ou abstracta,
como se de um método se tratasse.
~ As interfaces também suportam especificação de propriedades a serem
implementadas pelas classes que as suportam.

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

- É possível definir propriedades indexadas que permitem ver ura objecto da


ARETER classe como sendo uma tabela.
" Uma propriedade indexada é definida, utilizando a palavra-chave this e
pri indicando entre parêntesis rectos a lista de parâmetros formais:
public TipoDovalorDeRetorno
this[Tipol paraml, Tipo2 paramZ, ...]
get { // obtém o valor da propriedade }
set { // Modifica o valor da propriedade }

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

Figura 6.3 — Produtor/consumidor de eventos

Antes de abordarmos a estrutura de eventos em detalhe, teremos de examinar uma


construção da linguagem chamada delegate, na qual o modelo de eventos é baseado.

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.

1SO © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

Consideremos novamente a classe Matemática, mas, agora, possuindo três membros


estáticos: Max O - que calcula o máximo de uma tabela de números; Mi n Q - que calcula
o seu mínimo; e Medi a Q que calcula a sua média:
public class Matemática

', public static void Max(int[] valores)

int max = valores[0]; ;


foreach (int vai i n valores)

: i f (vai > max)


, max = vai;

Console.WriteLine("Max = {0}", max);

public static void Min(int[] valores)

int min = valores[0];


foreach (int vai i n valores)

i f (vai < min)


mi n = vai;

Console.WriteLine("Min = {0}", min); •

public static void Media(int[] valores) ;

int total = 0;
foreach (int vai i n valores) :
total+= vai;
Console.Writel_ine("Média = {0}", (doubl e) total/vai ores. Length) ;

.} ........ - - - . - - ..-- ----- -- - - •


Para exercitar esta classe, podemos fazer algo tão simples como:
int[] valores = { 12, '327 34," '43, 23 1; ;

Matemati ca.Max(vai o rés); ;


Matemática.Mi n(vai orés); :
Matemati ca. Medi aj^val o r é s ) ; . ... _
Suponhamos, agora, que em vez de querermos chamar explicitamente uma função em
particular, queremos ter uma variável que possa chamar uma das três funções. Para isso,
cria-se um delegate. Um delegate funciona como uma classe, mas na verdade representa
um protótipo de um método. Neste caso, qualquer uma das funções Max Q, Mi n Q e
Medi a Q leva como parâmetro uma tabela de inteiros e não retorna nenhum valor. Para
definir tal delegate, faz-se:
delegate void Função Çint.G valores) L \_ ~_ "_ T ._ . . . .

© FCA - Editora de Informática 15 !


C#3.5 _

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 }; ........... ..... "" :

Resulta no cálculo do máximo dos valores, isto é, 43.

Também podemos colocar f a referenciar outros métodos:


if = "ríew FuYicao(MatematicâYMin) ;"
f (valores) ;
f = new Função(Matemática.Medi a);

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 ;

Console. Writei_ine("Max = {0}", max);

public static void Min(int[] valores)


int min = valores [0];
foreach (int vai in valores)
i f (vai < min)
mi n = vai ;

Console. WriteLine("Min = {0}", min);

152, © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

public static void Med1aC1nt[] valores)


int total = 0;
foreach (int vai In valores)
total+= vai;
Console. writet_ine("Média = {0}",
(double)total/valores.Length);
}

delegate void Função (i nt[] tabela);


class ExemploCap6_l
static void Main(string[] args)
int[] valores = { 12, 32, 34, 43, 23 };
Função f = new Função(Matematica.Max);
f (valores);
f = new Função(Matemática.Mi n);
f(valores);
f = new Função(Matematica.Medi a);
f(valores);

Listagem 6.1 — Programa que ilustra o conceito de delegate (ExemploCap6_l.cs)

Note-se que a definição de um delegaie funciona de forma similar à definição de uma


classe, logo tem de ser feita fora de métodos, ao nível de topo do ficheiro. Também é
possível fazer a definição de um delegate dentro de uma classe, mas fora de todos os
métodos definidos. Assim, um delegaie pode ser declarado com modificadores de acesso,
como public, private ou protected.

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.

© FCA - Editora de Informática 1 53


C#3.5 ^

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.

Um outro ponto extremamente relevante é que no caso dos multicast delegates, os


métodos associados terão de retornar obrigatoriamente void. Isto, porque não existe um
meio de obter simultaneamente os vários valores de retorno das invocações que ocorrem
numa destas chamadas.

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.

Vejamos um exemplo simples. Consideremos a classe Ponto, que possui o método


Distanci aQ, que calcula a distância a um outro ponto:
,public"class ponto " """ '""" "" "" • - - • - • - ,
private int x;
: p n'vate int y; :

; public ponto(int x, int y)


i this.x = x; '•-
• this.y - y; ;

L .pub11c_yojd_ Distancia(Ppntp p) _ _ __ _ _ :
154 © FCA - Editora de Informática
PROGRAMAÇÃO BASEADA EM COMPONENTES

: double dist = Math.Sqrt((x-p.x)*(x-p.x) + (y-p,y)*(y-p,y)) ;

Console.WriteLine("distância([{0},{l}] [{2},{3}]) = {4}",


x, y, p.x, p.y, dist); ;
i }

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:

•delegate void' õperãcaóSóbrePòritõsCPõntõ~~p7;~" ~

;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^ _~ _ _ . ... ... ..

uma referência para p2. Di stanci a é também guardada.

Quando é feito:
;ponto p3 = new "PontoCS'0, 50) ;

é automaticamente chamado em ordem pl.Distancia(p3) e p2.Distancia(p3),


resultando nas seguintes linhas a serem impressas no ecrã:
[distanciaC[10f10]
Ldjstânçj.aC [20^30] ..CSO^SQD..». 36_,p5A51275463?9 ________ _. _ ...... ........ .

6.2.3 MÉTODOS ANÓNIMOS

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

um delegate. No entanto, também é comum a implementação de alguns desses métodos


corresponder apenas a uma ou duas linhas de código. Nessas circunstâncias, é uma
sobrecarga para o programador ter de definir completamente um novo método, apenas
para fazer uma chamada a uma função. Para evitar tal trabalho, em C# 2.0, foi definido
um novo conceito chamado "método anónimo". Entretanto, em C# 3.0, surgiu uma nova
funcionalidade, chamada expressão lambda, que permite utilizar uma sintaxe mais
concisa para definição de métodos anónimos.

6.2J3.1 MÉTODOSANÓNIMOSUSANDOZ?£LaS47E5

Consideremos um exemplo motivador. Suponhamos que uma determinada API define um


método estático que dada uma função (um delegate) e uma lista de inteiros, aplica essa
função à lista, resultando numa nova lista5. A implementação interna desta API seria
semelhante a:
"pubTic "delègatè int "FuncáoCint valor); ' " ........... ";
c static int[] Map(Funcao f, int[] lista) :
: int[] novaLista = new i nt [lista. Length] ; ;
for (int 1=0; i<lista. Length; Í-H-) :
novaLista [1] = f(lista[n]) ; ;
i return novaLista; '•

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);

pode ser substituído por:


;int[J quadrados = MapCdélegate(1nt:"vá*lor) - - - - - - .^
•-J- -- T -' L •
; - : ? -1' ,' return valor*valor; ;,:
' ' ;

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.

É de notar que sempre que se define um método anónimo, a lista de parâmetros de


entrada deste tem de ser compatível com a lista de parâmetros exigida pelo delegate
real. Naturalmente, o valor retornado no método anónimo também tem de ser idêntico, ou
conversível, com o delegate original. Caso o delegate original não tenha valor de
retorno, então o método anónimo não necessita de retomar nenhum valor.

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

_ "_ .7"... .".// ímpnme.q valor 15" " 777._______" 7


Repare-se que a única razão pela qual este método anónimo necessita de retornar um
valor é devido à definição original de Função a tal obrigar:
&lic ;de]ega?térijit_ _Fujicãp (int. vai õr);_______77. _ _ _. 7777". . 777.77 7~ "77777 "77
Não podemos deixar de salientar, que os métodos anónimos só devem ser utilizados muito
esporadicamente e em situações que não tomem o código confuso. Na verdade, a sua
grande utilidade é na programação visual, baseada em componentes, em que é necessário
utilizar pequenos fragmentos de código em resposta aos mais diversos tipos de eventos
(por exemplo, o utilizador carregar num botão do rato, sendo necessário actualizar um
quadro de texto). Sem métodos anónimos, o programador seria obrigado a definir
imensos métodos, cada um deles contendo, tipicamente, apenas uma ou duas linhas de
código.

6.2.3.2 EXPRESSÕES LAMBDA


As expressões lambda são a evolução natural de métodos anónimos, representando uma
funcionalidade muito importante introduzida na versão 3.0 da linguagem C#.

Consideremos, novamente, o exemplo que vimos anteriormente. Ao escrever o código


abaixo, conseguimos calcular tanto os quadrados como os cubos de uma certa lista de
valores:
rinfTI"vã1o?és "V í X, "2 i 3 , ' T , 5 }; ..... ~ ~ ............ '" ....... ;
|int[] quadrados = Map(delegate (int x) { return x*x; }, valores); :
|int[]_ cubos _ ..= Map(de]egate...(int. x) ..{_ return _ x * x * x ; _ } 3 yalqres) ;_ 7

As expressões lambda permitem escrever os métodos anónimos de forma mais compacta


e concisa. Em vez de escrever a expressão:

basta escrever, de forma abreviada:


'x7=>'7x*x;7._ "71"..7-7.77.ni7"l"77'_"7"7. 77 .7.77" 7,_ _ _7. 77. 77 "7 ~" "" ":
Esta expressão significa que é um método anónimo com um parâmetro de entrada (x),
retornando um outro valor (x*x). É utilizada inferência de tipos6 para determinar os tipos
correctos de entrada e saída. Ou seja, o código anteriormente apresentado pode ser escrito
como:
•j rit[] quadrados ="Mãp^(x~ =>x v r x; valores)"; "" - - - -- ----- ....... - . _ . ..
int[] cubos _ =LMap(x__=> x*x*Xj .valores);

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.

Em relação aos métodos anónimos convencionais, as expressões lambda oferecem


funcionalidades adicionais;

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 " " •

op mult = new Op( delegate (int 'a, int b)$kreturn a*b; } );


Op soma - new Op( delegate (int a, int b)Mf return a-t-b; } );

int a=m, .b=20; , "


b); •// c = 10*20 = 200
nnt d = somaCa,.. b); ......... // .d .= 10+20 =.30 ___________________
Neste exemplo, os delegates podem ser escritos mais directamente como:
op miflít = (a, "b)~ => â-b;
Op soma = Ca, b) _=> a+b; _ ......... ,. . _ _ _. _ ___________ . _. .. . . . . _
Estas expressões indicam que existem dois parâmetros de entrada (a e b), sendo o
resultado calculado a partir delas. No limite, até poderia escrever-se:
int c = Ca, b) => "a*by ......... - - - - - -• - - -
.int . c = Ca, b> =>..a+bj ...... __ ............. „ .... ._ ...... . ..-„•_ .-,.. .^, .--.,. í - :
Uma declaração lambda retoma sempre um resultado e tem a seguinte sintaxe:
{parâmetros de entrada') => expressão ou { declaração em bloco }

© FCA - Editora de Informática 1 S9


C#3.5

Existem dois tipos de lambda:

Expressão lambda (lambda expression). Consiste numa expressão em que o que


se encontra no lado direito da seta => corresponde ao cálculo de um valor. Não é
necessário utilizar a paíavra-chave return. Por exemplo; x => x*x

Declaração lambda (lambda statement). É semelhante a uma expressão lambda,


mas do lado direito, encontra-se um bloco de código entre chavetas. Nesse caso, é
necessário retomar explicitamente um valor. Por exemplo:
x => { console. Wrítel_ine("{0} {l}", x, x*x) ; return x*x; }

Quando estamos perante apenas um parâmetro de entrada, é possível omitir os parêntesis,


excepto se explicitamente definirmos o tipo de dados de entrada. Deve-se definir os tipos
de dados, sempre que se torne difícil, ou mesmo impossível, ao compilador conseguir
inferir o tipo dos mesmos. É também possível definir expressões lambda sem parâmetros
de entrada. Os seguintes exemplos ilustram estes pontos.
_ - ._ — 2#x ----- 'a inferência clé~"tipós ......
(int x) => 2*x // Tipo explicitamente definido
(x, y) => x*y // vários parâmetros de entrada
:Q => Cnew RandomQ) .NextDoubleQ 5 // Sem parâmetros de entrada
.// Expressão com várias linhas e retorno explicito do resultado
; x => ;
•{
Console. WriteLine("{0} {!}", x, x*x) ; :
' return x*x; |

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; :

Este pode ser reescrito como:


:int[]Valores = í l, "2TT3, ~47"5"7í
int total = 0;

Map(valpr\=> .total.+=_ valorj_ val.Qres]).; .._.. ________ ______ ;


Como se pode ver, dentro da expressão lambda, é usada a variável total, exterior à sua
definição. Dando ainda outro exemplo, o seguinte código retorna todos os alunos que
tenham uma determinada idade:

1 6O © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

pub"lic;st;at1 c Alunos[] procurà"Poridade(Alúno[] alunos, "nnt"idade)


neturn.alunos.Where(aluno -> aluno.idade==1dade);
l '•"' "- _ . .. _ .V .
Neste caso, a expressão lambda referencia o parâmetro i dade do método.

" Um delegate funciona como um ponteiro para um método, guardando


ARETER informação sobre o método a invocar e o objecto associado.
~ Um mulíicasl delegale permite invocar diversos métodos em ordem. Neste
De legates e
Expressões caso, os métodos não podem retomar nenhum valor.
lambda ~ Um delegate declara-se como se de um método se tratasse, colocando-se a
assinatura do mesmo. O nome do delegate passa a funcionar como um tipo
de dados, permitindo criar instâncias do mesmo.
- A declaração de um delegate é feita da seguinte forma:
delegate TipoValorRetorno NomeDelegate(...);
Exemplo: delegate void Funcao(int[] tabela);
~ Para criar instâncias de um delegate, utiliza-se o operador new. Por
exemplo:
Função f = new Funcao(objecto.Método);
- Para chamar o delegale, utiliza-se a instância previamente definida. Por
exemplo: f (tabela);
- Nos nnilticast delegates, o operador += permite acrescentar novos métodos
a serem chamados e o operador -= permite retirá-los.
" Um método anónimo corresponde a um método sem nome definido no
local, onde, normalmente, se encontraria um delegate.
- Para definir um método anónimo, utiliza-se a palavra-chave delegate,
como se de uma definição de um método se tratasse. Exemplo:
delegateCInt x)
return x * x ;

- A lista de parâmetros do método anónimo, assim como o seu valor de


retorno, tem de ser compatível com a do delegate original.
- Uma expressão lambda permite definir um método anónimo sem que se
tenha de usar a palavra-chave dei egate.
" Uma expressão lambda declara-se, usando o operador =>, colocando do
lado esquerdo os parâmetros de entrada, do lado direito o resultado da
expressão. Por exemplo:
x => x*x
" Os parâmetros de entrada e saída de uma expressão lambda podem ser
explicitamente especificados. Por exemplo:
(double x) =>

int x_floor = (int)x;


return Cint)(x*x-x_floor 1 ' f x_floor);

© FCA - Editora de Informática 161


C#3.5 ^—^^==^

6.2.4 SISTEMA DE EVErsTTOS NA PLATAFORMA .N ET


O sistema de eventos da plataforma .NET é integralmente baseado no conceito de
muUicast delegates. Para definir uma classe que lança eventos, são necessárias três
coisas:

Criar um nmlticast delegale que leve dois parâmetros. O primeiro é uma


referência para o objecto que lança o evento; o segundo, um objecto da classe
System.EventArgs ou seu derivado. Este segundo parâmetro representa os
argumentos relativos ao evento, system. EventArgs representa um evento sem
informação.

Publicar uma variável do tipo do mitlticast delegate definido, mas utilizando a


palavra-chave event. Isto permite ao evento ser reconhecido na plataforma .NET.

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.

Para implementar este sistema, começamos por definir a classe computador:


1'cláss"computador "" " " '"" " "" '
; public delegate void EyentoLogin(object produtor, EventArgs args);
| public event EventoLogin OnLogin;

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)

162 © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

// Faz o logiri da" pessoa no sistema" * ~~~ " "

// Avisa todas as partes interessadas de que o login ocorreu


OnLogin(this, new EventArgsQ);

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"); ;
: } !

Finalmente, falta ligar os objectos concretos de Log ao objecto concreto de Computador.


Isso é feito de forma directa:
Log log = new Log Q;"
Computador computador = new ComputadorQ;
' computador.onLqgin+= new Computador._EventoLogi n (log. Entradasistema) ;
Isto é, neste momento, ao fazer-se:
computador.LogiriC"pmarques n , "secrèt"); ~
'computador.Lp.gin.C"nernam ".j . "topsecret") ; _ _
o objecto l og irá imprimir no ecrã, que ocorreram duas entradas 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);

public event EventoLogin OnLogin;


public void Login (string userName, string password)
{
// Faz o login da pessoa no sistema

// Avisa todas as partes interessadas de que o login ocorreu


Onuogin(this, new LoginEventArgs(userName));

}
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)

1 64 © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

Um evento representa um acontecimento num certo objecto, sendo este


ARETER acontecimento enviado a um certo conjunto de objectos receptores
interessados no mesmo.
Eventos - para uma classe publicar um evento necessita de: d] Definir um nndticast
delegate que será utilizado como protótipo de método para os
consumidores implementarem; b} Declarar uma variável de instância desse
delegate, utilizando a palavra-chave event.
Para ser um evento correcto na plataforma .NET, o nndticast delegate
deverá retornar void e ter dois argumentos: 1) o objecto que produz o
evento; 2) uma instância de system. EventArgs ou derivada, que representa
os argumentos do evento:
public delegate void
TipoEvento(object source, EventArgs args);
System. EventArgs representa um evento sem parâmetros.
Para publicar o evento, faz-se:
class Produtor {

public event TipoEvento OnTipoEvento;

sendo OnTi poEvento a variável de instância publicada.


- Para um objecto registar o seu interesse em receber um certo evento, adiciona
o seu método consumidor ao evento associado ao objecto que lança o
evento:
Produtor produtor = new produtorQ;
produtor.onTipoEvento += new
Produtor.TipoEvento(consumi dor.MetodoConsumidor);

~ 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) ;

6.2.5 UM EXEMPLO UTILIZANDO WlNDOWS FORMS

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.

© FCA - Editora de Informática 165


C#3.5

J1-_J-;3H 0 * í» -j -J-''- • £3- . »! - ÍJ ^ 3 Jf- B ",

laObm - 9 X. -Jjjacasoma/FDHHL»[Urwl^_ a**3nenÉ«r ->fti*»Uccta-. ^ * i


o ís

L MucOnutOin
p* J HiucOd

Oauí iilw tx cm*d • ddd-

Figura 6.4 — Uma aplicação simples utilizando Windows Forrns

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 ~ "" " " "" " :

private void InitializeComponentC)

this.buttonl.Location = new System.Drawing.Point(40, 86);


this.buttonl.Name = "buttonl";
this.buttonl.size = new System.Drawing.SizeC/S, 23);
this.buttonl.Tablndex = 1;
this. buttonl.Text =_"buttonl";
~ ! ^

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

partia! çlass Forml ": Form " .....

p ri vate vol d búttòniCcTfck Cbbj éct sende K, " sysTêm".' ÉveritÁrgs ej


textBoxl.Text = "it W o r k s ! " ;
J . . ____ . . _ __________.........._ .........
} . . ..... _. _ . . _ . . ._ .......... _ ......... .
Ou seja, o VisualStudio acrescentou ao código, um método chamado buttonl_c1 i ck com
o mesmo protótipo do que o delegate System . EventHandl er. O objecto buttonl publica
um evento chamado click exactamente deste tipo. Neste caso, o botão é o produtor de
eventos e o nosso código, o consumidor. No código, é ordenado a propriedade de um
outro componente - a caixa de texto - mude, passando a mostrar "It Works!".

Como se pode ver, a utilização de componentes e de ferramentas visuais que os manipulam


tornam o desenvolvimento de certo tipo de software muito mais fácil. Em certa medida, e
para certos tipos de aplicações, tudo o que o programador tem de fazer é criar a cola entre
os componentes, tratando do processamento de eventos e da alteração do estado de outros
componentes em resposta a esses eventos.

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.

Vejamos um pequeno exemplo. Imaginemos que, inicialmente, na classe Matemáti ca,


existia um método que calculava o máximo entre três valores.
p u b l i c class Matemática
pub"Kc stat-ic int Max(int x 3 int y, int z) ^>
í
'return (x>z} ? x : z;
l . i í i u , - l 1
f ,
' + t,
* - " '
Gy>z) ? y : z; - *,

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

Por uma questão de compatibilidade com o código já desenvolvido, o programador não


pode retirar o método antigo. Ao mesmo tempo, quer forçar os outros programadores que
usam esta classe a começarem a utilizar o novo método.

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;

public static int Max(int[] valores) :


int max = valoresCO]; ';

foreach Cint vai i n valores)


i f (vai > tnax)
max = vai; '
return max; ;

J.. ... . ... . .. .. . _ _._ "


Sempre que o compilador detecte que está a ser utilizado o método antigo, irá emitir um
aviso:
p\Temp\Teste: es (18 ,"5) : wárning CS0618: 'Matemática. Max Cint, irítT int) 1
lis obsol ete:. 'utilize o método. M.axCint[] valores)'

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; :

[CohditiojiãlC"DEBaG")_l " _" " ------- ^ -


public void MostraEsfatisticaC) ~
console.Wn"teLineC"*Debug* Estatistica=={0}" , estatistica) ;

" CobsoTeteJ " "


[Dl l import C"Mpdul OAUXI l i ar^dl l "J J
public extern statTc void AntigoC);

l 68 © FCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

o método MostraEstatisticaQ só é compilado se o símbolo DEBUG estiver definido8 e o


método Anti go Q é obsoleto, estando definido numa DLL externa, chamada
"ModuloAuxiliar.dll".

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 ;

Na plataforma .NET, existem imensos atributos, com as mais diferentes utilizações. Os


atributos podem aplicar-se aos diferentes elementos de um programa, tal como é indicado
na tabela 6.1.
ESPECIFICADOS j DESCRIÇÃO
assembly | 0 atributo e aplicável ao assembly corrente.
module | 0 atributo e aplicável ao módulo9 corrente.
type | 0 atributo e aplicável a uma classe ou a uma estrutura.
method | 0 atributo e aplicável a um método.
property ! | 0 atributo e aplicável a uma propriedade.
event ;| 0 atributo e aplicável a um evento.
f lei d | 0 atributo e aplicável a um campo de uma classe/estrutura.
param | 0 atributo e aplicável num parâmetro. _
return | 0 atributo e aplicável a um valor de retorno.

Tabela 6.1 — Elementos aos quais se podem aplicar atributos

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.

8 Quando discutirmos as directivas de pré-processamento, examinaremos em detalhe a definição de


símbolos. Para já, importa saber que definir um símbolo corresponde ao programador ter escrito algo
como: #def i ne DEBUG no início do ficheiro, indicando que este código é apenas para testes.
9 Um módulo corresponde a uma pequena biblioteca dentro de um assembly, compilada com a opção
/target:module.
© FCA - Editora de Informática 169
C#3.5

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.

6.3.1 ALVO DOS ATRIBUTOS


Na tabela 6.15 anteriormente apresentada, são indicados os elementos a que se pode
aplicar atributos. Os atributos são sempre indicados entre parêntesis rectos e tipicamente
antes do elemento em causa. Por exemplo, se um atributo se aplica a uma classe, então,
deverá ser indicado antes da declaração da classe:
;[õbsoTlêté] •"-"" ~" " " ""~
•publlc class classeAntiga :

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 :

• > ""

Neste exemplo, o método contaBancaríaQ retoma o número da conta bancária de um


empregado. Dado que o número de conta pode estar em diversos formatos, o programador
teve o cuidado de especificar, para outras classes que queiram consultar essa informação,
que o valor retornado por este método está no formato NIB. Para isso, definiu um atributo
chamado FormatoNis 10 . Caso não fosse indicado return:, então o atributo referir-se-ia
ao método como um todo e não ao valor de retorno.

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
;}..

Neste caso, o atributo CLSCompliant indica que o assembly obedece às regras da


Common Language Specifícation.

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 é.

6.3.2 DEFINIÇÃO DE NOVOS ATRIBUTOS


Vejamos, agora, como é que se pode definir um novo atributo.

Quando o compilador encontra um pedaço de código marcado com um atributo, começa


por verificar se o nome do atributo termina em "Attri bute" ou não. Caso não termine,
adiciona essa cadeia de caracteres ao nome do mesmo. Assim, por exemplo,
FormatoNlB e FormatoNlBAttribute querem dizer exactamente o mesmo. Em seguida,
o compilador tenta encontrar uma classe com esse nome e verifica se esta deriva de
system. Attri bute. Caso isto não aconteça, existe um erro de compilação. Finalmente, o
compilador verifica se a classe indica de que forma é que o atributo em causa deverá ser
utilizado e, verifica essa especificação contra o elemento sobre o qual encontrou o
atributo. Caso sejam compatíveis, a informação presente no atributo é adicionada ao
elemento, caso contrário, existe um erro de compilaçã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.

© FCA - Editora de Informática 1 "7 1


C#3.5

O atributo é implementado da seguinte forma:


[Ãttri' buteUsageCAttrf bútefárgets .ATT,
• Al l owMul ti pi e=t rue ,
; Inherited=false)]
public class AutorAttribute : System. Attribute

: p n" vate string NomeAutor;

; public AutorAttribute(string nome)

thi s. NomeAutor = nome;

public string Autor

get
return NomeAutor;

O atributo deriva de system.Attribute, possuindo um único construtor que leva uma


cadeia de caracteres como parâmetro. Isto quer dizer, que ao utilizar o atributo, é
obrigatório especificar o autor. Os diversos construtores especificam as formas válidas de
utilizar um certo atributo, em termos dos parâmetros que levam.

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

172 © FCA - Editara de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

CAMPOS DE ATTRIBUTETARGETS

l) .Property
Returnvalue
í[ Struct

Tabela 6.2-Campos de AttributeTargets

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]

Na verdade, o atributo Autor não é aplicável a todos os elementos apresentados na tabela.


Nomeadamente, não é aplicável a Parameter e Returnvalue. Assim, a especificação
correcta para o atributo Autor deveria ser:
;[ÁttributeUsageC
AttnbtiteTargets.All &
~(Attrí;buteTargets. parameter |AttributeTargets.Returnvalue) ,
AllowMÚlti pie=true,
inherited-false)J .. . , _ .-_ '
Por uma questão de simplicidade, foram indicados todos os campos.

O segundo elemento de AttributeUsage é AllowMult1ple=true. Isto indica que podem


existir diversos atributos Autor aplicados ao mesmo elemento de código.

Finalmente, o último elemento é: inherited=false. Este elemento indica se o atributo é


válido em classes derivadas ou não. Neste caso, como o autor de uma classe derivada não
é necessariamente o mesmo que o da classe base, o atributo não deve ser herdado.

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.

O que se passa é que AllowMultiple e inherited não fazem parte do construtor e


constituem campos opcionais.
© FCA - Editora de Informática
C#3.5

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 ]

• private string NomeAutor; !


, private string Emai l Auto r;
public AutorAttribute(string nome)
[ this.NomeAutor = nome;
this.EmailAutor = "<desconhecido>":
}
i public string Nome :

l get
j return NomeAutor; ;
1 > :
puFIic strfng ÊmaiV" " ~~ ~~ "" " ~
get
return EmailAutor;

set
{
this.EmailAutor = value;
..}.... ...._. _..

A partir deste momento, o seguinte código passa a ser válido:


iTÀu"t9rT"Pau1ò"Márqu"es"J Émail="pinã~rques@dei .ucVpt")] "
public void NovoMetodoQ

6.3.3 OBTENÇÃO DE ATRIBUTOS EM TEMPO DE EXECUÇÃO


Para obter os atributos associados a uma certa classe, em tempo de execução, utiliza-se
reflexão. Esta operação é muito simples. Consideremos, novamente, a classe
csharp_cursocompl eto, mas com a informação sobre os autores mais completa:

174 © FCA - Editara de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

i [Autor ("Paul o" Marqués'VEmaiT= Il pmarques@déi .uc.pt' 1 )]


í[AutorC"Hernam" Pedroso", Email="hernani©criti calsoftware.com 11 )]
! dass csharp_cursocompleto

j}..".' . _ ..,. .._ .


Para descobrir quais os autores da mesma (se algum foi especificado), utiliza-se o
operador typeof :
,'públic class ÀtributõsRefTexáb
public static void MainO
System.Reflection.Memberlnfo info;
_
__ qbjeçt[] atributps__= jlnfo.GetCustpmAtt/i^ _______
foreach (object atributo in atributos)
i AutorAttribute autor = atributo as AutorAttribute;
i f (autor != null)
Console. WriteLine("Autor: {0} / Email : {!}" ,
autor. Nome, autor. Email) ; :

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.

O exemplo completo é mostrado na listagem 6.3.

/*
* 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

publlc strlng Nome

get
return NomeAutor;

publlc strlng Email


get
return EmallAutor;

s et
thls. EtnailAutor = value;

[Auto r C" Paul o Marques" , Email="pmarques@de1 .uc.pt")]


[Autor("Hernani Pedroso", Emai l ="hernan1@cri ti cal software.com")]
class CSharp_CursoCompleto

}
publlc class ExamploCap6_3
publlc statlc vold MainC)
System . Ref l ectl on . Memberlnf o 1 nf o ;

info = typeof (csharp_çursoCompleto) ;


object[] atributos = Info.GetCustomAttrlbutes(true) ;

foreach (object atributo In atributos)

AutorAttribute autor = atributo as AutorAttrlbute;


1f (autor 1 = null)
Console. Wr1teLlne("Autor: {0} / Email: {!}",
auto r . Nome , auto r . Emai l ) ;

Listagem 6.3 — Programa que ilustra o uso de atributos (ExemploCap6_3.cs)

176 © PCA - Editora de Informática


PROGRAMAÇÃO BASEADA EM COMPONENTES

Os atributos permitem adicionar metainformação ao código existente num


programa. Essa metainformação é tipicamente extraída utilizando reflexão.
Atributos Um atributo marca uma declaração no código, como unia classe, uma
interface ou um método. A sua declaração faz-se tipicamente entre parêntesis
rectos antes do elemento a marcar. Exemplo:
[Obsolete]
public void MetodoQ {, ... }
Caso exista ambiguidade no elemento ao qual se refere o atributo, é
necessário indicar explicitamente o elemento alvo ao qual este se refere.
Exemplo:
[assembly: CLSComplaint(true)]
As palavras-chave dos alvos válidos são: assembly, module, type, method,
property, event, fiel d, param e return.
Os atributos podem levar parâmetros. Os parâmetros correspondem aos
construtores definidos para o atributo em causa. Exemplo:
[obsolete("utilize o método XptiQ"]
public void XptoQ { ... }
É também possível utilizar parâmetros opcionais nos atributos. Estes são
usados sob a forma de atribuição, após os obrigatórios (construtor do
atributo) e correspondem às propriedades públicas do atributo. Exemplo:
[AutorC"Paul o Marques", Email="pmarques@dei.uc.pt")]
class Xpto { ... }
Para definir um novo atributo, cria-se uma classe derivada de system.
.Attribute. Essa classe encapsula a metainformação relevante para o
atributo em causa. Os construtores da classe indicam as formas possíveis
para os parâmetros obrigatórios do atributo. As propriedades públicas
especificam os parâmetros opcionais.
1 A classe definida tem de ser marcada com um System.AttributeUsage.
Este possui apenas um parâmetro obrigatório que especifica quais os alvos
sobre os quais pode ser aplicado. Os alvos possíveis são: Ali, Assembly,
class, constructor, Delegate, Enum, Event, Fiel d, interface, Method,
Modul e, Parameter, property, ReturnVal ue e Struct.
1 system. Attri buteusage possui ainda dois parâmetros opcionais importantes:
#) A l l o w M u l t i p l e indica que podem existir várias instâncias do mesmo
atributo associadas a um alvo; b} inherited indica que a instância do
atributo é herdada por classes derivadas.
Para obter os atributos definidos para uma certa classe, utiliza-se reflexão,
nomeadamente o operador typeof e o método GetcustomAttributesO da
classe Type.

© FCA - Editora de Informática 177


7« TÓPICOS AVANÇADOS
Ao longo dos últimos seis capítulos, tentámos descrever, de uma forma clara, quais as
principais funcionalidades da linguagem C#. Tratou-se de uma viagem longa, começando
pelos aspectos básicos sobre tipos elementares de dados, cobrindo o suporte à
programação orientada aos objectos, programação baseada em componentes e também
tratamento de erros.

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.

7.1 TIPOS ANÓNIMOS


Os tipos anónimos (ou classes anónimas) permitem-nos fazer o encapsulamento de um
conjunto de propriedades num objecto, sem termos de definir previamente o seu tipo. O
objecto, assim como a sua classe correspondente, é gerado no momento.

A criação de um tipo anónimo, à semelhança de um objecto normal, é feita usando o


operador new. O exemplo seguinte mostra a criação de um tipo anónimo com três
propriedades (Nome, Apelido e idade):
v a r pessoa = ' " "- - - - - ^
new MT Nome = "Maria", Apelido = "carvalho", idade = 19 }; v
t . ' • • • - ,
Consote.WriteLineC"Nome = {0}, pessoa.Nome);
Console.WriteLineC"Apelido = {O}, pessoa.Apelido);
Console.WriteLlneC!ldade_. _=..-£p.}.J..p_essoa,..ldade3 ; . ._ _
Como se pode verificar, após a criação do objecto, as suas propriedades ficam
imediatamente disponíveis. O nome do tipo anónimo (classe) é gerado automaticamente
pelo compilador. Neste caso, é também feita inferência automática do tipo de cada uma
das propriedades especificadas. Para tal, é usada a palavra-chave var, que iremos analisar
muito brevemente.

É 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.

© FCA - Editora de Informática 179


C#3.5

7.2 EXPRESSÕES DE CONSULTA


As expressões de consulta são uma funcionalidade que foi introduzida no C# 3.0.
Permitem, de uma forma muito simples, realizar operações de pesquisa e manipulação de
dados, sem que o programador tenha de explicitamente indicar corno essas operações
devem ser feitas. O programador apenas é responsável por declarar quais os dados que
quer e qual a fonte dos mesmos. Vejamos um pequeno exemplo:
•iht[] vKlorès ^{ l, ~2, 3, 4 7 5 , 6 7 7 , 87~9, 10"};

írfpgffii a itffii <ífé!U®(P@§

foreach (Int valor In numerospares)


çpnsole.writeX"{0} " j valor);
O pedaço de código, assinalado a mais escuro, constitui uma expressão de consulta.
Informalmente, a operação que está a ser realizada pode ser lida como: "para todos os
valores n presentes na tabela valores (f rom n i n valores), filtra os que são divisíveis
por dois (where n % 2 == 0), apresentando como resultado esses valores (sei ect n). O
resultado da execução deste código é:
2 4 6 8 1 0 . 7 7 _ " :~ _;;:;; . 7 :.:;;. ; .... 7 7 7 7 7 " "
Vejamos mais um exemplo. Imaginemos que queremos seleccionar todas as palavras de
uma tabela, que tenham cinco ou mais letras, mostrando, em maiúsculas, as palavras
correspondentes. O resultado deverá ser ordenado alfabeticamente. Tal poderá ser
conseguido com:
stringET palavras =""
{ "casa , "carro", "compridas", "rato", "aproximadamente", "desejo" };

foreach (string palavra i n palavrasCompridas)


:, Console.Wri.teC {0}. ", palavra).; _
O resultado da execução será:
APROXIMADAMENTE CARRO "COMPRIDAS DESEJO' ' _" . 7__ 7 7 7
Se quiséssemos ordenar pelo tamanho de palavra, bastaria mudar a expressão para:
var palavràscompridãs =
.. -.f-rpm pai i n palavras
where pai.Length >= 5
| orderby pai .Length " "7~ "" '
select pai .ToÚpperOj ""'" "" "
resultado em:
CARRO ptSÊJ07COMPRÍDAS APRÕXIMÃbÁMÉNTE '" " ""

18O © FCA - Editora de Informática


TÓPICOS AVANÇADOS

Como se pode ver, as expressões de consulta são extremamente poderosas. Estas


expressões podem não só ser aplicadas a tabelas e objectos, como a colecções e
enumerações. Mais importante ainda, permitem realizar operações em bases de dados
SQL, manipular ficheiros XML, ficheiros XSD, entre outros. Na verdade, são a pedra
basilar da LINQ (Language Integrated Queiy). Usando a LINQ e um conjunto correcto
de adaptadores, é possível manipular, de forma uniforme, diferentes tipos de dados em
.NET.

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.

O próximo quadro apresenta as palavras-chave usadas em expressões de consulta:


EXPRESSÃO | DESCRIÇÃO
Especifica uma fonte de dados e uma variável local que representa
from
cada elemento da colecção.
Especifica critérios de restrição da consulta, seleccionando resultados
where
que satisfaçam uma expressão lógica.
| select 'l Especifica os valores que devem resultar da^pesquisa.
Agrupa os resultados de uma consulta de acordo com uma
group
determinada chave.
Fornece um identificador que pode servir como referência aos
i nto :
resultados de uma cláusula join, group ou select.
orderby | Ordena de forma ascendente ou descendente os resultados.
Combina duas fontes de dados, usando um critério de
join
correspondência entre eles (por exemplo, igualdadejte dois campos).
Introduz uma variável local para armazenar os resultados de uma
let
sub-consulta.

Tabela 7.1 — Palavras-chave que podem ser usadas numa expressão de consulta

As expressões de consulta irão ser vistas pormenorizadamente no capítulo 11, quando se


discutirá a linguagem LINQ.

7.3 INFERÊNCIA AUTOMÁTICA DE TIPOS


7.3. l INFERÊNCIA EM VARIÁVEIS LOCAIS
A palavra-chave var instrui o compilador a inferir (tirar uma conclusão) sobre o tipo da
variável presente na expressão do lado direito da declaração. Desta forma, toma-se
© FCA - Editora de Informática 1 81
C#3.5

desnecessário declarar o tipo de variável, deíxando-se ao compilador, o trabalho de


descobrir qual o tipo correcto a usar. As variáveis locais podem ser declaradas, usando a
inferência de tipos em vez de explicitar-se o tipo das mesmas. Os seguintes exemplos
mostram como declarar variáveis locais usando var:
vãr~ 1 = 5"; " ~~//~comp11adã~comb Tht
:var s = "olá"; // compilada como strtng ;
,var d - _ l : 0 j _ //.Compilada como dpublei
Estes exemplos são equivalentes ao seguinte código:
i*nt i = 5; "~"' " " ;~"; ~ / •"
jStrlng s - ''Olã"; . . , :, • :
idouble d = .1:P;,_ : _ . „ ;.„;.:.: .".
A palavra-chave var pode ser utilizada nos seguintes contextos:

Sobre variáveis locais (variáveis declaradas no âmbito de uni método);

Na inicialização das variáveis em for, foreach e uslng.

Vejamos um exemplo prático da utilização de inferência de tipos, na inicialização da


declaração foreach:
var"números'="riew[] í O, T, "2, 3, 4,""5"}; V -•-•.-"" '"""""
\foreach_ Çyãr~n In'números]\"_/"'!"„
Console.WriteLlne("{0}", n) ;

Neste caso, serão impressos os inteiros de O a 5 sem que o seu tipo tivesse de ser
declarado.

O declarador de inferência de tipos (var) está sujeito às seguintes restrições:

O declarador tem que incluir um inicializador, logo, a variável local tem de ser
declarada e inicializada na mesma expressão;

O inicializador não pode ser do tipo nul l;

O declarador não pode ser usado nos campos da classe;

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;

Múltiplas declarações de variáveis não podem ser inicializadas na mesma


expressão;

182 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

O inicializador tem de ser uma expressão. O inicializador não pode ser um


objecto ou uma colecção por si própria mas pode ser uma nova instância,
utilizado a expressão new, de um objecto ou de uma colecção.

Os exemplos seguintes mostram algumas declarações incorrectas:


'dass Exemplo ' "

; // Erro, var não pode ser usada em campos de dados


; private var 1 = 5 ;
// Erro, var não pode ser usado como um valor de retorno
publlc var Função ("i nt x, int y) { . . . }
// Erro, var não pode ser usado com tipo de parâmetro
public vold MetodoCyar x, var y) { ... }
public vold Teste C)
// Erro, não possui a Inlclallzação
var x;
// Erro, o Inicializador não pode ser do tipo null
var z = null;
// Erro, a variável declarada não podem ser usada simultaneamente
var c = c + l

7.3.2 INFERÊNCIA EM TABELAS


A inferência de tipos também é muito utilizada quando é necessário o compilador tirar
conclusões sobre cada elemento presente numa tabela. Neste caso, é obrigatório que no
momento da inicialização, todos os elementos sejam do mesmo tipo. Os seguintes
exemplos mostram como declarar tabelas, usando inferência de tipos:
,var numeros_1nte1ros =í"n"ewD í"&","!, 2"i 3, 4, 5 }; //"*int[]
var numeros_fracc1on = new[] { l, 1.5, 2, 2.5 }; // double[]
'var palavras = new[] { "um", null, "dois" }; // strlngC] :
var nomes = new[] l
. new[] {"Ana", "Paula", "Teresa", "Maria"},
new[] {"Pedro", "Mário", "Rodrigo"}
;};_ _ . _ _, ._. i
É de referir, que a variável nomes é inicializada usando uma tabela de uma única
dimensão. Tabelas multidimensionais não são suportadas.

Os seguintes exemplos mostram declarações incorrectas de inferência de tipos em tabelas:


'•// Erro, utilização" de uma expressão Invalida
var tabela - {l, 2 , 3 } ; \/ Erro,

© FCA - Editora de Informática 183


C#3.5

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.

Como já foi referido, é obrigatório que, no momento da inicialização, todos os elementos


sejam do mesmo tipo. Em alternativa, poderemos combinar expressões de inferência de
tipos com iniciadores de objectos, criando estruturas de dados de tipos anónimos. Por
exemplo:
class" ínférericiaTípbsÃnõnimds "~ ~ ..... ........ i
•{ ' "• " !
static void Mai-n(string[] args) ;
-- "

new { Nome = "Pedro Martins", idade = 15 } ,


new { Nome = "Ana Cristina", idade = 16 },
new { Nome = "João Carvalho", idade = 14 }
____ _ _j JL._ . ____ .„ . _ ..... . ..._____......_______________________________. _________
foreach <var pessoa i n empregados) !
Console.WriteLine("Nome: {0}", pessoa. Nome) ;
Console. Wri1:eLineC"ldade: {0}", pessoa. idade) ; '

<} .............. ". ..:_; .. . ............... _____ : - .'.;, • ....- . . . . _ ........ _--\e tipo de

estruturas de dados simples, usando uma sintaxe concisa. Um aspecto interessante,


ilustrado neste programa, é o ser possível definir tipos de dados anónimos que englobam
o uso explícito de propriedades. Ou seja, ao escrever-se:
y_af ."pessoa "="_ new .{ 'NWé7=_//P_è_drp^ ...... ; ; "'"' V " . " " ' " 1
torna-se possível escrever expressões como:
^ "pessoa..

7.3.3 INFERÊNCIA EM EXPRESSÕES DE CONSULTA


Em muitos casos, a utilização de inferência de tipos é meramente opcional, sendo apenas
usada por conveniência de sintaxe. No entanto, a sua utilização é obrigatória quando
estamos perante um tipo anónimo. Encontramos esse cenário frequentemente em
expressões de consulta, em que o nome do tipo anónimo só é conhecido pelo compilador.

Consideremos o seguinte programa:

1 84 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

/*
* 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

private static void PesquisarAluno(int id)


var alunos - carregaAlunosQ ;
var alunoquery =
from aluno i n alunos
where aluno.Id == id
select new { aluno.Nome, aluno.Apelido, aluno.idade };

foreach (var aluno i n alunoQuery)

console.WriteLine("Nome = {0}", ai uno.Nome);


console.WriteLine("Apelido = {0}", aluno.Apelido);
Console.Writel_ine("Idade = {0}", aluno.Idade);
}

private static Aluno[] CarregaAlunosC)

var alunos = new Aluno[]

new Aluno {ld=l, Nome= Maria , Apelido="Carvalno", ldade=25


new Aluno {ld=2, Nome= 'Pedro 11 : Apeli do="Marti ns", Idade=23
new Aluno {Id=3, Nome= 'Ana", Apelido="Ferrei rã", Idade=20
new Aluno {id=4, Nome= 'Maria", Apelido="Cardoso", ldade=25
new Aluno {id=5, Nome= "Doao", Apelido="Abreu",

return alunos;

static void Main(string[] args)

PesquisarAluno(l);
PesquisarAluno(2);

Listagem 7.1 — Inferência automática em expressões de consulta (ExemploCap7__l.cs)

© FCA - Editora de Informática 185


C#3.5

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 }).

Neste caso, não só a variável de consulta alunoQuery tem de ser declarada


implicitamente (vár) como, mais tarde, a expressão foreach a terá de usar.

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.

7.3.4 INFERÊNCIA EM EXPRESSÕES LAMBDA


Por norma, ao escrever-se expressões lambda não é necessário especificar o tipo de dados
dos parâmetros de entrada. O compilador pode inferir quais os tipos de dados a usar,
baseando-se no corpo da expressão em causa. No exemplo seguinte, ao ser efectuada uma
consulta sobre uma tabela, o compilador descobre correctamente o tipo de dados. A
execução do código;
var ãl unos = 'new "Al uno
uno[]
[]'
new Aluno „ Nome= "Maria", Apelido= "Carvalho", Idade=25 },
new Aluno {Id=2, Nome= 'Pedro" , Apelido= "Martins", Idade=23 },
new Aluno {Id=3, Nome= 'Ana" , Apelido= "Ferrei rã" , Idade=20 },
new Aluno {Id=4, Nome= "Maria", Apelido= 'Cardoso" , Idade=25 },
new Aluno {Id=5, Nome= "3oao", Apelido= 'Abreu" , Idade=25 }
i};

186 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

var a l u n o s M a n a =

jforeach (var rés i n alunosMana)


l çpnso1e.WriteLlne_C{0}.
Fará com que seja impresso:
JMari a carvalho
JMarla Cardoso______.......___________
Note-se que o método whereC) permite filtrar os alunos necessários, usando uma
expressão lambda para tal. O compilador é capaz de inferir que esta expressão lambda
leva um parâmetro de entrada (aluno) que tem de ser idêntico ao tipo subjacente a
ai unos (isto é, a classe Al uno).
" É possível definir classes anónimas, que não possuem um nome específico.
ARKTER Para isso, utiliza-se a palavra-chave new, colando entre chavetas, os campos
da classe, inicializados directamente. Por exemplo:
Tipos Anónimos,
Expressões de
var pessoa = new { Nome="Pau"lo", Apel1do="Marques" };
Consulta e ~ A palavra-chave var é utilizada para criar variáveis sem declarar
Inferência explicitamente o seu tipo de dados. O compilador é responsável por inferir
o tipo correcto, de acordo com a expressão que se encontra do lado direito
do sinal de igual.
" Ao declarar-se uma variável usando var, a mesma tem de ser sempre
inicializada. Para a declaração ser válida, o compilador tem de ser capaz de
determinar o seu tipo de dados estaticamente. (Por exemplo, null não é um
valor que possa ser usado na inicialização.)
" As expressões de consulta permitem, de forma declarativa, tratar dados que
estejam presentes em objectos, colecções, bases-de-dados e ficheiros XML.
Utilizam as palavras-chave from, where, select, group, into, orderby,
join e let.

7.4 ENUMERADORES E ITERADORES


Como já vimos, o operador foreach permite, de uma forma fácil, percorrer o conjunto de
elementos pertencentes a uma estrutura de dados, tratando-os no corpo do mesmo. Nesta
secção, iremos ver como implementar tipos de dados que podem ser utilizados
transparentemente com foreach.

7.4.1 AlNTTERFACE IENUMERABLE


Existe uma interface chamada lEnumerable, pertencente ao espaço de nomes System.
.Collections, que merece um destaque especial. Esta interface permite que os objectos
que a implementem sejam utilizados com o operador foreach. Vejamos de que forma é
que isto é conseguido.

Consideremos a classe Tabel aoi nami ca da listagem seguinte:

© FCA - Editora de Informática 187


C#3.5

* Programa que implementa uma tabela dinâmica simples.


*/
using System;
class TabelaDinamica
// Tamanho inicial da tabela
private const int TAMANHO_INICIAL = 10;
// Número de elementos na tabela
private int Total Elementos;
// A tabela onde são armazenados os elementos
private object[] Tabela;
/* Constrói uma nova tabela */
public TabelaDinamicaQ
Tabela = new object[TAMANHO_INIC!AL];
Total Elementos = 0;

/* Adiciona um certo elemento à tabela */


public void AdicionaElemento(object elemento)
i f (Total Elementos == Tabela.Length)

objectC] novaTabela = new object[Tabela.Length*2];


Array.Copy(Tabela, O, novaTabela, O, Tabela.Length);
Tabela = novaTabela;
}
Tabela [Total El ementos-H-] = elemento;

/* Obtém o elemento que se encontra numa certa posição */


public object ObtemElemento(int posição)
return Tabela[posicao];

/* Número total de elementos na tabela -/


public int NúmeroElementos
get
{
return Total Elementos;
}

class ExemploCap7_2
{
static void Main(string[] args)
TabelaDinamica tab = new TabelaDinamicaQ ;

for (int i=0; i<20;

188 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

tab.Adici onaEI emento(1);


for (int 1=0; i<tab.NúmeroElementos;
console.Wr-iteLine("{0}", tab.obtemElemento(i));

Listagem 7.2 — Uma tabela dinâmica simples (ExemploCap7_2.cs)

Esta classe implementa o que é chamado de "tabela dinâmica". E possível adicionar


sucessivamente elementos à tabela e, também, verificar quais os elementos que estão
presentes. Ao contrário do que acontece com as tabelas normais, quando se esgota o
espaço interno da tabela dinâmica, esta cresce de tamanho. Isso é conseguido à custa do
seguinte código:
"public void Adi ciònaÉTemèntb(ob j ect elémentòy
i f (Total Elementos == Tabela.Length)
object[] novaTabela = new object[Tabela.Length*2];
Array.Copy(Tabela, O, novaTabela, O, Tabela.Length);
Tabela = novaTabela; i

Tabela [Total El ementos-H-] = elemento;


1.. . „ „... ... . .-
Antes de um elemento ser adicionado à tabela interna, verifica-se se ainda existe espaço
na mesma. Caso não exista, é criada uma nova tabela com o dobro do espaço e os
elementos existentes são copiados para a mesma. Finalmente, a referência da tabela
antiga é colocada a apontar para a nova tabela. Só depois de todas estas operações
estarem concluídas é que o elemento é realmente adicionado à tabela interna.

Se olharmos para a forma como a tabela dinâmica é exercitada no MainQ do programa,


verificamos que existem as seguintes linhas de código, que iteram ao longo de todos os
elementos da tabela:
: for (int 1=0; T<táb.NúmèWETerhefitos;
Cpnsol e . WriteLine C!.{OJ:"^ tab^pbtemEl emento.(;i)J)j__
No entanto, seria muito mais simpático poder escrever:
fforeách" (ob j ect" elemento ~in~tat>) ~
' Qonsol.e..WrlteLlne_C"_{Q}" ,_ ej.eme_n_to)j _____
Isto é, uma iteração simples ao longo da estrutura de dados. A interface lEnumerable
permite exactamente isso. Caso uma classe armazene elementos, sendo possível percorrer
os mesmos, enurnerando-os, então, é possível declarar a classe como implementado
lEnumerabl e, passando a ser possível utilizar o f oreach com objectos desta.

lEnumerabl e é uma interface declarada da seguinte forma:


;p'ub*líc interface lÉnumerablé " * :

© FCA - Editora de Informática l 89


C#3.5

; //" "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á:

,class TabelaDi nami caEnumerator : System.Co11ecti ons.lEnumerator


; // A tabela dinâmica à qual este enumerador se refere
> private TabelaDinamica Tabela;
1 // o elemento apontado neste momento
. private irvt Elemento;

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;

public void ResetQ


Elemento = -1;

1 public bool MoveNextQ


if (Elemento = Tabela.NúmeroElementos-1)
return false;
++Elemento;

return true;

public object current

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

Finalmente, a propriedade current devera lançar uma invalidoperationException


caso o enumerador não esteja numa posição válida:
fpúbTfc 'ò6~jéct"current
; qet [
1f (Elemento<0 [ I Element9>=Tabela.NÚmeroElementes)
; throw new invaíldOperatlonExceptlonQ ;
• return Tabela.obtemElemento(Elemento); '
: } \a implementação, e

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' "~ "" •

; // Retorna um enumerador para esta colecção


public lEnumerator GetEnumeratorQ ;
return new TabelaDlnamlcaEnumerator(thls);
}

A partir deste momento, passa a ser possível escrever código como:


; í Èh ume rafo r "i t ~= tabVGetÈnumèratorO ; " ~ "\e (it.M

.Console.WriteLlne.(''{0} IIJ _.it..Current);


ou, mesmo,
Tòreàch "CõbJect elemento Trrtab}
Console.Wn"teLTneC"{0>", elementp}.;

A listagem 7.3 apresenta o código completo deste exemplo.

/*
* Programa que Implementa uma tabela dinâmica simples,
* e o suporte para enumeradores da mesma.
*/
uslng System;
using System.Collections;

l 92 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

class TabelaDinanrica : System. Col l ecti ons. lEnutnerab lê

// Tamanho inicial da tabela


pri vate const int TAMANHO_INICIAL = 10;

// Número de elementos na tabela


private int Total Elementos;

// A tabela onde são armazenados os elementos


private object[] Tabela;

/* constrói uma nova tabela */


public TabelaDinamicaQ

Tabela = new object[TAMANHO_lNlC!AL] ;


Total Elementos = 0;
}
/* Adiciona um certo elemento à tabela */
public void AdicionaElemento(object elemento)

i f (Total El ementos == Tabela.Length)


object[] novaTabela = new obiect[Tabela. Length*2] ;
Array.Copy(Tabela, O, novaTabela, O, Tabela.Length);
Tabela = novaTabela;
}
Tabela[TotalElementos++] = elemento;
}
/* Obtém o elemento que se encontra numa certa posição */
public object obtemElemento(int posição)

return Tabela[posicao] ;
}
/* Número total de elementos na tabela */
public int NúmeroElementos
get
return Total Elementos;

/* Retorna um enumerador para esta colecção */


public lEnumerator GetEnumeratorQ
return new TabelaDinamicaEnumerator(this) ;
}

class TabelaDinami caEnumerator : System. Col l ecti ons. lEnumerator

// A tabela dinâmica à qual este enumerador se refere


private TabelaDinami ca Tabela;
// O elemento apontado neste momento
private int Elemento; _

© FCA - Editora de Informática 193


C#3.5

public TabelaDinamicaEnumerator(TabelaDinamica tabela)

thls. Tabela = tabela;


ResetQ ;
}
public void ResetC)
Elemento - -1;
l
public bool MoveNextC)
if (Elemento == Tabela. NúmeroElementos-1)
return false;
-H-El emento ;
return true;
}
public object Current
get
i f (Elemento<0 || Elemento>=Tabela.NúmeroElementos)
throw new InvalidOperationExceptiçnÇ
"Enumerador incorrectamente posicionado");
return Tabela. ObtemElemento(Elemento) ;

class ExemploCap7_3

static void Main(string[] args)


Tabel aoi nami ca tab = new Tabel aDi nami ca() ;
for (int i=0; i<20; Í-H-)
tab . Adi ci onaEl emento (i ) ;
lEnumerator it = tab.GetEnumerator() ;
whi l e (i t . MoveNext C) )
Console.WriteLine("{0}" , i t. Current) ;
f~n
v_ \j 1ncrtlo
10 u i c . Ufpn
w i i *f"o l "íi [no
L, c i— f""—
ic ^ __
——. —.—_—
_—_—
__— _—,. ______________________
— -- — — ._.__._.___,_ ^__ ""l
j »^

foreach (object elemento in tab)


Console.writeLineO^O}", elemento) ;

Listagem 7.3 — Uma tabela dinâmica com suporte a enumeradores (ExemploCap7_J3.cs)

© FCA - Editora de Informática


TÓPICOS AVANÇADOS

ARETER Para uma classe representar um conjunto de objectos iteráveis, deverá


implementar a interface lEnumerabl e:
... e
lEnumerable £public interface System. Col l ecti ons. lEnumerabl e
lEnumerator lEnumerator GetEnumeratorC);

lEnumerabl e requer que a classe implemente um método que permita obter


um enumerador para a colecção em causa. Um enumerador é um objecto
que implementa lEnumerator.
lEnumerator está declarado da seguinte forma:
public interface System.Collections.lEnumerator
bool MoveNextO;
void 'ResetQ;
object current { get; >

MoveNextO avança para o próximo elemento, retornando true. Caso não


seja possível, retorna f ai se.
Rés et O coloca o enumerador antes do primeiro elemento da colecção.
current é uma propriedade que obtém o elemento presentemente apontado.
Deve ser lançada uma invalidoperationException, caso current esteja a
apontar para uma posição inválida ou, ao chamar MoveNextO , a colecção
tenha sido modificada entretanto.
Ao implementar lEnumerabl e, os objectos da classe em causa passam a
poder ser utilizados dentro do operador f oreach.

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.

Um iterador corresponde à implementação de um método que retorna umas das seguintes


interfaces:

• System.Collecti ons.lenumerator;

System.Collecti ons.lenumerable.

Ao contrário do que acontecia nas anteriores versões da plataforma, o programador não


necessita de construir manualmente os objectos que representam os enumeradores: tal é
feito pelo compilador. Um iterador é um bloco de código que "produz" uma sequência
ordenada de valores, sendo a mesma automaticamente encapsulada no enumerador
© FCA - Editora de Informática 195
C#3.5

correspondente. Um iterador distingue-se de um método normal pela presença de uma das


seguintes palavras-chave:

• "yisld return", que indica o próximo valor a produzir;

"yi el d break", que indica que a iteração chegou ao fim.

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"

public lEnumerator GetEnumeratorQ l


return new TabelaDinamicaEnumerator(this) ; i
} i

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 compilador encarrega-se de criar o objecto que implementa lEnumerator,


armazenando toda a informação associada ao estado interno da chamada ao método.
Sempre que ocorre um "yield return Tabela[i] ;", o valor presente em Tabela[i] é
retornado. Ao mesmo tempo, o compilador gera o código necessário para guardar as
variáveis locais do método e também as suas variáveis internas2. Assim, da próxima vez
que o mesmo for chamado, a execução pode prosseguir na linha seguinte à do yield
return. É extremamente importante perceber que o método não irá ser executado de
início, mas irá prosseguir a sua execução do ponto onde se encontrava3. Assim, quando o
código:
iriamica tab~ = hew TabelaDTn"amicaO';" _ y 7 1. " . ..".. "..

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

.foreach (int valor n n tab)


Conso] e. Wri teLi ne(vai o r);

é chamado, o que acontece é que GetEnumeratorQ de TabelaDinamica também o é


implicitamente. No entanto, quando o compilador encontra "yi e l d return" nesse método,
gera automaticamente o enumerador necessário. Sempre que o enumerador é avançado, a
rotina GetEnumeratorQ é novamente chamada, a partir do ponto era que se encontrava,
até produzir um novo valor (yield return) ou até indicar explicitamente o fim da
iteração ("yield break;"). O compilador encarrega-se de transmitir os valores
produzidos automaticamente, ao enumerador que está a ser utilizado pelo operador
foreach.

A listagem seguinte apresenta o exemplo completo de Tabel aoi nami ca.


7*
* Programa que Implementa uma tabela dinâmica simples,
* e o suporte para enumeradores da mesma usando iteradores.
*/
using System;
using System.Collections;

class Tabelaoinamica : System.Collections.IEnumerable

// Tamanho inicial da tabela


pri vate const int TAMANHO_INICIAL = 10;
// Número de elementos na tabela
private int Total Elementos;

// A tabela onde são armazenados os elementos


private object[] Tabela;

/* constrói uma nova tabela */


public Tabel aDinami ca()

Tabela = new object[TAMANHO_INIClAL];


Total Elementos - 0;
>
/* Adiciona um certo elemento à tabela */
public void AdicionaElemento(object elemento)
i f (Total Elementos == Tabela.Length)

object[] novaTabela = new object[Tabela.Length * 2 ] ;


Array.copy(Tabela, O, novaTabela, O, Tabela.Length);
Tabela = novaTabela;
}
Tabela[Total Elementos++l = elemento;
}
/* Obtém o elemento que se encontra numa certa posição V
public object obtemElemento(int posição)

© FCA - Editora de Informática 197


C#3.5

return Tabela[posicão];

/* Número total de elementos na tabela */


public int NúmeroElementos
get
return Total Elementos;

}
/* 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);

Listagem 7.4 —Tabela dinâmica usando iteradores (ExemploCap7_4.cs)

7.4.3 ENUMERADORES GENÉRICOS


Uma classe que implemente a interface lEnumerable e, consequentemente, o método
GetEnumeratorQ, torna-se enumerável como um todo. Ou seja, a classe é vista como
sendo um tipo de dados que pode ser percorrido. No entanto, em muitas circunstâncias,
seria desejável poder-percorrê-la de várias formas diferentes. Por exemplo, uma árvore
binária pode ser percorrida pelo menos de três formas "normais": pré-ordem, em-ordem e
pós-ordem. Uma tabela dinâmica pode ser percorrida do primeiro elemento para o último
ou do último para o primeiro. A mesma coisa acontece com outras estruturas de dados.

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.

1 9 8 , , © FCA - Editora d e Informática


TÓPICOS AVANÇADOS

Regressando ao exemplo da tabela dinâmica, é perfeitamente possível ter as seguintes


propriedades:
iclasse TabéláDfnámlca :" ÍEriutnerãbTé
K :
V* RetornaTúnT ênumeTaclb r "para' esta" colecção"" prTncipib-r->fiin */ "
public lEnumerable DoPrincipioParaFim
g et
for (int i = 0; i < Total Elementos; i++)
yield return Tabela [i];

"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;

public static void Main(string[] args)

foreach (int quadrado in Quadradosperfeitos(l, 100))


Console.WriteLine(quadrado);
console.WriteLineC) ;

Listagem 7.5 — Uso genérico de métodos enumeradores (ExemploCap7_5.cs)

Neste caso, o método QuadradosperfeitosQ retorna um objecto lEnumerable que


representa os quadrados perfeitos, números cuja raiz quadrada é um valor inteiro, entre
dois valores que lhe são passados como parâmetro. Executando:
;fdréach~"Ci1hf" quadrado in QuadradosPérf eitos Cl ,
L ....Qpn_sp,l_e.WriteC"{0} _", quadrado) ;_
obtém-se:

;1'OT6~25 36 49 64 ~8T 100

~ Ura Uerador é um método que gera uma sequência ordenada de valores, de


ARETER acordo com um certo critério.
Itera do rés ~ Os iteradores distinguem-se dos métodos normais pelo uso de "yield
return" e "yield break", no corpo do método. O primeiro gera o próximo
valor da sequência; o segundo indica que a iteração está completa.
~ Os iteradores têm de retornar obrigatoriamente;
System.collections.lEnumerator ou System.Collections.renumerable.

" 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);

[i n t vai o r = C i n t) t ab. Qb t em El eme n t o CO) : ?-'"-: :t 'f::'-' #?.;*£ _|


Existem diversas desvantagens nesta abordagem. Em primeiro lugar, é entediante para o
programador ter de fazer constantemente estas conversões quando, à partida, o propósito
da tabela é claro: armazenar inteiros. Seria muito mais interessante não ter de o fazer.
Simultaneamente, as conversões explícitas implicam algum overhead em termos de
execução. Neste caso, o problema é ainda mais grave, pois o uso de um tipo elementar
(i nt) irá implicar que exista boxing e unboxing de valores, com a respectiva penalidade
em termos de tempo de execução e de espaço de armazenamento. Finalmente, pode
sempre dar-se o caso de haver um erro de programação e ser introduzido um tipo não
inteiro na tabela. Ao fazer-se a conversão para int, tal resultaria numa excepção. Este
erro só seria detectado em tempo de execução, pois quando um programador faz uma
conversão explícita, assume a responsabilidade pela correcção dos tipos de dados em
causa.

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 _

interfaces, delegates e métodos com os tipos de dados que utilizam e manipulam.


Vejamos o exemplo Tabel aoi natiri ca.

As instâncias de TabelaDInamica irão armazenar um certo tipo de dados. Por exemplo,


irão armazenar inteiros (int), números reais (double) ou, por exemplo, empregados
(Empregado). Assim, a classe Tabel aoinami ca deverá ser parametrizada pelo tipo de
dados T, que ira armazenar e manipular:
i clãs s" TábelaDi riàmi ca<T> ............ ......... ..... ........ ~ ........ :

private T[] Tabela;


public void Adi cionaEl emento (T elemento) { .
public T obtemEl emento (i nt posição) { ... }

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

Obviamente, várias tabelas dinâmicas podem coexistir simultaneamente, sendo


parametrizadas por diferentes tipos de dados:
Tabel àbí nami ca<Émprégado> ~ empregados "~= new "Ta b é l ãõnriãmicá<Émprègadd> Q ;
;TabelaDinamica<int> ordenados = new Tabelaoinamica<int>();
empregados.AdicionaElemento(new Empregado("patricio Domingues", 30)); i
;ordenados.AdicionaElemento(1500);

202 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

^Empregado emp ='èmJDregádos .ÒbtemÈ"leméhtò*COT; ~ í


íint _ordenado == ocdenadps .pbtemE]emento(0) ; __

7.5. l DEFINIÇÃO DE TIPOS GENÉRICOS


Como foi anteriormente referido, para definir um tipo de dados genérico (classe,
estrutura, etc.), basta colocar na sua definição, entre "<>", o tipo de dados que o irá
parametrizar. No entanto, é possível parametrizar uma definição com mais do que um tipo
de dados. Nesse caso, basta indicá-los separados por vírgulas. Por exemplo, é bastante
comum, em informática, utilizar-se uma estrutura de dados chamada "par". Um par
corresponde a dois elementos relacionados. Por exemplo: um nome e uma idade, um
nome e uma morada ou um número de conta e um valor. Tanto o primeiro elemento como
o segundo podem representar qualquer entidade, logo, os seus tipos podem ser quaisquer,
devendo ser parametrizados. Neste caso, a definição de uma estrutura Par poderia ser:
struct P a r < T , K > " ~ ~~ ------ - - -- - —• - - .
{ . .
: public T Primeiro; ,
: public K Segundo; •

: public Par(T primeiro, K segundo) ^

: thi s. Primei ro = primeiro;


: this.Segundo = segundo; :

A sua utilização é directa:


;Par<stri ng, i nt> pessoa" = nêw pãr<strTngYintVTIVitôTMT'27); ..... ........ :

:Console.WriteLine("Nome ~ {0}", pessoa. Primei ro) ; j


Conso1e.WriteLine_C"ldajde = {l}", .,pessoa_. Segundo) ; _......_. _____ ............... ;

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 ;

O código seguinte é inválido:


ístrúct par<T;~K>" "......"......" • - - • • ' - ............ ........ ~- •- :

public T Primeiro;
public K Segundo;

public void ImprimeC)

© FCA - Editora de Informática 2.O3


C#3.5

P ri liiéT ro". Tmp n me Q ; Tf Erro, ~Ím~p"nméU"n"ãò'~e membro de T


Segundo.imprimeQ; // Erro, imprime Q não é membro de K

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;

public void ImprimeQ


"PrímeTrõ"."ímprime Q ; " // OKT T implementa límpl imfvel
_S_egun_do.ljnprimeQ_; _//. .PKi K .irnplementa_ limplijTiiyel
l „ _ „ __ _.
Existem vários tipos de restrições que se podem indicar. Por exemplo, pode exigir-se que
um tipo implemente várias interfaces. Nesse caso, indica-se o nome das interfaces, sendo
estes separados por vírgulas. Por exemplo, para que seja obrigatório que T e K
implementem icomparable e icloneable, que iremos examinar no próximo capítulo,
escreve-se:
istruct Par<T,K> ..... "" ......... "...........~ " " "~ " " ---------- ..... • •
\e T : IComparable, ICloneable
;.. whj.re..j^j_icompa_rab]_ej__icloneable._
E também possível indicar que um certo tipo tem de ser uma certa classe ou então uma
classe derivada desta. Neste caso, dado que C# não suporta herança múltipla, só se pode
especificar uma classe. Por exemplo, se fosse necessário especificar que T teria de ser do
tipo Empregado, ou derivado desta, escrever-se-ia:
struct
where T Empregado
Note-se que é possível indicar várias restrições simultaneamente. Por exemplo, que T,
para além de ser Empregado ou derivada desta, tem de implementar IComparable e
ICloneable:

2O4 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

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.

7.5.2 DEFINIÇÃO DE MÉTODOS GENÉRICOS

Tal como é possível parametrizar classes e estruturas, também é possível parametrizar


métodos individuais. Vejamos um exemplo simples. Consideremos o método estático
obtemTabelaDinamicaQ que, dada uma tabela normal, de um certo tipo, retorna uma
tabela dinâmica contendo todos os elementos nela presentes. Ou seja, a sua utilização
seria semelhante a:
;int[]~ tabela = new int[] í 7,'3'i 5, 7Y~1I, 13 };
TabelaDi namica<int> ,_tab = pbtemTabelaDi nami ca<in.t>Ctabel.a) ; . . .

© FCA - Editora de Informática 2O5


C#3.5 _

obtemTabel aoi namica<int> ( . . .) é uma chamada a um método parametrizado com o


tipo int. A definição deste método é simples:
^stãtíc TabeTãDinamica<T> ObtèmTabelaDfnamfca<T>lT[] valores)" "
, Tabel aoi namica<T> tabela = new TabelaDinanrica<T>O ;
foreach (T valor n" n valores)
'- tabela. AcHcionaElemento(valor) ; .
return tabela;

Diz-se que obtemTabel aoinamicaO é um método parametrizado pelo tipo T. Como


parâmetro de entrada, é-lhe passado uma tabela contendo um conjunto de valores.
O método retorna uma tabela dinâmica, também parametrizada por T. No corpo do
método, é criada uma tabela dinâmica, sendo cada urn dos elementos presentes em
valores, adicionado a essa tabela. No fmal do método, a tabela criada é retomada. Note-
-se que a definição deste método, tanto pode estar na classe Tabel aoinartri ca, como em
qualquer outra classe.

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.

* Programa que implementa uma tabela dinâmica baseada em


* genéricos e com suporte para enumeradores.
*/
using System;
using System.Collections;
p u b l i c class TabelaDinamica<T> : lEnumerable
// Tamanho i n i c i a l da tabela
private const int TAMANHO_INICIAL = 10;
// Número de elementos na tabela
private int Total Elementos;
// A tabela onde são armazenados os elementos
private T[] Tabela;

2O6 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

/* Constrói uma nova tabela */


public Tabel aDi nami caQ

Tabela = new T[TAMANHO_INICIAL];


Total Elementos = 0;
}
/* Adiciona um certo elemento à tabela */
public void AdicionaElemento(T elemento)

i f (Total Elementos — Tabela.Length)


T[] novaTabela = new T[Tabela. Length * 2];
Array.copy(Tabela, O, novaTabela, O, Tabela.Length);
Tabela - novaTabela;
}
Tabela[Total Elementos++] = elemento;
}
/* obtém o elemento que se encontra numa certa posição */
public T obtemElemento(int posição)
return Tabela[posicão];
}
/* Número total de elementos na tabela */
public int NúmeroElementos
get
return Total Elementos;
}

/* Retorna um enumerador para esta colecção */


public lEnumerator GetEnumeratorQ
for (int i = 0; i < Total El ementos; Í-H-)
yield return Tabel a [i];
}

public class ExemploCap7_6


static Tabelaoinamica<K> ObtemTabelaDinamica<K>(K[] valores)
Tabelaoinamica<K> tabela = new TabelaDinamica<K>();
foreach (K valor i n valores)
tabela.Adi ci onaElemento(vai or);
return tabela;
}
public static void MainQ
int[] tabela - new int[] { 2, 3, 5, 7, 11, 13 };
TabelaDinamica<int> tab = ObtemTabelaDinamica<int>(tabela);

© FCA - Editora de Informática 2O7


C#3.5

foreach ("int valor In tab)


console.write("{0} ", valor);
console.WrlteLlneQ l

Listagem 7.6 — Tabela dinâmica usando genéricos (ExemploCap7_6,cs)

Existe, ainda, um problema que não referimos na definição de métodos genéricos.


Suponhamos que queremos criar um método ApagaEl emento ("int pôs). Dada uma
determinada posição, este elimina o elemento correspondente. O seguinte código não
compila:
:pubT1c 'class TãbéTãDlnam1ca<t> ": lEnumerable ' ~ "

' publlc vold ApagaElemento(Int pôs) i


Tabela[pos] = null; // Erro de compilação ;
;1 . . . . _ _ . __ j

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.

Usando def aul t, a forma correcta de escrever o código acima será:

publlc class TabelaDinamica<T> : lEnumerable '.

public vold ApagaElemento(Int pôs)


L ; Tabela [posl_=;defaul t;(t)I _ ; _ ; _ _ ; ; ; ; 77 coloca a posição "V 0_pulnu_]i;~ |
} .. .
Note-se que esta funcionalidade é essencial quando se está a implementar classes
genéricas de estruturas de dados, como listas ligadas e tabelas de dispersão, em que é
necessário inicializar elementos vazios.

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,

208 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

delegates e a generalidade das construções C# que envolvam tipos. Por exemplo, a


seguinte interface pode ser implementada por classes capazes de ordenar tabelas:
/* interface ihiplementado põf cTãsses capazes
interface !TabelaOrdenador<T> ' '• •
{ . -/.' - '
/* Retortna, o array orcjenado*/
T[] ordena(T[] original);
} " . „ .„ _
O mesmo se aplica a, por exemplo, delegates. ReduzTabela é um delegate que, quando
chamado, retorna a tabela passada como parâmetro reduzida a um único valor :
/* Mapeíia.os valores de uma" tabela "num Cínic
delegatóT ReduzTabe]a<T>CT[];_tab_eJ_a) >.__ _

- A maior parte das construções C# (classes, estruturas, interfaces, métodos,


delegates} pode ser parametrizada de modo a que os tipos de dados sobre
os quais operam sejam quaisquer. Tal parametrização é conhecida como
Genéricos genéricos.
- Para se parametrizar um elemento C# (e, g., classe), basta indicar na sua
definição o nome do tipo de dados genérico que irá utilizar. Por exemplo:
class TabelaDlnamica<T> { . . . }
~ É possível parametrizar um elemento C# como mais do que um tipo de
dados, bastando para isso separá-los por vírgulas. Por exemplo:
struct Par<T,K> { ... }
- Quando se instancia (ou utiliza) um tipo de dados genérico, é necessário
concretizá-lo com tipos de dados reais. Por exemplo:
TabelaDinamica<int> tab = newTabelaDinamica<int>Q ;

- Pode-se indicar restrições aos tipos genéricos associados a uma


parametrização. Essas restrições são feitas indicando: a) interfaces que os
tipos têm de respeitar; b) uma classe à qual têm de pertencer ou da qual têm
de derivar; c) que um construtor público sem parâmetros tem de existir
(newQ); d) que o tipo associado tem de ser "valor" (struct) ou
"referência" (class). Estas restrições são feitas utilizando a palavra-chave
where, após a declaração. Por exemplo:
class Tabel aDi nanri ca<T>
where T : class, newQ , icloneable, icomparable

Na parametrização de métodos, caso o compilador consiga deduzir quais


são os tipos associados à chamada, não é necessário indicá-los
explicitamente. Por exemplo, o seguinte código é lícito:
Tabel aoi nanri ca<int> tab = ObtemTabelaDinamica(tabela) ;

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

7.6 REDEFINIÇÃO DE OPERADORES


Muitas vezes, quando se define classes que representam objectos matemáticos, como
sejam vectores, pontos, conjuntos e outros, é útil poder utilizar uma notação baseada em
operadores, em vez de fazer chamadas a métodos. Concretizando: imaginemos que
possuímos uma classe chamada conjunto. É muito mais prático escrever:
cònluntoC ^"conjuntoA + cqnjuntoB; _' ' '/. ' ' " '".._. . .•
do que escrever:
conjuntoc. LimpaO; " " - - - - . . -
;ConjuntoC.Ad1cionaCconjuntoA);
\çpOJuntpc..Adicipna(conjuntoB); . . ...._.
Em C#, pode-se redefinir os operadores existentes, passando a ser possível utilizá-los
como se fossem métodos normais. Chama-se a este processo overloading de operadores.

7.6. l REDEFINIÇÃO SIMPLES DE OPERADORES


Vamos, então, explorar um pequeno exemplo que envolve redefinição de operadores.

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.

A classe complexo representa um número complexo simples. A parte real e a parte


imaginária são armazenadas em duas variáveis de instância, de vírgula flutuante
(double):
class Complexo' " "
double Real;
double imag;
public complexoCdouble ré, double im)
this.Real = ré;
this.imag = im;

public double Modulo


get
return Math.Sqrt(Real*Real + imag*lmag);

public override string ToStringO


return "C" + Real + " , " + Imag + ")"; :
} . .

© FCA - Editora de Informática


^_ TÓPICOS AVANÇADOS

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 " " . ; :

pubTic stãtic CompTéxo operátor+Ccomplexõ a, Complexo"b)


return new complexo (a. Real +b. Real , a.imag-fb.lmag) ;
" • - - • - : " - • — -••• - - ~ - -- -• • " " • " " "•• ~ " " " '
Sempre que se soma dois números complexos, existe um do lado esquerdo do sinal soma
e um do seu lado direito:
Complexo Gompl = new Complexo Cl, OJ;
Complexo; .GoriipZ = new Complexo (O, 1);
: Complexo ^resultado = compl + comp2;_ _
Assim, o método estático operator+Ccomplexo a, Complexo b) aplica-se a expressões
que contenham do seu lado esquerdo um número complexo, e do seu lado direito um outro
número 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

o valor de resultado deverá passar a ser (4, 3).

Para implementar esta funcionalidade, teremos de definir o seguinte método:


iclass CõmpTéxo ~
K

"pubTic "stafnc" ConipTéxò ~opêrãtó~r+(dou"bTe"à, "complexo b} *-:


{ &4
return new Complexo (a+b. Real , b.imag); -

Este processo resulta tal como se espera. No entanto, ao tentar compilar:


[Ç^niífl"ê^g~TésinTãao ..=Içomp +' 2.0;.." _"_7 . ._ T. . . T ..... . :
surge um erro de compilação. A razão é simples. O compilador sabe como somar um
double a um complexo, desde que estes estejam por essa ordem. O compilador não tem
hipótese de saber que a operação é comutativa8. Para permitir a ordem inversa, é
necessário definir um novo método:
jcTIss ~CorífpTèxõ~ ..... ......... " " ~ ' ~" ...... ..... ~" "~ ..... '
li '

~ pUblTcfs tãt"1 c Complexo' òpe rato r+ Cdõub"! e a ," complexo b) ^. ~ \


•f f. * í

return new complexo (a+b. ReS.1 , b.imagD; -;'•' *' - *


.J____________________________________........-•;______; ________ ...... __ .._._ x>^*' " - '"-
~~pub7Tc"stâtic còmpTèxõ opèTatòr+ÇcompTéxb a", "double b) ^ ,^ vi
{ •"""•. f ,* " ' \
return new Complexo Ca. Real +b, a.lmag); ^r^x*/-^ -*.*.$
} _ _ . _ „__________________ _ .'-L. „..;/ __ ____ i,,' ' A^"*

Si______________________________ _ _ _ _. . . . ._ „ _ _ _........ ... ....... . . _ . . . . . _. . . . . .____________.


Um facto interessante é que a seguinte expressão também compila e executa correctamente:
míDe"^_r_eJinjJLdj^ _ l _.„..!."._ . 7 ."
Note-se que 2 é um inteiro (int) e não um número de vírgula flutuante (double). O que
acontece é que, caso não seja possível aplicar um operador directamente ao tipo de dados
em causa, o compilador examina os tipos de dados em causa e tenta converter os
elementos para os que mais se aproximam. Neste caso, o compilador verifica que é
possível fazer uma conversão implícita de 1 nt para doubl e, o que realiza antes de utilizar
o operador.

É 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

Um ponto muito importante é que os operadores têm necessariamente de ser declarados


estáticos. Isto é especialmente relevante para os programadores de C-H-, onde os
operadores podem actuar directamente sobre instâncias de objectos. O facto de os
operadores serem estáticos não traz nenhuma desvantagem digna de nota e, ao mesmo
tempo, permite ao compilador determinar, de forma simples, quais os operadores
aplicáveis a cada situação.

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.

Se examinarmos com cuidado o código e realizarmos algumas experiências, verificamos


que um efeito lateral interessante de redefinir o operador soma (+) é que também é
automaticamente redefinido o operador +=, sem que o programador tenha de fazer alguma
coisa. Ou seja, o seguinte código compila sem nenhum outro código extra:
Complexo-;compl= néw Complexo QT, O"}; " " ~ j
:c0mpTexo comp2 = new ComplexoCl, l); _ '
/ - ' ' ' . " - ^ ^ y ., í "

c0mpl-f-=.co.mp2j ___ ^_. __, J


É trivial para o compilador gerar o código para esta operação. Basta-lhe somar compl
cora comp2 e colocar a referência compl a apontar para o objecto resultante.

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çã

Tabela 7.2 - Redefinição de operadores

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.

© FCA - Editora de Informática 2.1 3


C#3.5

" Para redefinir um operador, declara-se um método estático em que um dos


ARETER parâmetros corresponde ao tipo da classe em causa, usando a palavra-chave
operator, seguida do operador em causa. Por exemplo:
Redefinição de _ _
Operadores f ass Complexo
public static
complexo operator+Ccomplexo a, Complexo b)

} *""

" Se não for possível aplicar directamente um operador, o compilador tenta


converter os seus parâmetros, tentando encontrar o operador que melhor se
aproxima. Esta procura faz-se sempre por conversões implícitas directas.
~ Não é possível modificar o significado de todos os operadores.
Notoriamente, não é possível alterar a definição do operador atribuição =.
" Os operadores de comparação têm de ser redefinidos aos pares (por
exemplo, ==e !=).

7.6.2 CONVERSÕES CERNIDAS PELO UTILIZADOR


Na subsecção anterior, vimos de que forma é que se pode fazer a redefinição de
operadores. No entanto, existe ainda uma forma especial de redefinição de operadores,
que representam conversões implícitas ou explícitas.

Suponhamos que queremos poder escrever:


,CompTexq còmp.= 2YO; * " ]"'" ]" " "" . „ _ _ " . . "..
Ou seja, queremos ver o número real 2 como sendo o complexo (2,0). Dado que um
número real tem sempre uma representação como complexo, a conversão é lícita. Dado
que também não existe possibilidade de perda de precisão na conversão, ou qualquer tipo
de problema, a conversão pode ser feita de forma implícita. Isto é, não é necessário fazer
nenhum cast.

Esta funcionalidade é implementada da seguinte forma:


cTáss Complexo " " - - - — —.

"piibTic static impTícit ^operator complexo CdoutHe valoii)^,.^


return new ComplexoCvaTor, 0); vv

Neste exemplo, estamos a declarar um operador de conversão implícita (palavra-chave


implicit), que converte entidades double em entidades complexo. O código é directo,
retomando um novo número complexo que possui vai or como parte real e O como parte
imaginaria.

214 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

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!).

Para implementar uma conversão explícita, utiliza-se a palavra-chave expli cit:

piiblTc static éxpTfcft òperàtor dòúble TcompTexo valor)


{
i f (valor.lmag != 0.0)
throw new Arithmet1cExceptnon(
valor. ToStringQ + " não é convertivel em double!");
return valor. Real ;

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.

No exemplo anterior, descrevemos a situação em que se faz um conversão entre um tipo


elementar e um objecto de uma classe definida pelo programador. No entanto, nada nos
impede de definir conversões (implícitas ou explícitas) entre objectos de diferentes
classes ou mesmo entre estruturas. Relativamente às classes, existem duas restrições que
se aplicam:

Não é possível definir conversões entre duas classes, em que uma é directa ou
indirectamente derivada da outra.

A definição da conversão tem de estar dentro do corpo de uma das classes em


questão. E irrelevante em qual delas a conversão é definida, mas apenas se pode
encontrar numa delas.

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 primeira regra é simples de entender. Se tivermos uma classe B que deriva de A, o


seguinte código:

é sempre válido e representa o mecanismo básico que permite a existência de


polimorfismo, obj é simplesmente uma referência para um objecto que é realmente do
tipo B. De igual forma, a conversão explícita em sentido inverso:

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

é tão válido escrever:


fcTáss"cTàss~éà ' ~

~puÊn'Tc~'stati c i"mplTc~i t^^pèrator"TTãs"seBTclãsséÁ" "obj)


// Converte implicitamente uma referência de ClassA para ClasseB
__1_1"

como colocar essa definição na classe cl asseB:


rcl"ãss"cTãssèfe
K

pulfbc státTC implicit b"perátór "ClãsseBCclasseÁ o b j ) " ~ "


// Converte implicitamente um objecto de ClásseA em ClasseB
JL
li.....
A única restiiçao que se aplica é que cada conversão apenas poderá estar definida numa
das classes. A motivação é simples: caso o mesmo operador de conversão se encontrasse
definido em ambas as classes, o compilador não saberia qual utilizar.

216 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

- Para definir uma conversão implícita de um certo tipo de dados para um


ARETER outro, define-se um método estático em que um dos parâmetros corresponde
ao tipo de onde se está a converter. Para isto, utiliza~se as palavras-chave
Conversões
definidas pelo
i m p l n c i t operator. Por exemplo, para converter implicitamente de double
utilizador para Compl exo, faz-se:
class Complexo
public static Complexo implicit operator(double d)

> '"

- Para definir uma conversão explícita, aplica-se o mesmo princípio, mas


utilizando as palavras-chave explicit operator. Por exemplo:
class complexo
public static double explicit operator(Complexo c)

} "'

" 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.

7.7 TIPOS ANULÁVEIS


Uma situação muito comum quando se programa é haver uma variável em que é
necessário distinguir se a mesma possui um valor válido ou não. Por exemplo, no
seguinte código:
'int numçroUtilizadòr =" -1;
;numeroUtil-i:zador = LeValorUtilizadorQ ;

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.

© FCA - Editora de Informática 217


C#3.5

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.

Um ponto muito importante deste tipo de variáveis é o propagarem os valores de n u l l .


Assim, é perfeitamente possível escrever:
,~doublè? ralo; " " " ~ ~ " '
s double? área;

!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

."área = X.raio\HasVa1i^}_.7T^tt^ nuTl;"" [ _i


É também de referir, que é possível utilizar os habituais operadores de comparação (==,
!=, <, >, etc.), tanto entre variáveis anuláveis, como entre variáveis anuláveis e não
anuláveis. No entanto, nestes casos, não surge nenhuma excepção ao tentar aceder ao
valor presente na variável. Fazer:
idouble? ran-ó = riuTT;~" ~"'"~
,if Craió < 10.0)
'-, consone.WriteLine("o ralo é menor do que 10");

é absolutamente correcto. No entanto, o programador deverá ter alguns cuidados porque,


neste caso, o facto de raio não ser inferior a 10, não quer dizer que seja maior ou igual a
esse valor. Pode, simplesmente, querer dizer que o valor ainda não foi preenchido.

Também é perfeitamente aceitável escrever comparações explícitas com nul 1 :


iif Craio .== nullj " '"_ ......
Cpnsp1è'.Wr1teL-ine("Raio ainda não definido");
;eTse' '*• ;v "•'
i Consqte W>iteLi ne£lQ -_Y~al PX de.....ranp _ é : {O}.'1 ,_ _rai o^) j

7.7. l OPERADOR DE ADERÊNCIA ANULO


Existe um operador especial, ??, chamado operador de "aderência a nulo"10. A função
deste operador é permitir obter um valor "real" quando uma variável anulável é nula.
A expressão "a ?? b" resulta em a, se a não for nul 1 , e em b, caso o seja. Por exemplo:
<onsaTe.W.riteUnèCp^
Imprime o valor do raio, caso esteja tenha sido preenchido, ou zero, o valor por omissão,
caso não o tenha sido.

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 ;

rdoub1e"ráTò"= raiõHutTTizadbr"?? ~RAIQ_NÕRMAL:


estã-se efectivamente a converter uma variável do tipo double? em double, fornecendo
urn valor por omissão (RAIO_NORMAL), caso a variável doubl e? não esteja preenchida.

10 Na terminologia .NET denominado: nitll coalescing operaíor.


© FCA - Editora de Informática 219
C#3.5

- Uma variável anulável corresponde sempre a um tipo valor (value typé),


permitindo armazenar um valor ou uma indicação de que o mesmo ainda
não foi preenchido.
Variáveis
anuláveis " Para declarar uma variável anulavel, basta acrescentar um ponto de
interrogação ao tipo de dados correspondente. Por exemplo: int? x;
" Para aceder ao valor armazenado, basta utilizar o nome da variável (no
exemplo acima, x). No entanto, caso a variável não contenha nenhum valor,
é lançada uma excepção.
- Para verificar se uma variável anulavel possui o seu valor preenchido,
utiliza-se a propriedade Hasval ue. Por exemplo:
if (x.HasValue) console.WriteLine(x);
~ Para converter uma variável anulavel numa variável normal, é necessário
uma conversão explícita. Por exemplo: int x_real = (int) x;
" Caso se atribua a uma variável anulavel o resultado de uma expressão
contendo uma variável anulavel, se esta última for null, o mesmo será
propagado à variável atribuída.
~ É possível utilizar os operadores de comparação com variáveis anuláveis. O
resultado da comparação é sempre um valor verdadeiro ou falso (i.e. bool).
" O operador de aderência a nulo, ??, permite converter uma variável
anulavel numa variável normal, fornecendo um valor por omissão para o
caso da variável ser ntill. Por exemplo: int xr = x ?? -1;

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.

Apesar de o mecanismo de ponteiros ser muito poderoso, é também extremamente


perigoso. A partir do momento em que o programador começa a manipular endereços
físicos de memória, é extremamente fácil corromper a memória do sistema, escrevendo
em cima de outras variáveis, no stack ou noutras zonas menos próprias. Não podemos
deixar de realçar que, regra geral, o uso de ponteiros ern C# é fortemente desaconselhado.
Na verdade, na grande generalidade das situações, não é necessário.

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

É também possível marcar um método como sendo unsaf e:


[class Teste " - - - - - - - - - - - - - - .......

public unsàfê™võTcTxptb"O~ '"

ou mesmo uma classe (ou estrutura):

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.

Nesta secção, apresentaremos apenas exemplos simples de utilização de ponteiros, uma


vez que a sua relevância é mais associada com utilização de código legado, não tendo
muita utilidade em código escrito especificamente para .NET.

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''.

Associado à utilização das variáveis ponteiro, existem dois operadores especiais: o


operador "valor", representado por um *; e o operador "endereço", representado por um
&. O operador * permite obter o valor de uma variável identificada por um ponteiro.
O operador & permite obter o endereço de memória que uma determinada variável ocupa.
Vejamos um pequeno exemplo:

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:

irá fazer com que a variável x tome o valor 20.

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";" " ~ ' -

iPonto* ptr = &p; :•

C*ptr).x = 10; // p. x fica com o valor 10


:.C*ptr>.y = 20.; _ /_/_ p.y fica com o valor 20 ;

escrever C*ptr) .x e similares é bastante desagradável. Quando se trata de estruturas, é


possível utilizar uma sintaxe especial com o mesmo significado. Essa sintaxe é baseada
no operador ->, que tem o significado de "elemento da estrutura apontada" . O código
equivalente ao exemplo acima, utilizando este operador, é:

222 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

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.

7.8.2 ARTTMÉnCA DE PONTEIROS


Em C#, é suportado o conceito de aritmética de ponteiros. Isto quer dizer, que é possível
somar ou subtrair valores a um dado ponteiro, passando para ura elemento seguinte ou
anterior. Por exemplo, suponhamos o seguinte método:
public static unsãfé .
void CopiaRapida(double* origem, double* destino, int total)
fon^-Cirit t=0-; i<total ;
• í
. = - f ongem;
++òri'gem;
++destino;
} . , • , ..

O objectivo deste método é implementar um método de cópia rápida entre um buffer de


dados de origem e um buffer de dados de destino.

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; - \{

© FCA - Editora de Informática


C#3.5

r"for"(TrTt" f=0; i<tqtarl j á


l -fdestino = *orT'gem; r"/
j ++origem;
; -H-destino;

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.

Retomando a discussão acima, se tivermos um tipo de dados T e se a um ponteiro Pt r


para esse tipo de dados lhe somarmos um valor deslocamento, o endereço de memória
gerado será Ptr + deslocamento-sizeof(T).

7.8.3 PONTEIROS E TABELAS


Tal como em C/C++, existe uma relação muito íntima entre ponteiros e tabelas. Em C#,
ao ter-se um ponteiro para um tipo elementar12, é possível vê-lo como sendo uma tabela.
Isto é,
:*Cptr"+"~iri_ciexJ"=" total; " _ ' _ " • " _ " " " . " . . ~ . "~.'-7 " ". ".""... /...".". '.'.. """/. -
é perfeitamente equivalente a escrever:
.ptr[indèx] "=*'tòtáj;""" ' " . ""J. :"-""•" '.'•'" '-.'- . ',. ' .... .T!.-M. ".".".' •
em que ptr é um ponteiro para um tipo elementar (por exemplo, um double) e index,
uma variável inteira.

Usando esta sintaxe, o código do método copiaRapidaO poderia perfeitamente ser


escrito da seguinte forma:
lie:"stãt1c unsafe " " ~ ~~ "
;void CoplaRapidaCdõuble* origem, double" destino, int total)
for (int 1=0; 1<t9tal; i++)
'?> :*desi::ino.[n] = origemCi] ; - ;

Apesar de existirem similaridades entre a notação utilizada em tabelas e a utilizada com


ponteiros, o código seguinte não é válido:

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

.doubTieT]' X == nèW doub1e[20] ; ....... * " ....... -— . ~ - ... ... .


.Qrigem.= Cdp_ubl_e*;)_&x; ....... __. _ ...... ._ .//..Erroi .de .compilação!
Como já referimos, não é permitido obter um endereço de memória associado a um
objecto. Como as tabelas são uma forma especial de objectos e residem no heap, não é
possível obter o seu endereço de memória ou utilizá-las por intermédio de ponteiros. No
entanto, existe uma forma especial de tabelas, criadas no stack, que são utilizadas
juntamente com ponteiros13.

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.

7.8.4 PONTEIROS PARA MEMBROS DE CURSES

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

P ..... thTs.y '=~~y;


;1 . ____________________
Neste caso, ao fazer:

estamos a colocar ptr a apontar para a variável de instância x associada ao objecto p. No


entanto, o que irá acontecer se o garbage collector resolver mudar o objecto referenciado
por p de localização de memória? Na verdade, o código acima não é permitido, pelo
menos nesta forma directa.

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);

fixed (int* ptr = &p.x)

// Código que utiliza ptr, tendo p. x sido fixado em memória '.


*ptr = 20;
!> ._ ......... _. . .. ........ ._ ....... :

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

iflxedTint* ptrX = &p.x)


iflxed (Int* ptrY = &p.y)

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;

© FCA - Editora de Informática


Tópicos AVANÇADOS

ARETER Um ponteiro representa um endereço de memória de uma variável, sendo


possível manipular essa variável por intermédio do ponteiro.
Ponteiros Sempre que se utiliza ponteiros, é necessário marcar o bloco de código,
método ou classe com a palavra-chave unsaf e.
~ Para declarar um ponteiro, utiliza-se a seguinte sintaxe:
TipoDoponte iro * nomeDoPonte iro;
- Associados aos ponteiros, existem dois operadores: o operador endereço
(&), que permite determinar o endereço de uma variável; e o operador valor
(*), que permite alterar a variável apontada. Exemplos:
•int* ptr = &x; *ptr = 10;
~ Apenas se pode declarar ponteiros para variáveis elementares ou para
estruturas.
" O operador -> utiliza-se para aceder a membros de estruturas. Escrever
estrutura->var é equivalente a escrever (^estrutura).var. A primeira
forma é preferível, pois aumenta a legibilidade do código.
" É possível somar ou subtrair valores a um ponteiro, sendo o deslocamento
correcto automaticamente calculado.
~ O operador sizeof permite obter o tamanho em bytes que ura tipo de dados
ocupa. Por exemplo, si zeof (1 nt) resulta em 4.
- Pode-se aceder a uma variável do tipo ponteiro, como se de uma tabela se
tratasse. ptrCindi cê] é equivalente a *(ptr -í- Índice).
- O operador stackalloc permite obter um certo número de elementos
reservados no stack. Por exemplo:
int- tabela = stackaTloc int[20];
" Sempre que se declarar um ponteiro para um membro de um objecto, é
necessário garantir que a variável não muda de localização em memória.
Para isso, utiliza-se o operador fixed.
" Para utilizar o operador f i xed, colocam-se entre parêntesis a(s)
declaração(s) dos ponteiros correspondentes, seguida da sua atribuição. Por
exemplo:
fixed (int* ptr = Aponto.x) { . . . }

7.9 MÉTODOS COM NÚMERO ARBITRÁRIO DE PARÂMETROS


Até agora, temos vindo a examinar métodos que levam um número fixo de parâmetros
como argumentos. Sempre que o programador especifica um método, tem de indicar, de
forma clara, quais os parâmetros com que o método é chamado e quais os seus tipos. No
entanto, por vezes é interessante poder ter métodos que levem um número não
predefinido de argumentos. Na linguagem C#, tal é possível.

Consideremos novamente a classe Matemati ca e o método Max C):


public cTass Matemática - - - - - - i

© FCA - Editora de Informática 227


C#3.5

>úbTTc"'sfatic i n t Max~(int[l valores) """ - - - - - - ,

int max = valores[0];

i foreach (int vai in valores)


if (vai > max)
:- max = vai;
}
return max;
' }

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 especificar um parâmetro de entrada com a palavra-chave params, esse parâmetro


passa a representar uma tabela de parâmetros. Na verdade, tem de ser declarado como
sendo uma tabela. Quando o compilador encontra uma chamada ao método em causa,
encarrega-se de colocar os parâmetros especificados, dentro da tabela em causa. Mais
concretamente: se o método Max() for declarado da seguinte forma:
[puEFPic statlc int Maxtparams Int Q valores)""

. int max = valores[0];


' foreach (int vai i n valores)

: i f (vai > max)


; max = vai;
l return max;

passa a ser possível escrever


7', "3", 4)";~ "J""..".."!. _ _" .. _ " ."
Nesta expressão, os valores l, 2, 3 e 4 são automaticamente colocados dentro de uma
tabela e vistos no método MaxQ como sendo a variável valores.

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:

228 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

Apenas se pode declarar um parâmetro com a palavra-chave pararas.

O parâmetro declarado com a palavra-chave params terá de ser o último do


método, sendo os outros vistos como parâmetros obrigatórios.

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 ;
{ " " • " " " "
| :

int max = valorl;


foreach (int vai i n outrosvalores)

i f (vai > max)


max = vai;
}
return max;
} . _ _..
Ao fazer isto, apenas o código, que especifique pelo menos um inteiro como parâmetro,
irá compilar correctamente. Ao mesmo tempo, ao escrever:
int max_=. Matematica._Max(lJL 2, 3, _4); _ " * . . _ „ . _".7 _ .
o valor l será colocado na variável valorl e 2, 3 e 4 na tabela outrosvalores.
Obviamente que, neste exemplo, deixa de ser possível passar directamente uma tabela ao
método:
int[] tab = f l, 27"3, 4"}f "
int max.= Matemática.Max.(tabJ)j.
Mas se essa funcionalidade for requerida, pode-se criar uma segunda versão do método,
que leva apenas uma tabela como parâmetro.

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.

©TCA- Editora de Informática 229


C#3.5

" Um método pode ser declarado como tendo um número arbitrário de


parâmetros. Para isso, utiliza-se a palavra-chave params.
Métodos com um " Ao especificar um parâmetro com este modificador, o parâmetro tem de ser
número declarado como sendo uma tabela, correspondendo aos argumentos
arbitrário de variáveis do método. Exemplo: p u b l i c static int Max(params int[]
parâmetros valores);
" Apenas um parâmetro pode ser declarado com o modificador params,
tendo este de ser o último parâmetro do método.
" Todos os parâmetros declarados antes do modificado com params são
encarados como parâmetros obrigatórios.
~ É válido passar uma tabela na chamada do método, no local formal da
passagem do número arbitrário de parâmetros. Isto é, Max (l, 2, 3); e
MaxCnew 1nt[] {l, 2, 3}); são equivalentes.

7AO MÉTODOS DE EXTENSÃO


Os métodos de extensão são métodos estáticos definidos pelo programador que podem ser
"colados" a classes já existentes. Isso permite estender a funcionalidade dessas classes
sem implicar a criação de classes derivadas ou modificar o código original. Como vamos
ver, não existe, aparentemente, diferença entre chamar um método de extensão e os
métodos que são definidos directamente na classe original.

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

230 © FCA - Editora de Informática


TÓPICOS AVANÇADOS

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; , . .

; . return result; ......

•} _ _ _. _ __ _ _ .... .. . : , _ . . _ . . ,
A partir deste momento, torna-se possível escrever:
string" userNameS_èj3uro~ =~\U5^ "~ . . .""".!'

Ou seja, "colamos" o método TornaSeguroQ ao tipo de dados string, sem modificar a


classe correspondente.

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.

É de referir, que os métodos de extensão são mais Limitados em termos de funcionalidades


do que os métodos de instância. O princípio de encapsulamento não é violado: este tipo
de métodos não pode aceder a métodos privados do tipo de dados, ou a variáveis não
públicas.

Como seria de esperar, pode-se usar métodos de extensão para aumentar as


funcionalidades de uma classe ou interface, mas nunca para os substituir. Em termos de
prioridades de execução, este tipo de método tem menor prioridade do que os da própria
classe. Quando o compilador encontra uma invocação de um método, procura primeiro
© FCA - Editora de Informática 231
C#3.5

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.

ARETER ~ Os métodos de extensão permitem "colar" métodos à parte a classes já


existentes.
Métodos de ~ Os métodos de extensão têm de ser declarados numa classe estática, sem
extensão variáveis de instância ou propriedades. Estes têm de ser estáticos, levando
como primeiro parâmetro o tipo de dados que irão estender, precedido da
palavra-chave thls. Por exemplo:
public stafic string TornaSeguro(trns string s) { ... }
" Os métodos de extensão não podem aceder a variáveis ou métodos da classe
a que se aplicam, tendo a mesma visibilidade sobre estas que outro código
externo.
" Caso exista um conflito de nomes entre um método de uma classe e um
método de extensão, o que é executado é sempre o da própria classe.

7.1 1 DESTRUIÇÃO DE OBJECTOS


Uma das grandes vantagens da utilização de linguagens que dispõem de um garbage
collector é libertarem o programador da tarefa de gerir a utilização da memória.
O programador apenas tem de criar e utilizar os objectos e, quando estes já não são
necessários, o ambiente de execução encarrega-se de os limpar.

Existem várias formas de implementar esta funcionalidade. Uma fácil de entender é


utilizando o conceito de contador de referências14. Cada objecto possui um contador.
Sempre que existe mais uma referência a apontar para o objecto, o contador é
incrementado. Sempre que existe uma referência que deixa de apontar para o objecto, o
contador é decrementado. Quando o valor do contador chega a O, o ambiente de execução
sabe que pode limpar o objecto:

Pessoa emp = new Pessoa("Manuel Marques 11 );


Pessoa emp2 = emp; // o contador agora fica com valor 2
;emp = null; // o contador volta a l :
emp2 = null; _ ______ //^contador a __Q_, o objecto pode ser limpo

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.

Em C#, existe o conceito de destrutor, que é um método especial que é chamado


automaticamente quando o garbage collector está a eliminar um determinado objecto15.
O programador pode fornecer uma implementação deste método, colocando nele código
que se certifique de que recursos que estejam associados ao objecto em causa são
libertados.

Antes de avançarmos com a discussão, devemos advertir o leitor. Os destrutores apenas


são executados quando o CLR decidir fazê-lo. O programador não tem nenhuma garantia
sobre a altura em que um destrutor é chamado, sendo mesmo lícito por parte do CLR
nunca o fazer. Assim, num destrutor, nunca deverá existir código essencial para o
correcto funcionamento do programa. No objecto em causa, devem existir métodos
explícitos para o programador chamar, de forma a libertar recursos pendentes, quando
estes já não são necessários16.

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);

-XptòQ " """ " " "~ "


Console.wn'teLineC"Obi'ecto <{0>> destruído". Nomeoblecto):

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");

for executado, surgirá no ecrã:


[Objecto ~<õhrjÃ> construído
pblecto _<obj A> destruí do
Note-se que o destrutor é chamado para cada instância existente. Assim, se o código for:
iXpto objA = new Xpto("objA") ; " .............
Xpto pbjB _= new Xpto("pbjB") ;
a execução será:
lõbjèctò <ob"jA> construído
iobjecto <objB> construído
^Objecto <objB> destruído
[Qbjectp_ <obj/>_destrui'dp ;
Repare-se que o obje é destruído antes do objA. No entanto, não existe qualquer garantia
sobre qual a ordem pela qual o garbage collector irá chamar os destrutores. Nenhum
código devera depender de uma ordem especial.

Os destrutores possuem ainda uma característica muito interessante. Caso se esteja a


trabalhar numa classe derivada, apenas se tem de escrever o código correspondente à
classe em que se está a trabalhar. O CLR encarrega-se de chamar automaticamente todos
os destrutores da hierarquia de derivação. A destruição começa sempre da classe mais
derivada para a menos derivada.

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

Ao executar o seguinte código:


clãs s Teste .........
r
; static vold Main(string[] args)

Derivada d = new DerivadaO;

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.

Como se pode observar, os destrutores são um mecanismo muito simples de utilizar. No


entanto, pela sua falta de determinismo (nunca se sabe quando são chamados, se alguma
vez) e pelo sério impacto que podem ter na. performance, devem ser evitados ao máximo.

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.

© FCA - Editora de Informática 235


C#3.5 _

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.

De qualquer forma, por medida de precaução, o programador de uma determinada classe


deverá colocar no destrutor da classe código que se certifica de que os recursos são
efectivamente limpos. Este código constitui uma medida de programação defensiva para
os casos em que o programador se esquece de chamar closeQ ou DisposeQ.

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

p ri vate void FechaBaseoadosQ


// Fecha a ligação à base de dados

public void closeQ

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;

'• public void Cl ose C)


FechaBaseDados Q;
CloseExecutado = true;
}
~Li gacaoBaseoados C)
if (!CloseExecutado)
FechaBaseDadosQ ;
}

No entanto, o uso de GC.suppressFinalizeQ é mais eficiente porque evita que o


destrutor seja chamado.

© FCA - Editora de Informática 237


C#3.5

7.11.3 A INTERFACE IDlSPOSABLE

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 ;

janel_a.DisposeO; _ . „_ .._ ....


Ao chamar j anel a. DisposeO, a caixa de diálogo deverá desaparecer do ecrã, sendo
libertos os recursos que estão associados a esta.

A partir do momento em que o programador define uma classe que implementa


System. IDÍ sposabl e, torna-se possível utilizar a seguinte sintaxe:
09. -CCaixaDi a l ogo Jane l a_=.
// utilização de janela para os mais diversos fins

Quando o fluxo de execução sai do bloco em questão (bloco using), o método


DisposeQ é automaticamente chamado sobre o objecto janela.

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) • ' ' '-" :

ou encadear várias declarações usi ng:


, j'ahêTâ'1 ='rièw CaixaõialogoO)
using iCPíffietepesenho palette = new PalleteDesenhoQ!)
• -,- - - •

.}

~ Um destrutor é um método especial que é chamado quando o garbage


collector elimina, de memória, um objecto que já não está a ser utilizado. Ern
.NET este tipo de métodos chzimam-se.finalizers.
Destruição de
objectos " Para declarar um destrutor, utiliza-se o nome da classe precedida de til, sem
modificadores e sem parâmetros. Exemplo:
class Xpto {
-XptoQ í ... }

~ O garbage collector chama o destrutor da classe em causa e das suas classes


acima, na hierarquia de derivação. A ordem seguida é da classe mais derivada
para a menos derivada.
~ Não existe qualquer tipo de garantia sobre quando os destrutores são
executados ou por que ordem relativa, em relação a objectos diferentes.
" Quando uma classe representa ou possui objectos que devem ser explicitamente
removidos, deverá implementar um método CloseO ou um método DisposeO •
~ O método closeO deverá ser implementado ern classes que, de alguma
forma, representem uma ligação a algum recurso.
~ O método DisposeQ deverá ser implementado ern classes que representem
um recurso volátil que deverá ser eliminado no fim da sua utilização.
-Em classes que implementem CloseO ou DisposeQ, o destrutor deverá
fechar ou limpar o recurso associado, caso tal ainda não tenha acontecido.
~ GC.SuppressFinalizeO permite evitar que o destrutor de um objecto seja
chamado, sendo útil quando utilizado em conjunto com closeO e DisposeQ •
- Caso uma classe implemente system.ioisposable, é possível utilizar um
bloco using para garantir que oisposeO é chamado num certo objecto.
- Para utilizar um bloco using, utiliza-se a palavra-chave using e a declaração
do(s) objecto(s) em causa, que implementam iDisposabl e. Por exemplo:
using (caixaoialogo janela = new caixaDialogoQ) { ... }

© FCA - Editora de Informática 239


C#3.5 __

7.12 PRÉ -PROCESSAMENTO


Tal como noutras linguagens, o compilador de C# suporta a noção de pré-processamento.
Em C/C++ , esta noção é tipicamente implementada, utilizando um programa que corre
antes do compilador propriamente dito. No entanto, em C#, esta funcionalidade é
implementada directamente pelo compilador.

A ideia de pré-processamento é existir um conjunto de directivas no código, que indicam


de que forma é que este deve ser compilado. Ou seja, são instruções directamente
dirigidas ao compilador. Estas directivas nunca produzem código executável. Apenas
auxiliam o compilador na interpretação do código fonte.

Vejamos um exemplo. A directiva #def~1ne permite definir um símbolo no código.


Utilizando a directiva #if, é, depois, possível verificar se um determinado símbolo está
definido, tomando uma certa acção nesse caso. Consideremos o seguinte exemplo:

using system;

class Teste
{
private int total ;

: public vold F()


'
#rfDÉBUG~ " ..... "" . ' . *-. . ; -~ "-
console. WnteLlneC"[DEBUG] total={0}", total))-
fendif ...... _/. •_. ._'_'._; ........ _....._____.............._ ..... -:."'../:'•. :

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.

Note-se que as directivas de pré-processamento começam sempre com o símbolo cardinal


(#) e não são terminadas com ponto e vírgula.

24O © FCA - Editora de Informática


TÓPICOS AVANÇADOS

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.

De seguida, apresentamos as directivas existentes.

7.12. l DIRECTIVAS #DEFINE E #UNDEF


A directiva #define permite definir a existência de um determinado símbolo no processo
de compilação. As directivas #define têm de ser sempre os primeiros elementos de um
ficheiro, antes de quaisquer instruções da linguagem em si. Para definir um símbolo,
basta indicai1 o seu nome:
;#defi-ne DEBUG " _"; /".""_.'.7" .. T " ~7~ 1. ^"_...y../.r'ri.V"_.""".";/-T''"'_"."" " "i
A directiva #undef permite fazer exactamente o contrário da primitiva #def 1 ne. Caso um.
certo símbolo esteja correntemente definido, esta directiva retira-o da tabela de símbolos.
A sua utilização é semelhante ao #def i ne. Por exemplo:
#undef DEBUG ; ./ . _"_ "__"..""„_" ..l.".'___"." 7-7117. V7'l~7". 7.777T.77 7_J.". . .'•"..""

7.12.2 DIRECTIVAS #IF, #ELIFS #EUSE E #ENDIF


Este conjunto de directivas permite incluir condicionalmente blocos de código, de acordo
com um conjunto de símbolos definidos, #if representa um teste. #elif representa else
if, isto é, um novo teste num bloco else. #e*lse representa a condição contrária à testada
na parte if do bloco. Finalmente, todos os blocos condicionais têm de ser terminados por
#endif.

Eis um exemplo de utilização:


#define DEBUG
#define PROC_PENTIUM4

iclass prográmacalculo

pubiic static void Mal n C)

Console. WriteL-ineC"Programa em modo de debug.");


#endff

publle void cal c Q


i , "
#1f PROC_PENTIUM4
// Rotina de cálculo optimizada para Pentium 4

" #elif PRQC_PHNTIUM3'


/ / R o t i n a de cálculo optimizada para Pentium 3

© FCA - Editora de Informática


C#3.5

' " #élse ~ " . .. - - -


1 // Rotina de cálculo compatível com todos os processadores
#endif
}

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

7.12.3 DIRECTIVAS #WARNING E #ERROR


Tal como já referimos, as directivas de pré-processamento destinam-se ao compilador e
são utilizadas, exclusivamente, durante a fase de compilação. Quando o compilador
encontra uma directiva #warrring, gera um aviso de compilação, incluindo o texto
especificado na directiva. Quando encontra uma directiva #error, gera um erro de
compilação, terminando-a e mostrando a mensagem especificada. Eis um exemplo de
utilização:
',#if PENTIUM3~&& PENTIUM4 - - - - - -- . . .
1 #error "PENTIUM3 e PENTIUM4 definidos simultaneamente"
:#endif
; #if PENTIUM4 '
#elif PENTIUM3

#warning "A u t i l i z a r uma versão não optimizada do código!"


#endif_ . , . _

7. T 2.4 DIRECTIVA #LJNE


Na maioria das circunstâncias, a directiva #line não é muito útil. Esta directiva permite
indicar ao compilador, uma outra numeração para as linhas do código fonte e para o nome
do ficheiro que se encontra a ser compilado.

Tipicamente, esta directiva é utilizada quando o ficheiro possui instruções de outras


linguagens que irão ser retiradas por uma ferramenta, antes do ficheiro ser passado ao
compilador de C#. Caso sejam gerados avisos ou erros, as linhas reportadas pelo
compilador devem ter uma numeração coincidente com o ficheiro original escrito pelo
242 © FCA - Editora de Informática
_ TÓPICOS AVANÇADOS

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 _ '_" ' " __ ; ~ ...... '__ . ' ; '_ _

7. 1 2.5 DIRECTIVAS #REGION E #ENDREGION


As directivas #regi on e #endregi on permitem defímr uma região de código relacionado.
Esta região é utilizada para, no ambiente VisualStudio.NET, ser possível expandir ou
colapsar estas regiões de código de uma forma visual. Por exemplo, em:
•clàss Teste ........ ~ ". . . . ." ' ,, ". . " • - - - - - .
' #regi_o'o Declaração de variáveis
privpfcè-.int x; -
priyaire irit y; ., .i ' .
#endregiõn

J • • ' . . . _ . .... . . . . . . . . . . . .
existirá uma região visualmente expansível, identificada com o nome "Declaração de
variáveis

~ As directivas de pré-processamento indicam ao compilador de que forma


ARETER deverá ver e interpretar o código fonte. Não é gerado código máquina
devido às directivas de pré-processamento.
Pré-
-processamento ~ As directivas começam com o símbolo cardinal (#) e não são terminadas
com ponto e vírgula.
~ As directivas #defi'ne e #undef permitem definir e retirar símbolos de
compilação.
~ As directivas #if, #e1if, #else e #endnf permitem declarar blocos
condicionais de código, de acordo com os símbolos definidos. É possível
utilizar operadores lógicos para formar expressões sobre conjuntos de
símbolos.
- #warm"ng e #error geram, respectivamente, um aviso de compilação e um
erro de compilação, apresentando a mensagem especificada.

© FCA - Editora de Informática 243


C#3.5

~ #line permite alterar o número da linha e o nome do ficheiro em que a


ARETER compilação está a decorrer.
Pré- " #region e #endregion permitem definir zonas de código expansível
-processamento visualmente no ambiente VisiialStudio.NET (ou outros que para tal estejam
preparados).

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 (///).

Este tipo de comentários permite criar documentação em XML19, sobre o código em


causa. A ideia é que quando o programador escreve uma classe, ou ura método, preenche
a documentação associada à mesma. Usando esta documentação, é possível usar uma
opção do compilador para extrair directamente do código fonte a documentação da classe
em causa.

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. '

Tabela 7.3 — Tags XML para documentação de código

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

automaticamente as tags necessárias ao elemento que se quer documentar, quando são


escritas as três barras de início de comentário.

Por exemplo, vejamos a classe seguinte, comentada desta forma:


/77<summary>
/// Esta classe representa um empregado da empresa.
/// ._ _</_summa,ry> ._
class Empregado

<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)

Ao correr o compilador sobre o ficheiro em causa, utilizando a opção /doe:


:Tpès"sòarxmTTêssòa7cs" " ~
irá ser gerada a documentação em XML sobre a classe Pessoa (ficheiro "Pessoa.xmT).
É ainda possível utilizar este ficheiro para gerar documentação em HTML sobre a classe,
podendo esta ser vista ern qualquer browser de Internet. A figura 7.1 mostra uma das
páginas de documentação gerada para a classe pessoa.

© FCA - Editora de Informática 245


C#3.5

•3 Teste - Microsoft Internet Explorer ^T||n|fx|

0e &ít tfew Fayorites locb H* . fifr

QBacfc - Ô • 0 (g (ft pSea.* -^Favorite «-Meda © £- & H " D t> S

Addffw[Qc!\TenpVMtelP>deCoromenlRepoftlTeítElíresteJfTM [v | gj Go Unte "

jX>/y Code Comment Web Report


Solution i Proiect 1

B Global Empregado Class


Empregado
Esta classe representa um empregado áa empresa.

Access: Project

Base Classes: Object

|í,-[.:^;;-.í;- ft^vl^"';''';- j
Construtor da dasse.

Calcula o ordenado do empregado de acordo


CalculaOrdenado «=m ° número de horas que trabalhou.

r 1 iíi
ÈÊlDone . 9 M V Computer .;

Figura 7.1 — Uma página da documentação gerada para a classe Pessoa

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.

246 © FCA - Editora de Informática


II
,NET ESSENCIAL
v

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.

© FCA - Editara de Informática 249


C#3.5

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.

Tabela 8.1 — Métodos disponibilizados em System.Object

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.

À excepção do destrutor (método FinalizeQ), do método TostringC) e do método


MemberwisedoneQ, todos os outros métodos estão relacionados com comparação de
objectos. De seguida, iremos examinar de que forma é que estes métodos devem ser
utilizados e de que forma deverá ser feito o seu overnWe/implementação em classes
derivadas.

8.1.1 MÉTODO TOSTRJNGO

Tal como já vimos anteriormente, a utilização do método TostringC) é muito simples.


Este método retorna uma mensagem que representa o objecto em causa. Tipicamente, este
método é utilizado para efeitos de depuração de código. Sempre que se chama o método
WriteLineQ sobre um objecto, TostringC) é chamado, sendo usada a string retomada
para enviar a mensagem para o ecrã. Por exemplo, se declararmos Empregado da seguinte
forma:
:c1ass Empregado" ' " "
{ *
private string Nome; i

. public EmpregadoCstrlng nomeoapessoa)

Í this.Nome = nomeDapessoa;

públTc~~õvérridè striTíg TostrírigX) —— -

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.

8.1.2 COMPARAÇÃO DE OBJECTOS


Existem duas definições do método EqualsQ, uma delas estática, que executa a operação
sobre os dois parâmetros e outra de instância, que compara um objecto recebido por
parâmetro com o objecto corrente.

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.

No caso de Empregado, o override do método de instância EqualsQ ficaria:


s* Empregado
Í, " *" i "**• y
Nome; "^ >, ,
=r, ^ j __ t

pub|Ê%.iÉliipíregadoCstr1ng nomeDaPessoa) .^^ar^-f-^r'5 '


^èjnjg,-* =" nomeDaPessoa; ^ **3^**? - '-° "

"pUbITc overrTde't5ooT ÊqúãTlVCobject otHér)


Empregado emp = other as Empregado;
i f (emp == null)
return false;
return Çthls.Nome == emp.Nome);

© FCA - Editora de Informática 25 l


C#3.5

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.

Resumindo, a semântica global destes métodos é: object. ReferenceEqualsQ verifica


se duas referências apontam para o mesmo objecto, EqualsQ compara dois objectos
baseados no seu valor e os operadores == e ! = , tipicamente, comparam objectos
baseando-se na sua referência, no entanto, não existem garantias de que seja essa a
semântica utilizada.

252 © FCA - Editora de Informática


_ CLASSES BASE

Certamente que o leitor se está a questionai- se deverá também realizar a implementação


do método estático EqualsQ. Na verdade, tal não é necessário. Internamente, a
implementação deste método em object é algo semelhante a:
pUblfc static booTÉquàls (object à, object b ) • • • - - - • - ........ -
H . - . - -1 - • "
if Cõbjçct.ReferenceEqualsCa, b))
true; " ;
i f Ca. l=v.
return a.Equals(b) ;
i return. false;
J. - ....... ..
Ou seja, EqualsQ começa por verificar se a e b são referências para o mesmo objecto.
Caso sejam, retorna true. Esta comparação também trata dos casos em que a e b são
n u l l , resultando em true. De seguida, é verificado se existe realmente um objecto
associado a a. Caso exista, então, é chamado o método EqualsQ de instância,
comparando com b. Caso a seja n u l l , então o resultado é obrigatoriamente false, uma
vez que a é null e existe um objecto qualquer associado a b (se b também fosse n u l l ,
object. ReferenceEquals(a, b) teria resultado em true).

O ponto interessante é que a. Equals(b) está a chamar um método de instância associado


ao objecto a. Uma vez que EqualsQ é um método virtual, o método correcto acaba
sempre por ser chamado. Assim, nunca é necessário redefinir o método estático
EqualsQ 1 .

8.1.2.1 MÉTODO GETHASHCODE O

Se compilar o exemplo anterior, o compilador irá emitir o seguinte aviso:


test. es p) f I¥mprég"aãó'r~ovèm'désv<^^ " "
íbut _dóes not pyern_de object..GetHashCÕdeC) .!..:'„
isto, apesar de os exemplos funcionarem correctamente. Ou seja, o compilador quer que o
método GetHashcodeO seja redefinido, sempre que se redefine o método EqualsQ.

O método GetHashcodeQ deve retornar um número único, ou quase único, associado a


cada objecto existente. Embora possam existir dois objectos diferentes com o mesmo
código hash, isso deverá acontecer muito esporadicamente. Este código permite que
certas estruturas de dados armazenem, de forma muito eficiente, os objectos existentes
num programa. Para isso, os seus números associados (chaves) devem ser diferentes, ou

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).

A forma de criar códigos hash é um tópico relativamente complexo. No entanto, existem


as seguintes regras-de-dedo, válidas na grande maioria das situações. Os códigos hash
devem ser únicos ou quase únicos e relativamente diferentes para objectos parecidos.
Caso exista uma cadeia de caracteres única associada a um objecto, pode utilizar-se o
método GetHashcodeQ sobre essa cadeia de caracteres, para obter um bom código hash4.
Caso seja possível combinar várias cadeias de caracteres e números para obter uma
string única (ou quase única) para um objecto, pode-se utilizar a mesma abordagem
sobre a cadeia resultante. Caso um objecto tenha um número único associado, muitas
vezes, pode-se utilizar directamente esse número. No entanto, o programador deve ter o
seguinte cuidado: o método GetHashcodeQ deve ser muito rápido a executar. Assim, não
se deverá complicar demasiado o código presente no mesmo.

Ao implementar este método, as seguintes regras devem ser seguidas:

Caso dois objectos sejam logicamente o mesmo (isto é, caso EqualsQ retome
verdadeiro), GetHashcodeQ deverá retornar o mesmo valor para ambos os
objectos;

GetHashcodeQ deverá retornar sempre o mesmo valor, independentemente das


modificações que aconteçam no objecto em causa. Assim, GetHashcodeQ deverá
ser baseado num campo imutável do objecto;

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

Para existir uma melhor performance, GetHashcodeQ deverá gerar números


distribuídos sobre toda a gama de valores inteiros, de acordo com os objectos que
são possíveis de existir.

Voltando ao nosso exemplo de Empregado, uma hipótese seria colocar o número do


bilhete de identidade do empregado como código hash. No entanto, por uma questão de
simplicidade e assumindo que um empregado nunca pode mudar de nome, o código hash
será simplesmente o hash do seu nome:
pufcflic "ove r ri dê int GetHashcodeQ
return Nome.GetHashCodeQ ;
l ____ .
A listagem. 8.1 apresenta o exemplo completo.
r
* Programa que Ilustra a implementação de EqualsQ.
*/
using System;
class Empregado
private string Nome;
public Empregado(string nomeDaPessoa)
Nome = nomeDaPessoa;

public override bool Equals(object other)


Empregado emp = other as Empregado;
i f (emp == null)
return false;
return (this.Nome == emp. Nome);

public override int GetHashCodeQ


return Nome.GetHashCodeQ ;

public override string ToStringQ


return Nome;

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) ;

Listagem 8.l— Comparação de objectos (ExemploCap8_l.cs)

Eis o resultado da execução deste programa:


:émpl "= Pecirò 'Bizarro
;emp2 = Pedro Bizarro
|(empl == emp2) = false
'empl. Equals(emp2) = true
iEmpregado.EqualsCempl, emp2) = true
ihashCempl) = -1910489738
..= ^191048973.8
Note-se, que é perfeitamente válido o método GetHashcodeQ devolver um número
negativo. Quando o código hash é utilizado, é responsabilidade da entidade que o utiliza
garantir que o valor se encontra numa gama interna aceitável, mesmo que este seja
negativo.

É 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.

- O método virtual EqualsQ compara objectos baseando-se no seu valor. No


ARETER entanto, por omissão, em classes que o herdem não fazendo o seu override,
EqualsQ realiza uma comparação baseado na referência dos objectos.
Comparação de
objectos " O método estático object.ReferenceEqualsQ verifica se duas referências
apontam para o mesmo objecto.

256 © FCA - Editora de Informática


CLASSES BASE

" Os operadores == e !=, tipicamente, comparam igualdade de referências. No


ARETER entanto, em certos casos excepcionais, a comparação é feita tendo por base
o valor dos objectos, string é um exemplo deste último caso.
Comparação de - Apenas em casos muito excepcionais deverá ser feita a redefinição dos
objectos
operadores == e ! =.
- Ao fazer o override do método virtual EqualsQ, o programador deve ter
cuidado para nunca lançar excepções, mesmo quando a referência que lhe é
passada é null.
" Não é necessário fazer a redefinição do método estático EqualsO, pois
este está preparado para chamar o método virtual EqualsQ de instância,
automaticamente.
~ Quando se faz a redefinição do método EqualsQ , deve-se sempre fazer a
redefinição do método GetHashCodeO .
- O método GetHashCodeO deve devolver um número único associado ao
objecto em causa. Caso não seja possível garantir que o número é único,
deverá existir o mínimo de colisões possíveis (isto é, dado um conjunto de
objectos, deverá ser muito improvável que os códigos hash. sejam iguais).
"Caso dois objectos sejam logicamente o mesmo (isto é, caso EqualsQ
retorne verdadeiro), GetHashCodeO deverá retornar o mesmo valor para
ambos os objectos.
~ GetHashCodeO deverá retornar sempre o mesmo valor, independentemente
das modificações que aconteçam no objecto em causa. Assim,
GetHashCodeO deverá ser baseado num campo imutável do objecto.
~ O método GetHashCodeO de string implementa um algoritmo de hashing
eficiente, podendo este ser usado para gerar códigos hash em classes
definidas pelo programador.

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.

No entanto, MemberwisedoneQ é declarado como protected em System.object. Ou


seja, não é possível chamá-lo directamente em classes definidas pelo programador:
* Érnpréq*adbÇ"Ána ^
^a]1 cópn a _ = emp.MemperwiseClonejOL.. _ ./Z. Erro de compilação!
O que acontece é que Memberwi secl oneQ faz urna cópia byte a byte do objecto sobre o
qual é chamado. Embora isto não constitua um problema em classes que apenas possuam
tipos valor (value types), em classes que possuem referências no seu interior, o que é
copiado é a referência em si, e não o objecto à qual ela se refere. Assim, pode tornar-se
muito perigoso utilizar este método. Tipicamente, se o programador deseja disponibilizar
um método de cópia numa classe sua, então, implementa a interface icloneable, que

© FCA - Editora de Informática 257


C#3.5

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.

258 © FCA - Editora de Informática


CLASSES BASE

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"" " """ ' " "

privãtre int[] iDProjectos;

1 publlc object cloneQ


Empregado copla = new Empregado(NomeDaPessoaD;
copia.IDProjectos = (int[]) IDProjectos.CloneQ ;

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.

~ MemberwiseCloneO efectua uma cópia byte a byte de ura objecto. Campos


ARETER valor são copiados directamente, as referências continuam a apontar para os
objectos que referenciam.
Cópia
de objectos ~ MemberwiseCloneO é declarado como protected. O programador que deseje
suportar cópias de objectos deverá implementar a interface ICl oneabl e.
" No método Cl one O , caso seja aceitável realizar uma cópia directa do objecto,
basta chamar base.MemberwiseCloneO • Caso seja necessária uma cópia profunda,
é da responsabilidade de Cl one C) implementar essa cópia.

8.2 CADEIAS DE CARACTERES


Iremos, agora, discutir alguns tópicos relacionados com manipulação de cadeias de
caracteres. Nomeadamente, começaremos por ver de que forma são lidas cadeias de
caracteres da consola, de que forma podem ser convertidas de e para números. Em
seguida, examinaremos os principais métodos de string e stringBuilder, formatação
de cadeias de caracteres e, finalmente, expressões regulares.

© FCA - Editora de Informática 259


C#3.5

8.2.1 LEITURA DA CONSOLA


Ao longo deste livro, temos utilizado extensivamente o método console.writeLineQ.
No entanto, uma questão que não examinámos foi de que formas podem ser lidas cadeias
de caracteres a partir de uma consola5.

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 ReadQ lê um carácter da consola, retornando um inteiro que representa o seu


código. Ao chamar este método, pode ocorrer uma system.lOException indicando que
houve um problema com o dispositivo de entrada/saída. Caso não existam mais caracteres
a serem lidos, é retornado -L

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.

Por exemplo, o programa seguinte lê sucessivamente linhas do teclado, mostrando-as no


ecrã:
íusihg"SystériÍ; ' "
'class Teste
{
• public static void MainQ
try
string linha;
do
{
linha = Console.ReadLineQ ;
if (linha != nul])
Console.Writet_ine("Lido: {0}", linha);
} while CC1inhaí=null) && Clinha!="fim"));
; catch (Exception e)
: Console.writel_ine("Erro ao ler da consola - Programa abortado");
Console.WriteLine(e.stackTrace);

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.

8.2.2 CONVERSÕES DE VALORES


É extremamente frequente ser necessário converter um número numa string e
vice-versa. A classe convert permite exactamente isso. Esta classe possui métodos
estáticos que permitem ao programador, converter números entre diversos tipos numéricos,
de s t ri ng para um certo tipo numérico e desse tipo numérico para s t ri ng. Por exemplo,
o seguinte código converte um inteiro (i nt) para uma st n" ng:
string dez = convert".TpStrín^ClQ);^_....' _ ""_.'.V" ~ "..„".". '.."_
Existem diversas versões overloaded do método ToStringQ. Estas versões levam como
parâmetro todos os tipos básicos do Common Type System (CTS), desde byte até
decimal.

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.

A tabela 8.2 apresenta os principais métodos da classe convert. No entanto, é de notar


que esta classe possui ainda muitos mais métodos de conversão. Por exemplo, existem
métodos que permitem converter datas, converter entre tipos numéricos, converter entre
formatos de cadeias de caracteres, e outros.

© FCA - Editora de Informática 261


C#3.5

CONVERSÃO DE strlng | CONVERSÃO PARA st ri ng


publlc statlc public static
bool cqnyert_.To_BooleanCstríng s) string Convert.ToString(bool b) •
public static public static
byte Convert.ToByteÇstring s) string convert.Tostring(byte b)
publlc static pubiic static
s byte Convert.ToSByteÇstring s) string Convert.ToString(sbyte b)
public static public static
short convert.Toint!6Cstn"ng s) \c string Convert.ToString(short s)
static
public static
ushort conyert.Touintl6(string s) ; string Convert.ToString(ushort s)
public static public static
int Convert.Tolnt32C_string s) string Convert.ToString(int i)
public static public static
irint Convert.ToUInt32(string s) , string Convert.ToStringCuint i)
public static public static
long Convert.ToInt64(string s) string Convert.TostringOong i)
public static public static
ulong Convert.Touint64 (string s) string Convert.ToString(ulong 1)
public static : public static
float Convert.ToSingle(string s) string Conyert.ToStringCfloat f)
public static public static
double Convert.ToDouble(string s) ' string Convert.TostringCdpuble d)
public static public static
decimal Convert.ToDecimal (string s) string Convert.ToString(decimal m)

Tabela 8.2 — Principais métodos da classe Convert

8.2.3 A CLASSE SYSTEM.STRING


Ao longo deste livro, temos vindo a utilizar a classe System.string. A palavra-chave
st n' ng constitui uma abreviatura para o nome completo desta classe.

A classe string suporta o operador + para concatenar cadeias de caracteres, o operador


== para as comparar, baseado nos seus valores e não nas suas referências, e também o
operador índice [] para aceder aos seus caracteres. Os caracteres de string começam-se
sempre a contar a partir de 0. O código seguinte concatena três cadeias de caracteres e
compara-as com uma terceira:
istnng "resúTfadò " = "ÒTa" + " " " + ""Mundo"; ""
jif (resultado — "ola Mundo")
Console.WriteLine("iguais");
else
Console.WriteLine("Diferentes");
O resultado da execução deste código é "Iguais".

Para obter o tamanho de uma string, utiliza-se a propriedade Length. No seguinte


exemplo, é construída uma nova string, em que as suas letras estão invertidas
relativamente à original.
'st ririg frase = "ola Mundo";'

262 © FCA - Editara de Informática


CLASSES BASE

for (int i=frase/Lerigth-Í; i>=0; T--)"


1nversa+= f rase [1];

,Console.Wr1tel_1ne("0r1g1nal: {0}", frase);


Console.WriteLlneC"inversa : {O}"., Inversa) ;_

Ao executar este exemplo, surge:


:0"ri gi nal: 0 1 á Mundo . . . . . . .
Inversa : odnuM alo _ _ _ ._ . J
Um ponto extremamente importante relacionado com este exemplo tem a ver com
performance. Os objectos do tipo string são imutáveis. Isto é, não existe nenhuma
operação que permita modificar o conteúdo de um objecto do tipo string. Isto acontece
por forma a que seja possível ao compilador fazer certas optimizações que seriam
impossíveis de fazer, caso os objectos de stri ng pudessem ser alterados.

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. ;

char this[int pôs] { get; } | Propriedade que obtém o carácter que se :


encontra na posição pôs. !

Int compareTo(string other) : Compara a string corrente com uma outra '
string.
static !
string Concat(string a, string b) Concatena duas cadeias de caracteres. ;

voi d • Copia um certo número de caracteres da i


CopyTo(int índice, char[] destino, string para uma tabela de caracteres de ;
int indiceDestino, int total) • destino, '"
Verifica se o finai da st ri ng coincide com uma
bool Endswith(string s) \t Indexof (string s)
outra string.
Encontra na string corrente a primeira
int Indexof (char c) ocorrência de uma certa string ou de um ;
certo carácter.
Retorna o resultado de inserir uma certa !
string Insert("int Índice, string s) string na posição indicada da string i
corrente. _ .
Encontra na string corrente a última
int Lastlndexof (string s)
int Lastlndexof (char c) [ ocorrência de uma certa string ou de um ;
certo carácter. ;
Retorna o resultado de remover um certo •
string RemoveCint inicio, int total) ' número de caracteres à string corrente/ a •
partir de um certo ponto.
Retorna o resultado de substituir todas as •
string ReplaceCstring a, string b)
string Replace(char a, char b) ocorrências de uma string (ou de um ;
carácter) por outra (ou outro), .
Retorna uma tabela das cadeias de caracteres
stringC] SplitCparams char[] delims) parciais da string original que estão :
delimitadas pelos caracteres passados como ;
argumento.
bool StartsWith(string s) Verifica se a string corrente começa com uma
certa cadeia de caracteres.
Retorna uma string parcial da string •
string SubStringCint posição,
int total) corrente que começa numa certa posição e tem ;
um certo tamanho.
string ToLowerQ Retorna a mesma string, mas em minúsculas, .
string ToUpperQ Retorna a mesma string, mas em maiúsculas.

string TrimC) Retorna uma string sem os espaços em


branco no início e no fim da string corrente.

Tabela 8.3 — Principais operações disponíveis na classe System.String

264 © FCA - Editora de Informática


CLASSES BASE

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.

8.2.4 A CLASSE STRINGBUÍLDER


Tal como referimos na secção anterior, por forma a que a plataforma .NET possa lidar
eficientemente com os objectos do tipo string, esses objectos têm de ser imutáveis.
Dessa forma, tanto o compilador como o CLR podem fazer coisas como detectar strings
idênticas e juntá-las apenas numa. Isso permite poupar espaço em memória e tornar os
acessos às cadeias de caracteres mais eficientes. Esta é apenas uma optimização básica
que pode ser feita. Existem outras. No entanto, quando um programa faz muitas
manipulações de caracteres com as suas strings, isso leva a que exista imensa actividade a
criar objectos e a realizar cópias entre eles, o que é um processo altamente ineficiente.
É aqui que entra a classe stringBuilder. Esta classe deve ser utilizada neste tipo de
situações e representa uma string mutável, isto é, cujos caracteres podem ser
individualmente alterados.

A classe s t r i n g B u i l d e r pertence ao espaço de nomes System.Text. Assim, é necessário


fazer a sua importação para a utilizar.

A diferença marcante entre s t r i n g B u i l d e r e String é que, no caso de s t r i n g B u i l d e r ,


a grande maioria dos métodos que apresentamos na tabela 8.3 actuam directamente sobre
a instância em causa. Isto é, em vez de retornarem uma nova string com a modificação
feita, modificam a própria stri ng onde é aplicado o método. Por exemplo, pode fazer-se:
StringBuilder f rase ~= hew' StringBuilder("olá Mundo!");
frase.Replace("Mundo", "Muno Santos");
1 Cpasple.wn"teLineC"{QÍ" J frase);

Ao fazer frase.ReplaceQ, modificou-se directamente a instância frase. Caso


utilizássemos objectos stri ng, teríamos de fazer:
string frase W "Olá 'Mundo! ";
frase - f rase. Replace( n Mundo" , "Nunq Santos").;
Neste caso, frase.ReplaceQ não modifica a instância de frase. Apenas retorna uma
string com a modificação feita. É necessário realizar a atribuição novamente para
frase. Como se pode ver, neste caso é criado um novo objecto, feita a cópia do antigo
substituindo a palavra "Mundo" e, finalmente, retomado a nova cadeia de caracteres. No
final, acabamos por perder a frase original, pois fazemos a atribuição de frase ao novo
objecto. Ou seja, sem S t r i n g B u i l d e r todo este processo é altamente ineficiente.

© FCA - Editora de Informática 265


C#3.5

Uma outra característica interessante de stríngBuilder é que possui construtores que


permitem reservar imediatamente algum espaço para utilização, na cadeia de caracteres
em causa. Por exemplo, ao fazer:
ÍStTTrigBuTldè.ri .s .'=. n_ew; StririgBuilderC20) ; '..'.'".. '._.'. .".." _."_'.'. II".'." '"".'..-
está-se a criar uma nova cadeia de caracteres vazia, mas que contém espaço pré-reservado
para conter pelo menos 20 caracteres. Na verdade, caso mais tarde este espaço seja
insuficiente, s irá crescer automaticamente.

Não apresentamos uma listagem dos métodos de s t r i n g B u i l d e r , pois são basicamente


os de s t ri ng, mas actuando sobre a variável de instância em causa.

8.2.5 FORMATAÇÃO DE CADEIAS DE CARACTERES


Ao invocar o método console.writeLineQ, tipicamente, utiliza-se uma string de
formatação, seguida de um conjunto de parâmetros a imprimir. Sempre que é encontrado
um elemento {pôs} na string de formatação, isso representa o parâmetro número pôs a
imprimir. Por exemplo, ao executar:
TritTà =~"2; ' """ "" " " " ' " "" "" ~~" " " """•
int b = 3; :
ÇpnspTe i wri.teklne.ClQ}_ x. £_!}. =.{!}. x .{0} =.{2}^ a,, b, a*b) i _....
surge no ecrã:
2"x* 3"="3 x* 7~= 6 "
Neste exemplo, {0} representa a variável a, {1} representa b e {2} representa a*b. No
entanto, em muitas circunstâncias, é útil ter maior controlo sobre a forma como os
argumentos são apresentados. Pode-se querer controlar o alinhamento da escrita dos
caracteres, se um carácter é apresentado como sendo uma letra ou como sendo o seu
código ASCII, o número de casas decimais que são apresentadas num número de vírgula
flutuante e assim sucessivamente.

Para controlar a forma como um determinado parâmetro é escrito, coloca-se uma


especificação de formato após o número do parâmetro, ainda entre chavetas. Por
exemplo, ao escrever-se:
const Tnt TOTAL ="20007 " " " '"
for (int i=0; i<=TOTAL; i+=200)
Cpnsole.WriteLíneO'* {0,4} *", i);
o resultado desta execução será:
* ' O * " ~ ' ' - -' ' -
* 200 *
* 400 *
* 600 *
.* 800 *
* 1000 *
* 1200 *
•* 1400 *
i*. 1600.* .. _.. ._ _ ...._.

266 © FCA - Editora de Informática


CLASSES BASE

:*.2QOO

Isto deve-se ao facto de estarmos a utilizar a especificação {0,4}. Estamos a mandar


imprimir o parâmetro O alinhado à direita, tendo 4 caracteres reservados para o número
impresso. Se desejássemos alinhar à esquerda, bastaria colocar o sinal - antes do 4:

isto resultaria em:


0 A
i* 200 "*
'* 400 '*
600 * ' . ' '
:* 800 ' '" JL. ' '

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.

A tabela seguinte mostra quais as especificações de formatação utilizadas mais


frequentemente.
ESPECIFICAÇÃO APLICA-SE A , SIGNIFICADO ; EXEMPLO , RESULTADO
valor monetário de ;
valores em \l ; 1.000,32€
C ; acordo com o país ; "{0:C}", 1000.32
local :
D inteiros inteiro genérico _ ; "{0:D}", 23 J 23

© FCA - Editora de Informática 267


C# 3.5

ESPECIFICAÇÃO ;| APLICA-SE A | SIGNIFICADO j EXEMPLO | RESULTADO


valores em
E notação científica " { 0 : E 2 } " , 13.322 1.33E+001 ,
geral
número em formato ;
F valores reais "{0:F2}", 13.322 ; 13.32 '
de vírgula fixa

"{0:G}", 13.322 13.322


geral (é utilizada a
valores em
G representação mais
geral
adequada ao número) ;
"{0:G} n , 13 13

valores em número de acordo


N "{0:N}", 100000 ! 100.000,00 .
geral com o país local

valores em
P percentagem ; "{0:P}", 0.43 43,00 % ;
geral

x | inteiros | notação hexadecimal "{0:X}", 1000 í 3E8

Tabela 8.4 — Especificação de formatação de cadeias de caracteres

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.

8.2.5.1 FORMATAÇÕES DEFINIDAS PEIJO PROGRAMADOR


Suponhamos, que queremos que os objectos de uma classe definida pelo programador
suportem diferentes especificações de formatação. Para conseguir esta funcionalidade,
utiliza-se a interface iformattable, que é exactamente o tópico desta subsecção.

Quando é necessário imprimir um certo parâmetro, o sistema verifica qual é o tipo de


dados associado ao parâmetro. Em seguida, é verificado se este implementa a interface
iformattable. Caso não implemente, é chamado o método TostringO sobre o objecto
em questão. Caso implemente, então, é chamado um método TostringO especial. O que
se passa é que a interface iformattable obriga a que seja implementado um método
TostringO com dois parâmetros:
;int^rTfacê~TrfõTmãffãb1'ê " ~~ """"" "~ '
string ToStri ng(string format, IformatProvider formatProvider) ;

O primeiro parâmetro representa a string de formatação, utilizada para formatar o


objecto (por exemplo, no caso de {0,10:F2} representa o F2). O segundo parâmetro

268 © FCA - Editora de Informática


CLASSES BASE

representa informação sobre o local do mundo para o qual o computador está


configurado6.

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".

O código seguinte implementa esta funcionalidade:


• class Empregado : Iformãttable ""
:{
s private string Nome;
private int Idade;
public EmpregadoCstring nomeDaPessoa, int idadeDaPessoa)
Nome = nomeDaPessoa;
Idade = idadeDaPessoa;
}
public string ToStringCstring format, IformatProvider formatProvider)
i f (format==null)
return Nome;
string formatUpper = format.ToUpperQ;
bool usaNome = false;
bool usaldade = false;
for (int i=0; i<formatUpper.Length; i++)
if (formatUpper[i] — "N")
usaNome = true;
: else if (formatUpper[i]™ "l")
usaldade = true;
else
throw new FormatExceptionC"Especificação não conhecida: " +
: format[i]);

6 Neste livro, não iremos fazer uso deste tipo de funcionalidade.


© FCA - Editora de Informática 269
C#3.5

strfrig '""resultado = '"


i f (usaNome)
resultado+= Nome;
1f (usaldade)
{
i f (usaNome)
resultado+= "/";
resultado+= Idade;

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.

ARETEK Ao formatar-se uma cadeia de caracteres, a forma de um parâmetro de formatação


&>^ é ijríimeroParâinetro, espaçoReseivado '.fonnatação}. númeroParâmetro indica
qual o parâmetro que se está a formatar, espaçoReseivado indica o número
Formatação de
cadeias de
de caracteres mínimo a ocupar com o parâmetro (um sinal menos indica
caracteres alinhamento à esquerda e ausência de sinal ou positivo indica alinhamento à
direita) e, finalmente, formatação indica formatação específica do tipo de
dados a utilizar. Caso não se queira especificar espaço reservado,
{númeroParâmetro \fonnatacao'} é uma notação alternativa.

27O © FCA - Editora de Informática


CLASSES BASE

ARETER " As principais especificações de formatação são:


C- currency, D- decimal value, E- exponencial, F- fíxed floating point,
G- general numerical value, N- local numérica! value, P™ percentage value,
Formatação de X- Hexadecimal value.
cadeias de
caracteres " Sempre que é necessário formatar um objecto pertencente a uma classe, é
verificado se esta implementa iformattable. Caso implemente, é chamado
o seu método TostringO que possui dois parâmetros. Caso não
implemente, é chamado o método TostringO sem parâmetros.
~ A interface iformattable requer que seja implementado um método com a
seguinte assinatura:
string Tostring (string format, iformatProvider formatProv);
format representa a especificação de formatação a ser utilizada.
formatProvider contém informação específica de formatação, como seja a
informação do país onde o programa se encontra a correr.
" No caso de não ser passada nenhuma informação de formatação, format
fica com o valor nul l.
" Caso exista um erro na especificação de formatação, deve ser lançada uma
excepção do tipo FormatException.
" Caso seja necessário formatar cadeias de caracteres estaticamente, utiliza-se
o método string.FormatC) .

8.2.6 EXPRESSÕES REGULARES


Ao examinarmos a classe string, verificamos que esta possui métodos que permitem
realizar algumas operações como encontrar cadeias de caracteres no seu interior e
substituir essas cadeias por outras. No entanto, existem muitas situações em que é
desejável ter mais flexibilidade do que simplesmente encontrar e substituir strings. Por
exemplo, suponhamos que temos uma string que pode ter sido lida de uma base de
dados:
string contas = "Péréi ra~ 4343 "èuros\n" +
: "Germano 12534 euros\n" +
: "Sacramento 212 euros\n"; ,
Esta string representa as contas bancárias de um conjunto de pessoas. Imaginemos,
agora, que queremos, de uma forma simples, extrair os valores para calcular o valor total
presente no ficheiro. Fazer isto com processamento simples de caracteres toma-se
complicado e fastidioso. Note-se, que nem todos os números têm o mesmo comprimento
e que não surgem numa posição fixa da string. É aqui que entram as expressões
regulares.

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

© FCA - Editora de Informática 371


C#3.5

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.

Um conceito fundamental das expressões regulares é o conceito de match. Ao escrever


uma expressão regular, estamos a tentar fazer com que essa expressão, isto é, que esse
padrão seja idêntico a uma certa frase sobre a qual a estamos a aplicar. Por exemplo,
existe um match entre "O custo é de 432 euros." E a expressão [0-9]+. O match
encontra-se no número 432.

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]+""'; "~ - — — — - • - — - - - -- ,

MatehColTectTon resultado = Regex.Matches(contas, padrão); , ' \>


forêajeh ÇMatchvnúmero i n resultado) ' •/-"-.
córrsole.WriteUneC"{0>". número.ToStríngO) : .
Regex é uma classe central nas expressões regulares. Esta classe, assim como todas as
classes relacionadas com expressões regulares, existe no espaço de nomes
System.Text.RegularExpressions.

Ao chamar o método estático MatchesQ, é passado como parâmetro, a string onde se


irão realizar as pesquisas e o padrão a pesquisar. Este método irá devolver uma colecção
de matches que correspondem aos padrões encontrados. A colecção resultante pode ser
iterada, utilizando o operador foreach. Assim, ao executar o código anterior, surge:
4343 ' ' ' -" ' =
12534
212 . ._. _ _ J

Se quiséssemos calcular o total presente nas contas bancárias, bastaria fazer:


string padrão = " [0-9]+ ";
MatchCollection resultado = Regex.Matches(contas, padrão); i
int total = 0 ;
foreach (Match valor in resultado)
total+= Convert.Toint32(valor.ToString());
Console.WriteLine("O total é: .£01 " j total) ;

Uma outra funcionalidade muito interessante das expressões regulares é permitirem


agrupar expressões, guardando-as como um conjunto. Vamos escrever uma expressão
regular que permita obter sucessivamente pares (nome, valor). A expressão \ representa
um carácter que não é espaço em branco. Assim, \s+ representa uma palavra. Se

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.

A nível de capturas, o símbolo $0 representa o match como uni todo, SI representa a


primeira captura, $2 a segunda e assim sucessivamente. Existem diversas formas de obter
estas capturas, mas uma forma muito útil é utilizando o método ResultQ de Match. Este
método leva como parâmetro uma string, substituindo nessa stríng, as ocorrências de
$n, em que Sn representa a captura número n, pelo valor correspondente. Vejamos o
seguinte código:
string padrão = @" (\s+) ([0-9]+) eufos"; "" ' ' •
MatchCollectlon resultado = Regex.Matches(contas, padrão)';
int total = 0;
•foreach (Match m In resultado)
; Console.WriteLine(m.Result("Nome: $1 - Valor: S2"));
total += convert.Toint32(m.Result("S2"));
'}

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

A • Inicio da st ri ng ou de '• @"AO caso é grave" . @"o caso é grave"


uma linha
Fim da string ou de uma \ linha @"o caso é grave"
$ '• @"o caso é graveS"

Um único carácter excepto @"aten.ão" ©"atenção"


' o\
@"cao"
0 carácter anterior está ;
©"caro" ,
* : @"car*o"
presente 0 ou mais vezes ©"carro"
0 carácter anterior está @"caro"
+ ' @"car+o" :
©"carro"
presente 1 ou mais vezes
?
0 carácter anterior está @"car?o" @"cao"
presente 0 ou 1 vezes @"caro"
Qualquer espaço em @"a a"
\; branco O/Af). .;
@"a\sa"
@"a\ta"
Qualquer carácter que não .
@"asa"
\ . seja um espaço em branco . @"a\Sa" ©"ala"
(\V\tO
\ ; @"ção\b" Qualquer palavra
Fronteira de uma palavra
terminada em "cão"
Qualquer posição que não .
W no meio de uma
\ ! seja fronteira de uma @"\BA\B"
palavra
palavra :

Qualquer dígito decimal i @"12"


\ ; @"\d\d" @"23"
..([0-9]) _ :
Qualquer carácter que não ;
\ ' seja um dígito decimal : @"\D" ; @"A"
CCAO-9]). ;
Qualquer carácter ; @"A"
\ alfanumérico @"\w" @"a"
([a-zA-zO-9]) . : @"4"
Qualquer carácter não :
\ ; alfanumérico j @"\w" : @"\t"
([Aa-zA-ZO-9]) ;

Tabela 8.5 — Principais elementos utilizados em expressões regulares

274 © FCA - Editora de Informática


CLASSES BASE

Tal como referimos, ao colocar um conjunto de caracteres entre parêntesis rectos,


indica-se que qualquer um deles é válido. Por exemplo, [aAbe] representa um dos
caracteres 'a1, C A', 'b' ou 'B', mas apenas um. Caso se separem os caracteres por hífen,
então, está a exprimir-se um intervalo. Por exemplo, [a-zA-zO-9] representa um carácter
alfanumérico sem acento.

É 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.

Finalmente, deixamos como desafio ao leitor perceber a seguinte expressão:


@"A(?<protoco1o>\w+) ://[A/]+?(?<porto>:\d+)?/". Esta expressão é utilizada para
extrair alguma informação de um URI9 (por exemplo, de http://www.deLuc.pt/ ou
hHp:/Avww.fca.pt:80/index.htnil).

ARETER " As expressões regulares representam padrões especificados sobre a forma


de regras. Estes padrões são utilizados para efectuar pesquisas e,
eventualmente, substituições em cadeias de caracteres.
Expressões
regulares ~ Ao chamar system.Text.Regex.MatchesO, é passado como parâmetro, a
string sobre a qual iremos trabalhar e o padrão a encontrar. Este método
irá devolver uma colecção de maíches que correspondem aos padrões
encontrados. O operador f oreach pode ser utilizado nessa colecção.
~ É possível agrupar pedaços de texto de uma expressão regular, utilizando
parêntesis C). Ao fazer um match da expressão entre parêntesis, esta é
guardada nurna captura.
" Ao fazer o match de uma expressão regular, $0 representa o match de toda
a expressão, $1 representa a primeira captura, $2 representa a segunda e
assim sucessivamente. Match.ResultO permite substituir estes valores
numa cadeia de caracteres passada como parâmetro.
~ É 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 vezes, é útil indicar que um certo conjunto de caracteres deve ser visto
corno um grupo, mas que não deve ser guardado enquanto captura. Para
isso, utiliza-se o símbolo ?: a seguir ao abrir parêntesis.

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.

Nesta secção, iremos examinar a API de Collections dísponibilizada na plataforma .NET.


O nome Collections, que traduziremos para colecções, deriva do facto de se tratar de uma
interface de programação para tratamento de colecções de objectos. Cada tipo de colecção
tem as suas particularidades, mas, globalmente, é sempre possível ver este tipo de
estruturas como um agrupamento de objectos.

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.

8.3. l A INTERFACE PRIMORDIAL." ICOLLECTTON


No capítulo anterior, onde falámos sobre a interface lenumerable e o operador foreach,
já contactámos brevemente com o tópico de colecções. Na verdade, lenumerable habita
no espaço de nomes System.Collections: o espaço de nomes principal das colecções.
Uma característica comum a todas as classes que são uma colecção é implementarem a

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

interface System.collections.lcollection. Esta interface encontra-se especificada da


seguinte forma:
using system.Collections; " "

interface Icollection : lenumerable


{ . ,
int Count { get; }
bool IsSynchronized { get; } ,r
object syncRoot { get; } '.
void copyTo(Array tabelaDestino, int indiceoestino);
•"} . . . :" . • . • / „ ,

Os pontos importantes desta definição são:

• icollection deriva de lenumerable, logo, é sempre possível utilizar o operador


f oreach para percorrer todos os elementos da colecção;

• Existe uma propriedade chamada count que permite obter o número de elementos
presentes numa colecção;

Existe um método copyToO que permite copiar os elementos de uma colecção


para uma tabela. Relativamente à tabela destino, é também especificado o índice
a partir do qual se começa a copiar.

Existem ainda outros elementos, como a propriedade IsSynchronized, que permite


descobrir se é seguro várias threads diferentes acederem simultaneamente à colecção, e
syncRoot, que permite sincronizar o acesso à colecção em threads diferentes. Para já, não
nos iremos preocupar com estes elementos. O tópico de threading e acesso concorrente é
discutido no próximo capítulo.

Sabendo que todas as classes que representam agrupamentos de objectos implementam


System.Collections.lcollection, vamos, agora, ver as principais classes disponíveis
na plataforma .NET. Tal como referimos anteriormente, existem dois grandes
agrupamentos de colecções, que residem em dois espaços de nomes separados:

As colecções tradicionais, que permitem guardar e reaver objectos que possuem


qualquer tipo de dados, correspondendo ao espaço de nomes system.
.Collections;

As colecções baseadas em genéricos, cuja utilização é recomendada, que


representam conjuntos de objectos que possuem todos o mesmo tipo de dados
base. Estas colecções existem em System.Collections.Generic.

A tabela seguinte mostra as principais classes existentes, na forma tradicional e na forma


de genéricos.

© FCA - Editora de Informática 277


C#3.5

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.

Tabela 8.6 — Principais colecções da plataforma .NET

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.

8.3.2 COLECÇÃO Usr E ARRAYLJST


Numa tabela normal, após o seu tamanho estar definido, não é possível mudá-lo. A ideia
da classe List (e ArrayList em System.collections) é muito simples: implementa
uma tabela dinâmica. Sempre que não existe mais espaço na tabela, esta cresce. Na
verdade, a sua implementação interna é muito semelhante à classe TabelaDinamica que
apresentámos no capítulo 7.

278 © FCA - Editora de Informática


CLASSES BASE

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); ;

for (int i=0; i<lista.Count; Í-H-)


int valor = lista[1];
, Console.<WriteLine("{0}: {1}"3 i , valor);
-} ........ . .. . . . . . . .....
Uma questão importante em List é que existe uma distinção clara entre a capacidade da
tabela e o número de elementos nesta presente. Embora, por exemplo, ao fazer:
;L1_st<irit> lista .= new List<int>CXO)_; _ _ . "_" _

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).

© FCA - Editora de Informática 279


C#3.5

A tabela 8.7 mostra os principais elementos de List<T> e ArrayList 1 1 .


MÉTODO/PROPRIEDADE DESCRIÇÃO
.ListO Cria uma tabela com a capacidade por omissão. ;
List(int cap) Cria uma tabela com capacidade para cap elementos. ;
Int
c= ount_{ g et; _>_
Número de elementos na tabela.
Int
capacity { get; } Capacidade da tabela.
T
this[i_ndex]I___{ get; set; } 0 elemento que se encontra na posição index.
Int Adiciona obj ao fim da tabela. Retorna o índice do local onde :
Add(T obj) o objecto foi adicionado. _'
Faz uma pesquisa binária na tabela para encontrar obj/ .
Int retornando o seu índice ou -1, se não for encontrado.
BinarySearch(T obj) A tabela tem de estar ordenada e os seus elementos têm de ,
implementar a interface Icomparabl e.
Void
, Çlearp ... Apaga os elementos da tabela.
BOOl
Contai ns (T obj) Verifica se obj se encontra na tabela.
Int Encontra a primeira ocorrência de obj na tabela, retornando '•
lndexof(T obj) o seu índice. Retorna -1 caso não esteja presente.
Void
insertCint index, T obj) Insere obj na posição index da tabela.
Void Remove a primeira ocorrência de obj da tabela (se
Remove(T obj) presente).
Void
RemoveAt(int index) Remove o elemento da posição i ndex. \a os eleme
Void
SortO implementar a interface Icomparabl e.
Void Reduz o tamanho interno da tabela para o número de
TrimExcessQ elementos presentes nesta ou para um tamanho adequado.

Tabela 8.7 — Principais elementos das classes List<T> e ArrayList

8.3.3 COLECÇÃO LJNKEDLJST


A classe LinkedList apenas existe na versão genérica e representa uma lista duplamente
ligada. As listas duplamente ligadas permitem armazenar os valores em nodos, em que
cada nodo possui uma referência, tanto para o nodo anterior, como para o nodo seguinte.
LinkedList apenas armazena as referências para o primeiro (First) e último nodos
(Last) da Hsta, sendo o resto da mesma implicitamente mantido pelas referências existentes.
A figura seguinte ilustra o conceito.

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

Anterior "Pat icio" Próximo


,.

,,

LAST • > Anterior "Cat« Tina" Próximo

\L

Figura 8.1 — Lista ligada contendo três elementos

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.

Para criar a estrutura visível na figura 8.1 basta fazer:


LinkedList<string>'lista = new LinkedList<string>O;
lista.AddFirst("Catarina");
lista.AddFirstC"Patrí cio") ;
lista.AddFirst("Brtmp");
Para iterar ao longo de uma lista duplamente ligada, basta utilizar um enumerador ou
então o operador f oreach:
foreach Cstrihg "nome i h lista)
Console.WriteLine(nome) •

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.

A tabela seguinte mostra os principais métodos e propriedades de Li nkedLi st<T>.


MÉTODO/PROPRIEDADE ,| DESCRIÇÃO
Li nkedLi st Q Cria uma lista ligada vazia.
Li nkedLi st Clenumerable<T> Cria uma lista ligada a partir de uma outra estrutura de
collection) dados enumera vel.
Int
count { get; } • Número de elementos na lista.

© FCA - Editora de Informática 281


C#3.5

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.

Encontra a primeira ocorrência de obj na lista, retornando


Li nkedLi stNode<T>
Find(T obj) • uma referência para o nodo correspondente . Retorna null
se o mesmo não estiver presente.
Li nkedLi stNode<T> -t
Addeef ore (Li nkedLi stNode<T> Insere obj antes do nodo especificado em riode. ;
node, T obj) ;
Li nkedLi stNode<T>
AddAf ter CLi nkedLi stNode<T> Insere obj depois do nodo especificado em node.
node, T obj) ;
BOOl Remove a primeira ocorrência de obj da lista (se :
Remove (T obj) = presente). Retorna se foi bem sucedido.
Void
RemoveFi rst() Remove o primeiro elemento da lista.
Void
RemoveLastQ Remove o último elemento da lista.

Tabela 8.8 — Principais elementos da classe Li nkedLi st<T>

8.3.4 COLECÇÃO BITARRAY


A classe BitArray implementa uma tabela de bits, ocupando apenas o espaço necessário
para o número de bits em causa. É certo que o programador pode criar uma tabela de bool e
utilizar essa tabela. No entanto, como cada bool ocupa l byte (8 bits\ tabela irá ser
8 vezes maior do que o necessário. BitArray utiliza eficientemente o espaço em causa,
usando apenas um bit por cada valor lógico. Dado que esta classe armazena sempre bits,
não existe uma versão genérica da mesma. BitArray pertence ao espaço de nomes
system . col l ecti ons.

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.

282 © FCA - Editora de Informática


CLASSES BASE

Para aceder a cada um dos bits, basta utilizar o operador []:


,tábe1a[10] - true; // Coloca" o "bít XD'a trúe"
;bop"l valor _- tabe1a[20J ; ._ // obtém, o^yalpr do bit 20
Nesta altura, não resistimos a utilizar esta estrutura de dados para implementar um
algoritmo clássico: o crivo de Eratóstenes. O crivo de Eratóstenes permite calcular, de
uma fornia relativamente eficiente, todos os números primos até um certo valor. Como
sabemos, um número primo é um inteiro superior a um que só é divisível por si e por um.

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

Figura 8,2 —Cálculo dos números primos até 100

A listagem 8.2 mostra uma implementação do crivo de Eratóstenes.


7 * ;
* cálculo de números primos utilizando o crivo de Eratóstenes,
*/
using System;
using System.Collections;
class ExemploCap8_2
const int DEFAUI_T__MAX = 50000;

© FCA - Editora de Informática 283


C#3.5

static vold Main(string[] args)

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) ;

// Criação da tabela; O e l não são primos


BitArray tabela = new BitArrayCn+1, true) ;
tabela[0] = false;
tabela[l] = false;
// Crivo de Eratostenes
int limiar = (int) Math.Sqrt(n) ;
for Cint i=2
if Ctabela[i] == true) // se i for primo
for (int j=i-í-i ; i<=n; j+= i) // Elimina os seus múltiplos
tabela[j] = false;

// Envia o resultado para o ecrã


for (int i=0; i<=n; i++)
if Ctabela[i] == true)
console. WriteC" {0,6}\ ", i) ;
Consol e . Wri teLi neC) ;
}

Listagem 8.2 —Cálculo de números primos, utilizando o crivo de Eratostenes (ExemploCap8_2.cs)

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.

Na tabela seguinte apresentamos os principais elementos da classe BitArray.


J MÉTODO/PROPRIEDADE | DESCRIÇÃO
l BitArrayCint n) | Cria uma nova tabela de n ô/felnlcializados a false.

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.

Tabela 8.9 —Principais elementos da classe BitArray

8.3.5 COLECÇÃO DICTIONARY E HASHTABLE


A estrutura de dados hashtable13 é provavelmente das estruturas de dados mais úteis e
mais utilizadas na maioria dos programas actuais. Uma hashtable permite armazenar e
pesquisar, de forma extremamente eficiente, elementos baseados numa chave de procura.

Imaginemos que um programador tem de armazenar em memória l 000 000 de


empregados. Caso o programador decida utilizar uma tabela, então, para encontrar um
dos empregados, utilizando pesquisa linear, é necessário percorrer em média 500 000
empregados. Se os empregados forem armazenados numa hashtable, tipicamente,
consegue~se encontrar o empregado correcto apenas com uma comparação!

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.

13 Também conhecida como "tabela de hash" e "tabela de dispersão".


© FCA - Editora de Informática 285
C#3.5 _

A desvantagem da estrutura hashtable é que, tipicamente, ocupa mais espaço em


memória do que uma tabela simples ou dinâmica. No entanto, hoje em dia, isto não
constitui uma forte limitação e o seu extraordinário desempenho é uma vantagem muito
importante.

Em .NET} existem duas classes que implementam hashtables: Dictionary e Hashtable.


Dictionary representa a versão, utilizando genéricos, que é a recomendada, enquanto
que Hashtabl e utiliza directamente referências object.

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}.

A utilização de Dictionary é muito flexível. Por exemplo, imaginemos que queremos


criar uma hashtable que guarda objectos do tipo Empregado, sendo os mesmos
pesquisados por nome. Para tal, bastaria fazer:
'Dfctlonary^Strihg^ÊmpregadoV empregados* ...... "
' ^ Qnc$iariary<string>Empregado>O > -
!Empre'gado: p1'5^ new Empregado("pedro Abreu'1);

empregâdos;[p.Npme]^= p; ...... _. . ._ _ _______ _ ..... .


para encontrar a ficha de um determinado empregado, bastaria fazer:
string nome ^ Console. ReadUneQ ; .
1f (empregados. Contai nsKeyCnome))
f * " * * - ' * '
Empregado emp - emp regados [nome] ; .•->."
Consotnè>.WnteLlneC"Encontrado: {0}", emp);
} " .... ........ . ....... ......... _„.. . . .;
Neste exemplo, fazemos uso explícito do método contai nsKey C), que permite verificai-
se uma certa chave está presente na tabela.

Finalmente, para apagar um elemento de uma hashtable, utiliza-se o método Remove C) :


pedro" Abreu") ;_"_.". ."..'."„".. ..'_:_!"_ _ \ _."!":_ .'.. .^•'í^-^-.^-í: .•-.
Existem alguns pontos importantes a não esquecer. Em primeiro lugar, o código de hash é
calculado sobre a chave e não sobre o objecto. Isto é extremamente relevante. Por
exemplo, implementar o método GetHashcodeQ de Empregado não adianta de nada, a
não ser que este seja utilizado como chave. No entanto, há que ter cuidado porque uma
chave deve ser um elemento que ocupe pouco espaço e, em princípio, não o objecto a
guardar. Outra questão muito importante é que, caso se tente colocar numa hashtable um
© FCA - Editora de Informática 287
C#3.5

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.

Tabela 8.10 - Principais elementos da classe Dictionary<Tkey,Tvalue>-e Hashtable

8.3.6 COLECÇÃO HASHSET


Um Hashset armazena um conjunto de valores simples, permitindo a sua pesquisa, de
forma extremamente eficiente. Um conjunto é uma colecção sem elementos duplicados,

288 © FCA - Editora de Informática


CLASSES BASE

não possuindo nenhuma ordem particular. Como o nome indica, a implementação desta
classe é baseada em hashtables.

O código seguinte cria um conjunto de números de telefone, em que os duplicados são


automaticamente eliminados.
Hashset<int> telefones = new HashSet<int>O ;
teTefpnrês.AddC91414414) ;
telefones.Add(91986800);
'telefones.AddC93923344);
telefones.Add(93923344);
telefones.AddC91986800) ;
Ao mandar imprimir o conteúdo de tel efones:
foreach (int telefone i n telefones)
Console.Wr1teLineC"{0}", telefone);
surge:

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; }

© FCA - Editora de Informática 289


C#3.5

(cont.)

Adiciona o elemento value ao conjunto. Retorna true se


bool :
Add(T value) o elemento ainda não se encontrava no conjunto, false
caso contrário.
void
Apaga todos os elementos do conjunto.
clearO...
BOOl
ContainsOr other) Determina se um elemento está presente no conjunto.
vond L - 1 Elimina, do conjunto, todos os elementos presentes em
ExceptwlthC
Ienumerable<T> other) other.
void Calcula a intercepção com outro conjunto, colocando o
IntersectwithC
Ienumerable<T> other) resultado no conjunto actual.
Retorna true se o conjunto actual está estritamente
contido no conjunto other, false caso contrário. Note-se
Bool
que para um conjunto estar estritamente contido, todos os
IsProperSubsetof C
lenumerable<T> other) seus elementos têm de pertencer ao segundo conjunto. No
entanto, não pode ser idêntico ao segundo conjunto.
Matematicamente, denota-se AcB.
Retorna true se o conjunto actual contém estritamente o .
conjunto other, false caso contrário. Note-se que para '
bool um conjunto conter estritamente outro, todos os elementos
Ispropersuperset0f( do outro necessitam de pertencer ao conjunto corrente. No
Ienumerable<T> other) entanto, o conjunto corrente necessita de ter pelo menos
mais um elemento que o outro. Matematicamente, denota-
se ADB.
bool Retorna true se o conjunto actual está contido no
issubsetof C ; conjunto other, ou lhe for idêntico; false caso contrário.
Ienumerable<T> other) '. Matematicamente, denota-se AcB.
bool Retorna true se o conjunto actual contiver o conjunto
issupersetofC other, ou lhe for idêntico; false, caso contrário.
lenumerable<T> other) Matematicamente, denota-se AaB.
bool Retorna true se os conjuntos tiverem elementos em
overlapsC
lenumerable<T> other) comum, false, caso contrário.
BOOl Remove o objecto value. Retorna true se o elemento
Remove(T value) estava no conjunto, false, caso contrário.
Bool
Retorna true se ambos os conjuntos forem idênticos,
setEqualsC
lenumerable<T> other) false, caso contrário.
voi d Altera o conjunto actual para que contenha a união dos
SymmetricExceptwith( elementos presentes em ambos os conjuntos, excepto os
lenumerable<T> other) que são comuns a ambos.
void
UnionWithC Altera o conjunto actual para que contenha a união dos
lenumerable<T> other) elementos presentes em ambos os conjuntos.

Tabela 8.11 — Principais elementos da classe HashSet<T>

8.3.7 COLECÇÃO SORTHDDlCTIONARY


Num objecto do tipo SortedDictlonary, é possível armazenar pares [chave, valor], tal
como se de oictionary se tratasse. Mas, ao mesmo tempo, existe a noção de ordem nos
29 O © FCA - Editora de Informática
CLASSES BASE

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.

Vejamos um exemplo simples. Imaginemos um programa que necessita de armazenar os


números de bilhete de identidade de um certo conjunto de pessoas. A qualquer altura,
pode ser necessário obter rapidamente o número do bilhete de identidade de uma pessoa,
assim como obter uma listagem alfabética de todas as pessoas existentes14. A estrutura
Sortedoictionary é ideal para este tipo de problemas.

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

Comparando com oictionary, Sortedoictionary é utilizado quando se necessita da


flexibilidade e da velocidade de uma hashtable, mas mantendo, ao mesmo tempo, os seus
elementos ordenados, de acordo com um certo critério. Em termos de velocidade,
sortedDicfionary é algo mais lento do que Dictionary se bem que, mesmo assim, seja
suficientemente rápido para a maioria das utilizações comuns15.

Consideremos, agora, a questão do ordenamento dos elementos. Quando é utilizada a


classe Sortedoictionary, os objectos que são utilizados como chave têm de
implementar a interface icomparable. Esta interface especifica um método que permite
comparar o objecto corrente com um outro objecto, obtendo uma relação de ordem:
interface system.icomparable
í
// Devolve O se forem Iguais, <0 se o objecto corrente for menor do
// que aquele com que está a ser comparado e >0 se for maior.
Int CompareTo(object other);

O método compareToQ é chamado sobre os objectos que constituem a chave, de tal forma
a ser possível ordená-los. Este método deverá devolver O caso os objectos sejam
idênticos, um valor menor do que O caso o objecto seja menor do que o que é passado
como parâmetro e um valor positivo caso seja maior. Este método deverá lançar uma
ArgumentException caso other não seja do mesmo tipo do que o objecto em causa e,
caso seja passado nul l como argumento, deverá devolver um valor positivo.

As classes básicas da plataforma .NET, incluindo os tipos valor e a classe string,


possuem icomparabl e implementado. Em st n" ng, é feita uma comparação lexicográfica
entre cadeias de caracteres e, no caso dos tipos valor, é implementada uma comparação
simples entre os seus conteúdos.

Caso o programador não queira ordenar um sortedoi cti onary de acordo com o método
compareToQ das suas chaves, então deverá indicar no construtor de SortedDictionary
um objecto que implemente a interface icomparer. Esta interface requer que sejam
implementados diversos métodos:
interface System.Collections.lcomparer<t>
/-' compara dois valores, retornando l, se a>b; -l se a<b e O se a==b */
int Compare(T a, T b) ;
/* Retorna se a==b */
bool Equals(T a, T b ) ;
/* Retorna o código de hash de um objecto */
int GetHashCode(T obj) ;

15 Internamente, SortedDicfionary é implementado corno sendo uma "árvore binária equilibrada", do tipo
"vemelha-preta". Este tipo de estruturas, tipicamente, demora um tempo proporcional a Iog2(N) a encontrar
uni elemento, em que N é o número de elementos existentes. As hashtables, tipicamente, demoram um
tempo constante.
292 © FCA - Editora de Informática
CLASSES BASE

Felizmente, não é necessário o programador fazê-lo explicitamente. Em .NET, já existe


uma classe abstracta tendo o mesmo nome da interface, icomparer<T> J que fornece uma
implementação por omissão para estes métodos. Esta classe pode ser utilizada para
implementar de forma fácil e rápida, comparadores entre objectos do mesmo tipo. Por
exemplo, suponhamos que queremos ordenar as pessoas do exemplo anterior não por
ordem alfabética, mas sim pelo comprimento do seu nome. Para isso, ao criar tabel a,
faríamos:
SortedDictionary-çstringjinl^ tabela =
new.'SprtedDictionary<stn"ng,int>Cnew TamanhoNomecomparer C));
em que TamanhoNomecomparer seria definido da seguinte forma:
class TamanhoNomecomparer : System.Collections.Generic.icomparer<stríng>
public int Compare(string nomeA, string nomeB)
int tamanho = nomeA.Length - nomee.Length;
i f Ctamanho != 0)
return tamanho;
else
return nomeA.compareTo(nomeB);
> .... _ :
O método compareQ começa por calcular a diferença de tamanhos das string passadas
como argumento. Caso tenham tamanhos diferentes, é retornada a diferença. Essa
diferença será maior do que O, caso a primeira string seja mais comprida do que a
segunda, e menor do que O, caso seja mais pequena. Para cadeias de caracteres com o
mesmo tamanho, é retornado o resultado da comparação alfabética entre as duas.

Utilizando este comparador, ao executar o código, vem:


Andreia Reis " 15003244
Rui Oliveira 12992334
Catarina Reis . 14235455
Paul o Marques 11345465
Carlos, Bernardes 10609129
As pessoas estão ordenadas pelo tamanho do nome.

Note-se que, na classe, TamanhoNomecomparer a implementação foi um pouco


simplificada. Nomeadamente, não foram implementadas as relações de comparação com
referências nulas. Caso um objecto seja comparado com uma referência nula, por
convenção, o resultado é sempre positivo. Apenas não o fizemos a fim de simplificar o
exemplo e por sabermos, à partida, que este tipo de comparações não iria ocorrer neste
programa. Em código escrito para ambientes de produção, seria necessário uma
implementação mais cuidada.

Antes de apresentarmos a tabela com os principais métodos de


sortedDictionary<Tkey ] Tvalue>, queremos chamara atenção para o facto de existirem
duas interfaces Icomparer. icomparer simples reside no espaço de nomes
© FCA - Editora de Informática 293
C#3.5

system. Gol lections, enquanto a sua versão genérica, que usámos nos exemplos acima,
reside em System.Collections.Generic. Quanto à interface icomparable, ao contrário
do que seria de esperar, reside em System.

A tabela 8.12 mostra os principais métodos da classe SortedDictionary.


MÉTODO/PROPRIEDADE DESCRIÇÃO
SortedDictionary () Cria uma nova tabela ordenada.
SortedDictionary ( Cria uma nova tabela ordenada/ sendo a ordenação
Icomparer<T> comparar) especificada_pelo objecto comparer.
Int
Count { get; > : Número de elementos na tabela.
Obtém ou adiciona um elemento à tabela, utilizando a
chave key. Caso já exista um elemento com a chave
Tvalue apresentada, este é substituído. Os objectos são
th1s[Tkey key] { get; set; } guardados de forma ordenada na tabela. Caso se tente
obter um objecto não presente na tabela, é lançada uma
excepção.
sortedoi cti onary .
KeyCol 1 ecti on<Tkey , Tval ue> Obtém uma lista de todas as chaves presentes na tabela.
Keys { get; }
SortedDi cti onary . Obtém uma lista de todos os objectos presentes na
Vai ueCol 1 ecti on<Tkey , Tval ue> tabela. A ordem dos objectos é a correspondente à
Values { get; } _ utilizada na propriedade Keys.
Adiciona o elemento value à tabela, utilizando a chave '
Void key. Caso já exista um elemento com a chave
Add(Tkey key, Tvalue value) apresentada, é lançada uma ArgumentException. Os
objectos são guardados de forma ordenada na tabela.
Void
Apaga todos os elementos da tabela.
ClearQ.
BOOl Determina se uma determinada chave está presente na
Contai nsKeyCTkey key) tabela. Exactamente igual a Contai ns Q.
Bool Determina se um determinado objecto está presente na
Contai nsValue (Tvalue value) tabela.
Bool Remove o objecto associado a key, assim como a
Remove (Tkey key) respectiva chave. Retorna se foi bem sucedido.
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 Dictionary.)

Tabela 8.12 —Principais elementos da classe sortedDi ctionary<Tkey,Tvalue>

8.3.8 COLECÇÃO SoRTEDLJsrr


A classe SortedList, em termos de interface, é muito semelhante a SortedDictionary.
De facto, todos os exemplos apresentados na secção anterior funcionam perfeitamente ao
mudar-se SortedDi cti onary<stri ng, i nt> para sortedLi st<stri n g , i ntx Isto

294 © FCA - Editora de Informática


__ CLASSES BASE

aplica-se, inclusivamente, à questão da utilização de diferentes métodos de ordenamento


das chaves (i. e. comparadores).

A principal diferença existente é que, enquanto Sortedolctlonary é internamente


implementado como sendo uma árvore binária, sortedList é implementado como duas
tabelas, uma para as chaves e outra para os valores. Ambas as tabelas são mantidas
sempre ordenadas, de acordo com a ordem das chaves. Isto quer dizer que sempre que se
insere ou apaga um elemento numa SortedList, tal é uma operação relativamente
pesada, uma vez que implica deslocar um conjunto de elementos para abrir espaço para o
elemento que se vai introduzir ou consolidar os elementos existentes, quando se apaga um
elemento. Quando é necessário armazenar milhares de elementos, a discrepância de
velocidade entre SortedDictionary e SortedList é abissal.

Em termos de pesquisa, a performance de ambos é semelhante, uma vez que é usada


pesquisa binária sobre as chaves. A única funcionalidade extra que SortedList traz,
relativamente a SortedDictionary, é permitir aceder imediatamente a qualquer
elemento, por índice, em tempo constante. Uma vez que SortedList é implementada
usando tabelas, o acesso por índice é trivial. Vejamos um pequeno exemplo de acesso por
índice.

Para criar, preencher e aceder a uma sortedLl st, o processo é o habitual:


sòrtedLn.st<stn"ng^int>" tabe>la?= hew SbrtédLfst<string",int>O"Í '. ' ' :
tabe]a["-p.aulo Marques 11 ] = 11345465; -'
tabela ["cari os Bernardes"] = 10609129;
tabela["catarina Reis"] = 14235455;
tabela["Rui .ollvel rã"] - 12992334;
:tabelá["Ãndre1a Reis"] = 15003244;
foreach (K.eyValuepa1r<str1ng,1nt> pessoa In tabela)
Consp1e.Wr1teLlne("{0}_ \ {!}_", p_essoa.Key, pessoa. Vá] ue)^
Mas, se for necessário aceder por índice, pode-se utilizar as propriedades Keys e vai ues,
que retornam listas acessíveis por valor (111 st). Por exemplo, o código seguinte imprime
as pessoas presentes pela ordem inversa da qual se encontram em tabel a:
for (Int 1=tábela.count~T; 1>=0~; 1--) .......... ~~
'{ . - . • - . - - .
• c.onsol'e.Wn'teLlne("{0} \ {l}",
}... . . . . . . _ _............._ _ ......
O resultado desta operação é:
Rui oliveira.
Paulo
. -Bernardes
Andreia .Reis ........
Outra funcionalidade importante é que SortedList permite obter o índice associado a
uma determinada chave ou mesmo valor. Por exemplo, pode escrever-se:
1nt_pos = tab'elà7ínffèx"õfKeyí^ /."""_. . . ....;
© FCA - Editora de Informática 295
C#3.5

: Console.WriteLine("Andreia Reis: {0}", tabela.Values[pôs]);


else
: Console,_WriteLlneC n Andreia Reis: NÃO ENCONTRADO!");

O método indexof Key() retorna -l caso a chave não esteja presente.

Regra geral, é aconselhável utilizar a classe sortedoictionary, pois a su&peifonnance,


na maioria dos casos, é muito superior a SortedList. No entanto, caso o acesso por
índice seja um requisito, então, pode-se recorrer a esta última. A próxima tabela resume
os principais métodos de sortedList<Tkey ] Tvalue>.
MÉTODO/PROPRIEDADE DESCRIÇÃO
sortedunst () Cria uma nova tabela ordenada. _ _ _
SortedList(Icomparer<T> Cria uma nova tabela ordenada, sendo a ordenação •
comparar) especificada pelo objecto comparer.
Int
Count { get; } !
Número de elementos na tabela.
Obtém ou adiciona um elemento à tabela, utilizando a chave
key. Caso já exista um elemento com a chave apresentada,
Tvalue
this[Tkey key] { get; set; } este é substituído. Os objectos são guardados, de forma
ordenada, na tabela. Caso se tente obter um objecto não
presente na tabela, é lançada uma excepção.
Obtém uma lista de todas as chaves presentes na tabela. 0
ilist<Tkey> ;
Keys { get; } método retorna n i st, uma tabela que respeita a ordem
presente em SortedList.
Obtém uma lista de todos os objectos presentes na tabela. '
I11st<Tvalue> :
Values { get; } :
0 método retorna ilist, uma tabela que respeita a ordem
presente em SortedList.
Adiciona o elemento value à tabela, utilizando a chave
Void key. Caso já exista um elemento com a chave apresentada,
Add(Tkey key, Tvalue value) é lançada uma ArgumentException. Os objectos são
guardados de forma ordenada na tabela.
Void
Apaga todos os elementos da tabela.
ClearO ;
Bool Determina se uma determinada chave está presente na
Contai ns Key (Tkey key) tabela. Exactamente igual a Contai ns C).
Bool : Determina se um determinado objecto está presente na
Contai nsvalue (Tvalue value) tabela.
Retorna o índice (número de ordem) em que se encontra a
Int :
indexofKeyCrkey key) chave na tabela ou -1 se não encontrado. Pode ser usado
em conjunto com a propriedade Keys.
Retorna o índice (número de ordem) em que se encontra o
Int
lndexofValue(Tvalue value) !
elemento na tabela ou -1 se não encontrado. Pode ser
usado em conjunto com a propriedade vai ues.
Bool :Remove o objecto associado a key, assim como a respectiva
Remove (Tkey key) : chave. Retorna se foi bem sucedido.
voi d ; Remove o objecto que se encontra em index, assim como
RemoveAt(int index) 1 a chave associada.
Void ;
SetByindexCint index, object ; Altera o objecto que se encontra num certo índice para um
o) novo valor. A chave mantém-se.

296 © FCA - Editora de Informática


CLASSES BASE

(cont.)
MÉTODO/PROPRIEDADE DESCRIÇÃO
Tenta obter o valor associado à chave key. Caso seja
bool possível/ o mesmo é devolvido em value/ retornando
TryGetva"lue(Tkey key, ; true. Caso não seja possível, retorna false, colocando em
out Tvalue value) value/ o valor por omissão para esse tipo de dados. (Este
método só existe na classe Dictionary.)
void Reduz o tamanho interno da tabela para o número de
TrímExcessQ elementos presentes nesta ou para um tamanho adequado.

Tabela 8.13 —Principais elementos da classe SortedList<Tkey,Tva1ue>

8.3.9 COLECÇÃO QUEUE


Comparada com as classes anteriores, a utilização de Queue é bastante simples. Queue
representa uma fila de dados. Existem dois métodos principais associados a Queue: o
método EnqueueQ, que permite adicionar um objecto à fila, e o método DequeueQ, que
permite retirar um objecto da fila. Os objectos são guardados pela ordem que foram
introduzidos, sendo retirados por essa ordem. Isto é, o primeiro objecto a entrar é o
primeiro a sair.

Vejamos um exemplo simples. Numa repartição pública, as pessoas são atendidas por
ordem de chegada. Assim, ao chegarem três pessoas, estas são colocadas na fila:
Quèué<stn'hg> filaDéEspèra~ =~~hew ~Queue<string>OT
filaDéEspèra.Enqueue("Paulo Simões");
filaDeÈspena.Enqueue("Luis silva") ;
fi1aDeEspera.EnqueueC n Edmundo _Mpnteiro");

se agora retirarmos as pessoas:


while (filaDeEspera.Count >~0)"~" "

strinq -próximo = fil aoeEspera. DequeueQ ;


Consol'e.WriteLineC"Próximo a ser atendido: \ {0}", próximo);

estas irão aparecer por ordem:


Próximo a ser atendido: Paulo Simões - .
próximo a ser atendido: LUÍS Silva ' ^
próximo a ser atendido: .Edmundo^Monteiro
Note-se que o método DequeueQ remove o próximo objecto da fila. Caso se queira
apenas "espreitar" qual o próximo objecto, sem o remover, deve-se utilizar o método
PeekQ.

A tabela 8.14 apresenta os principais elementos da classe queue<T>.


| MÉTODO/PROPRIEDADE f DESCRIÇÃO
QueueQ Cria uma nova fila.

© FCA - Editora de Informática 297


C#3.5

(com.)
int
çount { get; > Número de elementos na fila. j
void
ClearQ Apaga todos os elementos da fila. :

Determina se um determinado objecto está presente na fila, ;


Bool
contai ns (T obj) Este método não é eficiente uma vez que realiza uma :
pesquisa linear na fila. _ _ _ _i
Remove e retorna o objecto que se encontra no início da :
T
DequeueQ fila. Caso a fila esteja vazia, é lançada uma .
invalidoperationException. ;
Void
Enqueue(T obj) Adiciona um objecto ao finai da fila. ;
Retorna o objecto que se encontra no início da fila sem o ;
T
PeekQ remover. Caso a fila esteja vazia, é lançada uma ;
_lnval i doperati onExcepti on.
void Reduz o tamanho interno da fila para o número de :
TrimExcessQ '. elementos presentes nela ou para um tamanho adequado.

Tabela 8.14— Principais elementos da classe Queue<T>

8.3. l O COLECÇÃO STACK


A classe stack é semelhante a Queue, mas a ordem de entrada é inversa à de saída. Isto é,
o último elemento a entrar na fila é o primeiro a sair. Ou seja, implementa uma pilha de
objectos.

Para adicionar um elemento à pilha, é utilizado o método pushQ. Para retirar um


elemento, é utilizado o método PopQ. O método PeekQ pode ser utilizado para examinar
o topo da pilha sem a modificar. Eis um pequeno exemplo de utilização:
;stàck<striríg> pílhãDeLTvrÕs "~= "héw stack<string>Q;
pilhaDeLivros.Push("c# - curso Completo");
p11haDeLivros.push("A Insustentável Leveza do ser"); :
pilhaDel_1vros.push("o Estrangeiro") ;

;while (pilhaDeLivros .count > 0)

string livro = pilhaDeLivros.PopQ;


Console.WriteLine("Retirei <{0}> da pilha de livros.", livro); :

Ao executar este código, surge:


Retirei <ò Êstrãhgeí" ro>" dã~píTha de livros. " "
Retirei <A Insustentável Leveza do Ser> da pilha de livros.
Retirei <C# - Curso Cpmpjeto>_da pilha de livro_s,. _
Ou seja, os livros aparecem por ordem inversa da qual foram adicionados à pilha.

A tabela seguinte sumaria os principais métodos de stack<T>.

29 S © FCA - Editora de Informática


CLASSES BASE

MÉTODO/PROPRIEDADE DESCRIÇÃO
StackQ ! Cria uma nova pilha,
int
Count { get; } Número de elementos na pilha. :
void
Apaga todos os elementos da pilha.
ClearO
Boo] j
Contai ns (T obj) Determina se um determinado objecto está presente na pilha.
Retorna o objecto que se encontra no topo da pilha sem o
T
remover. Caso a pilha esteja vazia, é lançada uma
PeekQ
Inval i doperati onExcepti on.
Remove e retorna o objecto que se encontra no topo da pilha.
T
Caso a pilha esteja vazia, é lançada uma
PopO
inval i doperati onExcepti on.
Voi d ;
Push(T obj) Adiciona um objecto ao topo da pilha.

Tabela 8.15 — Principais elementos da classe stack<T>

8.3.11 RESUMO DAS COLECÇÕES


Nesta secção sobre colecções, optámos por não criar uma tabela "pontos a reter" por cada
classe, uma vez que isso apenas iria replicar a informação presente nas tabelas referentes
a cada classe. No entanto, decidimos resumir a informação que, como um todo, é
importante reter sobre o tópico de colecções.

ARCTER " As classes associadas às colecções implementam diversas estruturas de


dados úteis ao programador. Actualmente, estas estruturas de dados existem
em dois espaços de nomes System. col l ecti ons e system.
Colecções
.CoIlections.Generic. É aconselhável utilizar as últimas, uma vez que
utilizam genéricos, permitindo executar o código de forma mais rápida,
segura e menos verbosa.
~ Todas as colecções implementam os métodos de icol l ecti on e os de
lenumerable. Consequentemente, é sempre possível chamar os seguintes
métodos/propriedades dessas classes: count, que obtém o número de
elementos na colecção; CopyToO, que copia a colecção para uma tabela
unidimensional; e GetEnumeratorQ, retorna um enumerador que permite
percorrer a colecção. (Logo, também é possível utilizar o operador foreach
com todas as colecções.)
~ As principais colecções são:
~ List<T>, que funciona como uma tabela dinâmica cujo tamanho cresce
automaticamente quando já não existe espaço para novos elementos.
~ Linkedi_ist<T>, que implementa uma lista duplamente ligada,
armazenando objectos linearmente.

© FCA - Editora de Informática 299


C#3.5

ARETCR B-itArray, que armazena, uma representação compacta de bits. Funciona


como uma tabela de bits em que cada bit representa um bool.
Colecções D-ictionary<Tkey J Tvalue>, que representa uma colecção de pares
{chave, valor}. A sua organização é baseada na chave, sendo o acesso aos
seus elementos muito eficiente.
" SortedDlctionary<Tkey,Tva1ue>, que representa uma colecção de pares
[chave, valor}, que se encontra ordenada por chave, sendo muito eficiente,
não só a armazenar os objectos, como a aceder-lhes.
~ HashSet<Tvalue>, que representa um conjunto de valores, não havendo
repetições. Suporta um conjunto de operações normais em conjuntos.
~ SortedListxTkey,Tvalue>, que representa uma colecção de pares
{chave, valor}, que se encontra ordenada por chave, sendo implementada
como duas tabelas ordenadas. É possível aceder aos membros da colecção
através de chave ou por índice. A sua performance, em inserções e
remoções, é inferior a SortedDictionary.
• Queue<T>, que representa uma colecção de objectos em que o primeiro
objecto a ser colocado na colecção é o primeiro a sair (fila).
" stack<T>, que representa uma colecção de objectos em que o último
objecto a ser colocado na colecção é o primeiro a sair (pilha).

8.4 FICHEIROS E STREAMS


Nesta secção, iremos discutir de que fornia se acede a ficheiros na plataforma .NET e o
principal mecanismo associado a estes: as streams.

Na plataforma .NET, os ficheiros são representados por objectos. No entanto, ao contrário


de linguagens como o C em que as operações de leitura e escrita em ficheiro são directas,
em .NET, a leitura e escrita de ficheiro é abstraída em termos de streams. Uma stream
representa um fluxo de informação. Este fluxo de informação vem de algum lado, não
importa de onde, ou, está a ser dirigido para algum lado. Na verdade, o conceito é
genérico, sendo aplicável a imensas situações. O conceito de stream é utilizado
extensivamente na plataforma .NET. Por exemplo, quando se está a ler dados de um canal
de rede, esse canal é abstraído em termos de uma stream. Quando se está a escrever, a
mesma coisa.

Antes d& analisarmos mais profundamente o acesso a ficheiros e o conceito de stream,


vamos ver de que fornia é possível manipular ficheiros e directórios. Um ponto a não
esquecer é que todas as classes relacionadas com entrada e saída16 se encontram no
espaço de nomes System. 10, pelo que é necessário fazer a sua importação.

Chama-se operações de entrada e saída a chamadas que envolvem a manipulação de dados de e para
dispositivos periféricos. Um exemplo clássico é o acesso ao sistema de ficheiros de um disco.
30O © FCA - Editora de Informática
CLASSES BASE

8.4. l GESTÃO DO SISTEMA DE FICHEIROS


Para manipular o sistema de ficheiros, existem diversas classes que têm de ser utilizadas.
Estas classes representam estruturas como ficheiros e directórios e a informação a eles
associada. As classes relevantes são:

MarshalByRefobject é a classe base de todas as classes da plataforma .NET que


podem ter referências em outras máquinas. Neste livro, não nos iremos preocupar
com esse tipo de situação e relativamente à manipulação de ficheiros, nunca
iremos utilizar esta classe directamente. Apenas a mencionamos porque as classes
que representam ficheiros e directórios derivam desta classe, uma vez que podem
ser acedidos em máquinas remotas;

Fil esysteminfo é a classe base que representa qualquer objecto (ficheiro ou


directório) no sistema de ficheiros;

Fileinfo e F i l e permitem examinar e manipular ficheiros em disco. Fileinfo


representa um ficheiro em concreto, enquanto File apenas contém métodos
estáticos para realizar operações sobre ficheiros. No entanto, a mesma
funcionalidade é coberta em ambas as classes. Fileinfo é mais prático caso se
esteja a realizar múltiplas operações sobre um ficheiro;

Directorylnfo e Directory permitem examinar e manipular directórios em


disco. A situação é análoga ao caso anterior. Di recto ry apenas contém métodos
estáticos, enquanto Di recto rylnf o representa um directório em particular, sendo
possível realizar diversas operações sobre o mesmo;

path é uma classe utilitária que permite realizar operações sobre cadeias de
caracteres que representam nomes de ficheiros. Por exemplo, é possível
determinar directamente se a extensão de um ficheiro está ou não incluída numa
string que representa o nome de um ficheiro, combinar cadeias de caracteres
que representam directórios e ficheiros ou descobrir qual o directório temporário
do disco.

A figura 8.3 ilustra esta hierarquia de classes.

© FCA - Editora de Informática 301


C#3.5

Figura 8.3 — Hierarquia de classes correspondente ao sistema de ficheiros

Tal como referimos anteriormente, pilelnf o e Flle são similares, assim como
Dl rectoryinf o e Dl rectory. Concretizando, tanto é possível escrever:
iFLleTMõv^^ @"c:Xter"P\xp_tn...txt.'l3.;."_. ."J_."'.'.'".'.'.-_.Vr~"!' "•
para alterar o nome do ficheiro "c:\temp\xpto.txt" para "c:\temp\xpti.txt", como é
possível escrever:
fFilèíhfo xptò"= nèw Fnelnfo"Ç@"c:\temp\xpto.txt II >r
[Xptg_.MpyeTo^C©"c:\temp.\xpti.. txt") ;
0 efeito é -exactamente o mesmo.

Uma questão muito importante é que antes de realizar uma operação sobre um ficheiro
(ou directório), é aconselhável verificar se este existe. Isso pode ser conseguido,
utilizando a propriedade Fll elnf o. EXT sts ou, caso se esteja a utilizar File, através do
método estático Fi 1 e . EXI sts Q . Por exemplo:
iFilelriTõ xpto = new Fi l èlnfo(@" c:\tlimp\xpto.tx~t"} ; ..... "
1 f (xpto.Exists)
L . xptp_.MpveTp(@"c:\temp\xpti .
Apesar deste tipo de precaução, existe, ainda, um conjunto vasto de formas de MoveToQ
falhar. Por exemplo, o ficheiro pode ter sido apagado entre a altura em que foi feito o
teste e a altura em que MoveToQ é executado, o utilizador pode não ter permissões para
ler o ficheiro, pode existir um erro ao ler o ficheiro do disco e assim sucessivamente. O
que isto quer dizer é que existe uma panóplia de excepções, que podem ser lançadas em
virtude de algo correr mal. A maior parte das excepções derivam de
System. 10. lOException, mas existem algumas excepções, que não estão directamente
relacionadas com entrada/saída, que podem ser lançadas. Por exemplo, caso o utilizador
não tenha permissões para aceder a um ficheiro, é lançada uma System .
.security.SecurityException. Assim, deve proteger-se este tipo de operações por um
bloco try-catch:

3O2 © FCA - Editora de Informática


CLASSES BASE

; try
•í
Filelnfo xpto = new FllelnfoC@"c:\temp\xpto.txt"); i
1 i f (xpto.Exists)
[ xpto.MoveTo(@"c:\temp\xpti.txt");
! else ;
Console. Writel_ine(@"o ficheiro c:\temp\xpto.txt não existe! ") ; \h (Exce

Note-se que o facto de estarmos a apanhar as excepções não invalida que se faça um teste
explícito à existência do ficheiro. Tal como discutimos anteriormente, as excepções
devem ser utilizadas em situações de erro e não em situações em que um simples teste
basta. De facto, é tão comum um utilizador enganar-se a escrever um nome de um
ficheiro que isso deve ser verificado com um teste simples, explícito, e não com um
mecanismo de tratamento de erros genérico como as excepções.

Após estas advertências, iremos listar as propriedades e os métodos disponíveis em


Filelnfo e olrectorylnfo. A fim da tabela de Filelnfo ficar completa, incluímos
também as operações que permitem obter as streams associadas a um ficheiro. No
entanto, estas só serão discutidas na próxima secção.

Tal corno dissemos atrás, a funcionalidade associada a estas classes também é coberta nas
classes File e Directory sob a forma de métodos estáticos. O programador é livre de
utilizar as que entender.

As tabelas 8.16 e 8.17 mostram os principais métodos das classes Filelnfo e Dkectorylnfo.
MÉTODO/PROPRIEDADE DESCRIÇÃO i

Filelnfo(string name) Cria um novo objecto que representa um ficheiro .


em disco.
FileAttributes Obtém ou altera os atributos associados ao :
Attributes { get; set; } ficheiro (archiveL read-only, hidden, system, etc.).
DateTi me Obtém ou altera a hora e a data de criação do
creationTime { get; set; } ficheiro.
DI rectorylnfo Obtém um objecto que representa o directório do
Di recto ry { get; } ; ficheiro.
string i Obtém uma string que representa o directório
DirectoryName { get; } ! do ficheiro.
BOOl
Exists { get; } . . . . . .: Verifica se o ficheiro existe.
string \n {Obtém
get; uma> string correspondente
; à extensão
do ficheiro.
string . '. Obtém uma string correspondente ao nome
FullName- { get; } i completo do ficheiro.
DateTi me ! Obtém ou altera a hora e a data do último acesso
LastAccessTime { get; set; > ; ao ficheiro. '.

i FCA - Editora de Informática 303


C#3.5

(cont.)
MÉTODO/PROPRIEDADE: DESCRIÇÃO |
Dateti me Obtém ou altera a hora e a data da última escrita ,
LastWriteTime { get; set; } no ficheiro.
Long
Length { get; } Retorna o tamanho do ficheiro em bytes. \m o nome canón
string
Name { get; }
StreamWriter Cria um StreamWriter que permite adicionar ••
AppendTextO texto ao final do ficheiro.
Copia o ficheiro para um novo ficheiro, sendo
Filei nfo
CopyTo(string destName) especificado o nome deste. É retornado um ;
objecto que representa o novo ficheiro, :
FileStream Cria o ficheiro, retornando a FileStream :
CreateC) associada.
StreamWriter
CreateTextQ
Cria o ficheiro para escrita de texto. :
Void
Apaga o ficheiro.
DeleteQ
Move o ficheiro para um certo destino. Caso dest ;
Void
MoveTo(string dest) especifique um nome de ficheiro no fim, o ficheiro
muda de nome.
Abre o ficheiro, utilizando um certo modo
FileStream
Open(FileMode mode,
(exemplos: open, Append) e para um certo tipo '•
FileAccess acess) de operação de acesso (exemplos: Read, Wri te,
ReadWrite).
FileStream
OpenReadQ . . . . :
Abre o ficheiro apenas para leitura.
streamReader !
Op_enText() . . . .:
Abre o ficheiro para leitura de texto.
FileStream
OpenWriteQ Abre o ficheiro apenas para escrita.
Void Refresca o estado do objecto, a partir do estado
RefreshQ ; corrente do ficheiro.

Tabela 8.16 — Principais elementos da classe Fi l elnfo

MÉTODO/PROPRIEDADE | DESCRIÇÃO

Directorylnfo(string path) Cria um novo objecto que representa um


directório.
FileAttributes Obtém ou altera os atributos associados ao
Attributes { get; set; } directório (archive, read-only, hídden, system,
etoO-
oateTime Obtém ou altera a hora e a data de criação do
CreationTime { get; set; } directório.
Bool
Exists { get; } Verifica se o directório existe.
string Obtém uma string correspondente à extensão
Extension { get; } do directório.
String i Obtém uma string correspondente ao nome
FullName { get; } : completo do directório.

3O4 © FCA - Editora de Informática


CLASSES BASE

(cont.)
MÉTODO/PROPRIEDADE ' DESCRIÇÃO
DateTime i Obtém ou altera a hora e a data do último acesso ;
LastAccessTime { get; set; } ! ao directório.
DateTime í Obtém ou altera a hora e a data da última escrita
LastwriteTime { get; set; } no directório.
strlng
Natne { get; > Obtém o nome canónico do directório.
Directoryinfo j Obtém um objecto que representa o directório
parent { get; } acima do corrente.
Directoryinfo Obtém o directório raiz do sistema de ficheiros do
Root { get; } directório em causa.
vold
createQ Cria o directório. :

Dl recto ryinto
CreateSubdi rectoryC '- Cria um subdirectório. :
stríng path) í
void '•
Apaga o directório. :
DeleteQ !
Apaga o directório/ especificando se os '
void
De1ete(bool recursive) subdirectórios e os ficheiros nele contidos devem
i ser apagados.
Di recto ryinfo[] Retorna uma tabela com os subdirectórios ;
Getoi rectori es () presentes no directório.
Directorylnfo[] Retorna uma tabela com os subdirectórios
Getoi rectori es ( presentes no directório que obedecem a um
string wildcard) certo padrão (uso de wild cards).
Filelnfo[] Retorna uma tabela com os ficheiros presentes -.
GetFilesQ | no directório. :

Retorna uma tabela com os ficheiros presentes


Fi1elnfo[] ;
GetFi lês (string wildcard) ; no directório que obedecem a um certo padrão .
(exemplo: "*.txt").
FileSystemInfo[] i Retorna uma tabela com todos os ficheiros e :
GetFi 1 eSystemlnfos Q directórios presentes no directório. ;
FileSysteffllnfoG Retorna uma tabela com todos os ficheiros e
GetFi 1 eSystemlnfos C directórios presentes no directório que obedecem
string wildcard) a um certo padrão.
voi d Move o directório para um outro directório.
MoveTo(string destPath)

Tabela 8.17 — Principais elementos da classe Di rectorylnfo

Finalmente, falta-nos abordar a classe path. Esta classe dispõe apenas de métodos
estáticos, permitindo ao programador, manipular, de forma simples, cadeias de caracteres
que representam directórios e ficheiros. Por exemplo, para alterar a extensão do nome de
um ficheiro de ".txt" para ".doe", basta fazer:
string ? -noyóNòmè =/ Pã±H.chanjeExiénTiP^
Para descobrir quais os caracteres que não se pode utilizar no nome de um ficheiro, pode
fazer-se:
cbnsol e.Wn"teLineCrc|iract_eYeslxnvalJi~dps: "{0};'' r.PltifrinyalIdpathChàrsJj. ]

© FCA - Editora de Informática 305


C#3.5

A tabela seguinte lista os principais métodos e propriedades de path.

MÉTODO/PROPRIEDADE DESCRIÇÃO
static readonly char 0 carácter utilizado para separar o nome dos
Di rectoryseparatorchar directórios.
static readonly char[] Caracteres que não podem ser utilizados no nome í
invalidpathchars de um ficheiro ou directório.
static string Altera a extensão de path para uma nova .
ChangeExtensnonC extensão. Caso ext seja null, a extensão é •
string path, string ext) apagada.
static string Combina duas paths, adicionando caracteres
Combine (
string pathl, string path2) separadores apropriados, se necessário.
static string Obtém apenas a parte correspondente ao :
GetoirectoryNameCstring path) directório em path.
static string Obtém apenas a extensão do nome especificado
GetExtension (string path) em path.
static string ' Obtém a parte correspondente ao nome do .
GetFileName(string path) ficheiro e a extensão em path.
static string Obtém apenas a parte correspondente ao nome,
GetFileNameWithoutExtension(
string path) sem extensão, do ficheiro em path.
static string Obtém a path completa relativamente ao
GetFull path (string path) argumento passado como parâmetro.
static string Obtém a path correspondente à raiz utilizada no
GetPathRoot(string path) argumento passado como parâmetro.
static string Retorna o nome de um ficheiro único em disco,
GetTemp Fi 1 eName () criando esse ficheiro com tamanho 0.
static string Retorna o caminho para o directório temporário
GetTempPathQ em disco.
static bool
HasExtensionCstring path) Verifica se o argumento possui uma extensão.
static bool Verifica se o argumento corresponde a um
ispathRootedCstring path) caminho absoluto ou relativo. ,

Tabela 8,18 — Principais elementos associados à classe Path

" Todas as classes relacionadas com dispositivos de entrada e saída


encontram-se em system. 10.
Acesso ao " A leitura e a escrita para ficheiros é abstraída em termos de streams. Uma
sistema de stream representa um canal de comunicação de ou para uma certa fonte de
ficheiros informação.
" Para manipular ficheiros, utiliza-se as classes File e Filelnfo. Ambas
possuem a mesma funcionalidade. No entanto, File apenas dispõe de
métodos estáticos, enquanto Fil elnfo representa um ficheiro em particular.
~ Para manipular directórios, utilizam-se as classes oi recto ry e
Directorylnfo. A situação é análoga a File e Fil elnfo. Directory apenas
possui métodos estáticos, enquanto Directorylnfo representa um
directório em disco.

3O6 © FCA - Editora de Informática


CLASSES BASE

" Antes de fazer uma manipulação de um objecto de um sistema de ficheiros,


ARETER é conveniente verificar se este existe, utilizando a propriedade
Fileinfo.Exists ou Dnrectorylnfo.ExIsts.
Acesso ao
sistema de " Ao realizar uma operação que envolva um objecto no sistema de ficheiros,
ficheiro podem ocorrer diversos problemas. Tipicamente, podem ser lançadas
excepções que descendem de System.IO.lOExcepfion. No entanto, podem
ocorrer outras excepções.
" A classe Path permite manipular, de forma simples, cadeias de caracteres
que representam ficheiros e directórios.

8.4.2 LEITURA E ESCRITA DE FICHEIROS


O conceito de stream é muito simples e, ao mesmo tempo, muito poderoso. Urna stream
de entrada representa uma fonte de informação. Um stream de saída representa algo que
envia informação para determinado local.

As streams são combinadas ern cadeia para fornecer funcionalidades cada vez mais
elaboradas. Por exemplo, é possível ter uma stream simples que apenas é capaz de
devolver um byte de cada vez, lido de um certo dispositivo. Sobre esta stream, pode-se
colocar uma outra stream capaz de juntar bytes e de os encarar como inteiros, cadeias de
caracteres ou números de vírgula flutuante. Do ponto de vista do programador, este
apenas tem de criar o encadeamento de streams que necessita, utilizando a última para
realizar as operações complexas que necessita. A figura 8.4 ilustra a ideia.

O programador utiliza a
A P I fornecida pela
stream que se encontra
no final da cadeia.

Sfreambase Stream A Stream K


(associada fisicamente a uma (agrupa Informação da slieam anterior (agrupa informação da stream anterior
certa fonte de [rifotmaçao) oferecendo uma maior funcionalidade) oferecendo uma grande funcionalidade)

Cada stream utiliza a API


da anterior para tratar r
informação, fornecendi
uma API mais poderosi
à seguinte

Figura 8.4 — Princípio de funcionamento das streams

Felizmente, na plataforma .NET, já existem classes para manipulação da informação


presente em ficheiros que fazem grande parte deste agrupamento. Basicamente, para

© FCA - Editora de Informática 3O7


C#3.5

tratar ficheiros de texto, utiliza-se as classes streamReader e StreamWriter e, para tratar


ficheiros binários, as classes BinaryReader e Binarywriter. Antes de examinarmos
estas classes, iremos, no entanto, apresentar as classes de streams existentes na
plataforma .NET. Achamos que é importante o leitor ficar com a noção do que se
encontra disponível, uma vez que, em muitas situações, é necessário combinar várias
streams. Tipicamente, essas situações ocorrem quando se está a fazer tratamento de
informação que provém, ou então é enviada, para uma rede.

8.4.2.1 HIERARQUIA DE BTREAMS


Na hierarquia de streams da plataforma .NET, podemos encontrar dois tipos distintos de
streams. Por um lado, temos as streams que representam fontes de informação ou
consumidores de informação. Exemplos deste tipo são Fil estream e Memorystream. Este
tipo de streams constitui a base à qual as outras streams se ligam. O segundo tipo de
streams utiliza as streams base para fazer tratamento dos dados que estas disponibilizam.
Alguns exemplos são as classes StreamReader e BinaryReader.

0 segundo tipo de streams leva como parâmetro, no seu construtor, uma stream do tipo
básico. Por exemplo, em:
| ç p y - - - — -- ----- - -- - - - -

| FileStream ficheiro = new FileStream(@"xçto.txt", FileMode.Create) ; ,


! StreamWriter writer = new streamwriter(ficheiro); l
1 ;
j writer.write("Hello") ;
l writer.CloseQ ;
jcatch (Exception e)

! Console.Wri teti ne(e.Message); :


j Consol e. Wri tet_i ne(e. stackTrace) ;

a expressão

cria uma stream básica associada a um ficheiro ("xpto.txt"). Esta stream apenas permite
operações como ler e escrever blocos de bytes. Para conseguirmos escrever texto para o
ficheiro, é necessário encapsular esta stream numa outra mais poderosa: streamWriter.
No construtor desta stream, é colocada uma referência para a stream associada ao
ficheiro.

Tendo sempre este conceito em mente, vamos, então, examinar a hierarquia de classes
relativa a streams. Esta hierarquia é apresentada na figura 8.5.

3O8 © FCA - Editora de Informática


CLASSES BASE

Sysiem.Object

System.lO.BinatyWriter System.lO.BInaryReader

System. IO.TextWriter
,
' t
der System.IO.StreamWn

System. lO.StringReader Sysíem.lO.StringWriter

Figura 8.5 — Hierarquia de classes relativas a streams

Nós não analisaremos profundamente todas estas classes, mas iremos dar uma pequena
descrição das classes mais derivadas. Isto é, as que são realmente utilizadas pelo
programador. Recomendamos que se consulte a documentação pormenorizada de cada
classe, sempre que é necessário utilizar uma delas. A tabela seguinte resume qual a
utilização de cada uma destas classes.
CLASSE DESCRIÇÃO
stream Representa a classe base de onde todas as streams básicas derivam. ;
Trata-se de uma stream que faz bufferíng dos dados lidos (ou escritos) de uma •
Bufferedstream outra stream, por forma a aumentar o desempenho das leituras e escritas nesta. !
É construída utilizando uma instância de Stream. :
É uma stream básica cuja fonte de dados se encontra em memória/ sob a forma
MemoryStream . de uma tabela de bytes. É muito útil quando é necessário preparar dados para
enviar para a rede ou para um outro dispositivo
Representa uma stream associada a um ficheiro, sendo utilizada por outras
FileStream
5frea/77spara realizar leituras e escritas para disco.
Implementa uma stream que permite ler dados sob a forma de texto de uma
StreamReader stream básica (instância de Stream). No entanto/ também possui um construtor
que permite associar directamente a stream a um ficheiro.
É uma stream que permite realizar leituras de dados a partir de uma string
StringReader
em memória.
Implementa uma stream que permite escrever dados sob a forma de texto para
Streamwriter uma stream básica (stream). No entanto/ também possui um construtor que
permite associá-la directamente a um ficheiro.
É uma stream que permite realizar escritas de dados numa cadeia de caracteres
Stringwriter
em memória. Essa cadeia de caracteres é uma instância de stringBuilder.
Trata-se de uma stream que permite ler dados/ sob a forma binária, de uma
BinaryReader
stream básica (uma instância de stream).
Representa uma stream que permite escrever dados/ sob a forma binária/ para '
BinaryWriter
uma stream básica (uma stream).

Tabela 8.19 — Hierarquia de classes relacionadas com streams

© FCA - Editora de Informática 3O9


C#3.5

8.4.2.2 A CLASSE FILESTREAM

A classe Fllestream constituí a abstracção mínima para acesso a um ficheiro. O seu


construtor permite associar um objecto deste tipo a um ficheiro, abrindo-o para leitura,
escrita ou para leitura/escrita, assim como com um certo modo de abertura (exemplo:
escrita no fim do ficheiro, recriação do ficheiro, etc.). Os objectos desta classe podem,
depois, ser utilizados para a construção de streams mais elaboradas.

Existem vários construtores que podem ser utilizados, mas se analisarmos


Fi1estream(stri ng name, FileMode mode, FileAccess access) cobrimos quase
todo o espectro útil de construtores desta classe.

Quando se abre um ficheiro, é necessário especificar o seu nome (name), o modo em que
se quer abri-lo (mode) e qual o tipo de acesso que se irá realizar (access). Existe, ainda
uma variante deste construtor onde é possível especificar o tipo de partilha que deverá
existir relativamente ao ficheiro. Tanto FileMode como Fi l eAccess são enumerações,
sendo os seus valores possíveis apresentados na tabela seguinte.
j VALOR DESCRIÇÃO

FileMode. Append Caso o ficheiro não exista/ cria o ficheiro. Caso exista/
abre-o, sendo as escritas realizadas no fim deste.

Fil eMode. Create Caso o ficheiro não exista, cria o ficheiro. Caso exista,
o ficheiro é colocado com tamanho 0, sendo reescrito.
Caso o ficheiro não exista, cria o ficheiro. Caso exista, :
Fi 1 eMode . CreateNew
é lançada uma excepção.

Fil eMode. Open Abre simplesmente o ficheiro. Caso o ficheiro não


exista, é lançada uma excepção.
Abre o ficheiro caso exista. Caso o ficheiro não exista,
Fil eMode. OpenOrCreate
o ficheiro é criado.
FileMode.Truncate Abre um ficheiro e coloca o seu tamanho a 0.

FileAccess.Read \ ficheiro é aberto para leitura.


FileAccess.Write O ficheiro é aberto para escrita,
FileAccess.ReadWri te O ficheiro é aberto para leitura e escrita.

Tabela 8.20-Valores das enumerações Fi l eMode e Fil eAccess

Por exemplo, fazer:


FiléstrearrTlog "~
= new FileStreamClog-txt", Fil eMode. Appendj FileAccess.Write) ;
abre um ficheiro de nome "log.txt" para serem acrescentados dados ao final deste. Caso o
ficheiro não exista, é criado. Por exemplo, ao fazer:
Filestream passwòrdFile
. = new FileStream("passwd", Fil eMode. Open, Fil eAccess ...Read) ;
é aberto o ficheiro "passwd* apenas para leitura.

310 © FCA - Editora de Informática


CLASSES BASE

Uma forma muito usual de utilização deste construtor é especificar apenas o modo de
abertura. Neste caso, o tipo de acesso é para leitura e escrita, o que cobre todas as
situações possíveis de utilização. Por exemplo:
Fílestream log = 'new FiTeStre_ámCl]pa-.txtn^ FlleMpdé_lAppend)j "
De seguida, iremos ver de que forma é que esta classe é utilizada para ler e escrever
ficheiros de texto e também ficheiros binários.

- Uma stream representa um fluxo de informação. A origem ou o destino da


ARETER informação é escondido do programador.
Streams " As streams podem ser combinadas, oferecendo funcionalidades cada vez
mais elaboradas. O programador chama os métodos da stream que se
encontra mais acima na cadeia.
" Fi l estream representa a stream básica associada a um ficheiro.
" A forma mais frequente de criação desta stream é utilizando apenas o
construtor onde se indica o nome do ficheiro e o modo de abertura. Por
exemplo:
Filestream logFile
= new FileStreamC"log.txt", FileMode.Append) ;
" Muitas vezes, também se utiliza o construtor de três argumentos, indicando
o modo de abertura e o tipo de acesso. Exemplo:
Filestream passwordFile
= new FileStream("passwd" , FileMode.Open, FileAccess.Read) ;

8.4.2.3 FICHEIROS DE7HXTO


Ler e escrever em ficheiros de texto é muito simples. Para ler de um ficheiro de texto,
basta usar uma instância de streamReader. Para escrever para um ficheiro, basta usar
uma instância de streamwriter. Estes objectos possuem diversos métodos writeQ que
permitem escrever tipos elementares de dados sob a forma de texto. No entanto, os mais
práticos de utilizar são os nossos habituais writeQ e writeLineQ, que temos vindo a
utilizar em Consol e.
Para obter uma instância de StreamReader associada a um ficheiro, pode fazer-se:
'Filestream ficheiro = new FileStreamC"tabuadã.txt" , FileMode.Create) j
streamwriter writer = new; streamWriterCfichei rd) ; .....
ou simplesmente:
Streamwriter writer = new "^treãmwViterC^tabuada.txt11^ ; . . j
A partir deste momento, é possível escrever texto para o ficheiro:
.wn-ter.WriteLinèCTábuada1'); .............. . ' '
writer. WriteLineQ ; , -. . . . i
for
for (int j=l;
x.. {1,2}. = {2,2}" ,. i , j , i - j ) ;

© FCA - Editora de Informática 3ll


C#3.5

writer.writeLineQ;

No final das escritas terem sido feitas, é essencial que as streams associadas ao ficheiro
sejam fechadas. Caso isso não seja feito, podem existir dados que não são escritos em
disco. Para fechar toda uma cadeia de streams, basta chamar o método cl os e Q na stream
mais acima:

Atenção: caso se chame o método cios e Q na stream do ficheiro e não na stream de


topo, os dados podem não ser correctamente escritos no ficheiro, pois ainda se podem
encontrar em buffers intermédios. Da mesma forma, é incorrecto fechar primeiro
Filestream e depois, streamWriter. Tal situação resulta numa excepção.

A seguinte listagem ilustra este exemplo. Nesta listagem, o acesso ao ficheiro encontra-se
protegido por um bloco try-catch, tal como seria de esperar.

* Programa que ilustra o acesso a ficheiros de texto criando


* um ficheiro com a tabuada.
*/
us-ing System;
using System. IO;
class ExemploCap8_3
static void Main(string[] args)

try
StreamWriter writer = new StreamWriter("tabuada.txt") ;
writer. Writel_ine("** Tabuada **") ;
writer.writeLineQ ;

for (int i=l; i<10; Í-H-)


for (int j=i; j<=10
writer. WriteLineCíO^} x {1,2} = {2,2}", i, j, i*j);
writer. writetineC) ;
}
writer. closeC) ;
}
catch (Exception e)
Console. WriteLine(e.Message) ;
Console.WriteLine(e.stackTrace) ;

Listagem 8.3 — Programa que cria um ficheiro com a tabuada (ExemploCap8_3.cs)

312 © FCA - Editora de Informática


C LASSES BASE

Para ler a partir de um ficheiro de texto, o procedimento é similar. Tanto se pode


escrever:
iFileStreám fichei rõ~= new TneStr~eamCtabu~ada.txt", FileMòde.Opén)";'
!streamReader reader_= new Str_earnReader(f içhei ro) ; __ ~
como:
.streamReadér. reácTer ="_nèw streamRea^derCtabuáda.txt 1 ^;
A classe streamReader está vocacionada para ler um ficheiro linha a linha. Embora
existam métodos que permitem ler um carácter ou um conjunto de caracteres, isso não é
usualmente utilizado. Para ler uma linha, utiliza-se o método ReadLlneQ. Caso seja
necessário decompor essa linha em partes, pode-se utilizar os métodos disponíveis em
String, como splitQ, ou a API de expressões regulares.

0 seguinte código lê todas as linhas do ficheiro aberto anteriormente, mostrando-as no


ecrã. Note-se que quando se chega ao fim do ficheiro, o método ReadLineQ devolve
null.
strfng T i n e ;
;do

1 Une = reader.ReadLineQ ;
: if ("line != null)
Console.Wri teLi ne(li ne);
J while ("Hne |= null);

Finalmente, a stream deve ser fechada, fazendo reader.closeQ. A listagem seguinte


apresenta o código completo deste exemplo.

/* Programa que lê um ficheiro de texto.


*/
uslng System;
using System.IO;
class Exemp1oCap8_4
static void Main(str1ng[] args)
try
StreamReader reader = new StreamReader("tabuada.txt");
string Une;
do
{
line = reader.ReadLineQ ;
if(line != null)
Consol e. Wri teLi ne (line) ;
} while (line l- null);
reader.Close();
catch (Exception e)
Console.WriteLine(e.Message) ;

© FCA - Editora de Informática 3 13


C#3.5

y
Console.Wr1teLlneCe,5tacj<Traçe3
< _• • •- .- ' - - '. ..

Listagem 8.4 — Programa que lê um ficheiro de texto (ExemploCap8_4.cs)

Existe um último ponto com o qual é necessário ter algum cuidado. Hoje em dia, quando
se escreve um ficheiro de texto, este encontra-se codificado de uma certa forma. Embora
a maioria dos caracteres apareçam "normalmente", existem certas sequências de
caracteres que representam letras especiais. Isto deve-se ao facto de o código ASCII
apenas permitir representar 255 letras diferentes (correspondente a 8 bits)}1.

Actualmente, utiliza-se um conjunto de caracteres chamado Unicode que permite


representar praticamente todas as letras de todas as línguas do mundo. No entanto, essas
letras podem ser codificadas de diferentes formas. A maioria das codificações
correntemente utilizadas representa os alfabetos ocidentais directamente. Para certas
letras utilizadas em outras partes do mundo, surgem sequências especiais que permitem
codificar essas letras. No ponto de vista do programador, a API disponível na plataforma
.NET trata tudo isto transparentemente. Caso sejam encontradas essas sequências
especiais, streamReader encarrega-se de colocar em string os caracteres (char)
correctos. Da mesma forma, streamwríter encarrega-se de escrever os caracteres
correctamente codificados. Por omissão, a codificação utilizada é a UTF-8, que
representa uma certa forma de codificação de caracteres Unicode.

Caso o programador deseje, pode especificar outras formas de codificação, tanto para ler,
como para escrever ficheiros. A forma de codificação é passada como argumento no
construtor de streamReader e streamWriter. Algumas outras formas possíveis de
codificação são: ASCiiEncoding, urricodeEncoding e UTF7Encod1ng. Todas estas formas
de codificação são implementadas como clas.ses em System.Text.

" Para ler um ficheiro de texto, basta ter uma instância de StreamReader.
Essa instância pode ser obtida, associando uma Filestream a um
StreaniReader ou criando directamente um StreamReader, indicando o
Ficheiros de nome do ficheiro em causa. Exemplo:
texto l
StreamReader ficheiro = new streamReader("log.txt");
- Para escrever para um ficheiro de texto, basta ter uma instância de
StreamWriter. Essa instância pode ser obtida, associando uma Filestream
a um streamWriter ou criando directamente um streamWriter, indicando
o nome do ficheiro em causa. Exemplo:
StreamWriter ficheiro = new streamWriter("dados.txt");

17 Estritamente falando, o código ASCII apenas especifica codificação para 7 bits (isto é, 127 caracteres).
A utilização do oitavo bit é urna extensão, existindo vários mapeamentos possíveis na zona 128-255.
314 © FCA - Editora de Informática
CLASSES BASE

" Tipicamente, um ficheiro de texto é lido linha-a-Hnha, utilizando o método


ARETER ReaduineQ. Este método devolve n u l l , caso se tenha chegado ao flm do
£r*^
ficheiro.
Ficheiros de
texto " Para escrever para o ficheiro, utiliza-se os métodos writeQ e wríteLineQ.
Estes métodos funcionam de forma semelhante aos de consol e.
" No final de se utilizar o ficheiro, é necessário fazer o seu closeC). Caso se
esteja a utilizar múltiplas streams, deve ser feito o closeQ apenas da que
está no topo do encadeamento.
" Todos os acessos a ficheiros devem ser protegidos por um bloco
try-catch.
" No construtor das streams de texto, é possível indicar um tipo de
codificação diferente para o alfabeto que é utilizado. Por omissão a
codificação é UTF-8.

8.4.2.4 FICHEIROS BINÁRIOS


A utilização de ficheiros binários é bastante semelhante à dos ficheiros de texto. Para ler
de um ficheiro binário, utiliza-se a classe BinaryReader e para escrever, utiliza-se a
classe Binarywriter. No entanto, enquanto nos ficheiros de texto os dados são
codificados como sendo caracteres (letras), nos ficheiros binários, estes são representados
num formato próprio não perceptível a humanos. Isto é, em formato binário.

A classe Binarywriter possui versões do método WriteQ que permitem escrever todos
os tipos básicos do CTS (Cominou Type System). Por exemplo, existe um Bi narywrí ter.
.Write(char), um Bi n a r y W r i t e r . w r i te (st ri ng), Binary.Write(int) e assim,
sucessivamente.

No caso da classe BinaryReader, não é possível existir um método ReadQ unificador,


uma vez que não é possível saber o que se pretende ler. Assim, o que é disponibilizado
nesta classe é um conjunto de métodos cujo nome começa por Read, sendo acrescentado à
frente o tipo de dados a ler: BinaryReader.ReadCharQ, BinaryReader.ReadstringQ,
Bi naryReader. Readlnt32 Q, etc.

Ao contrário das classes streaniReader e streamwriter, não é possível associar


directamente este tipo de streams a um ficheiro. É necessário passar no construtor das
classes, uma instância de stream:
Fil esn;rze,anT nmagem7 =" néw FiTeStr~eamO'còimbra."j pg" 7"~Fi'l èMode.
'sn naryReader réader = .néw Bi naryReaderí^imagoem).;. _„
Na listagem seguinte, é mostrado um exemplo de um programa que calcula todos os
números primos até um certo valor máximo, colocando-os num ficheiro binário. Cada
número é codificado como sendo um inteiro (i nt). No início do ficheiro, é indicado o
número de primos que lá se encontram guardados. Após a escrita do ficheiro, este é lido,
sendo os números primos mostrados no ecrã. Este exemplo faz uso do algoritmo do crivo
de Eratóstenes, assim como da classe BitArray.

© FCA - Editora de Informática 315


C#3.5

/*
* programa que calcula números primos até um certo máximo
* escrevendo esses números num ficheiro. Finalmente,
* o ficheiro é lido e os números mostrados no ecrã.
*/
using System;
using System. IO;
using System. Coí "l ections;
// classe que trata de todos os assuntos relacionados com
// números primos
class Primos
{
private int[] TabelaPrimos;

// Guarda a tabela corrente em ficheiro, podem


// ser lançadas excepções. No ficheiro fica guardado
// o número de primos na tabela seguido dos primos
// em si
public void GuardaTabelaFicheiro(string nomeFichei ro)

FileStream f s = new FilestreamCnomeFichei ro, FileMode.Create) ;


BinaryWriter fich = new BinaryWriter(fs) ;

if (TabelaPrimos == null)
fich.Write(O);
e! s e
{
fich.WriteCTabelaPrimos.Length) ;
foreach (int primo in TabelaPrimos)
fich.Write(primo) ;
}
fich.closeC) ;
}
// Carrega a tabela presente num ficheiro para memória. Podem
// ser lançadas excepções. TabelaPrimos é limpa no inicio deste
// processo,
public void CarregaTabelaFicheiro(string nomeFichei ro)
TabelaPrimos = null;
FileStream fs = new FilestreamCnomeFichei ro, FileMode.Open) ;
BinaryReader fich = new BinaryReader(fs) ;
int totalprimos = fich.ReadlntSZQ ;
TabelaPrimos = new int[totalPrimos] ;
for (int i=0; i <total Primos;
TabelaPrimos[i] = fich.Readlnt32() ;

fich.closeC) ;

316 © FCA - Editora de Informática


CLASSES BASE

pubnc void calculaPnmosAte(int n)

// criação da tabela; O e l não são primos


BitArray tabela = new BitArray(n-hl, true);
tabela [01 = false;
tabela[l] = false;
// Crivo de Eratostenes
int limiar = (int) Math.Sqrt(n) ;
for ("int i =2; i <1 i mi ar; i++)
{
i f (tabela [i] == true)
{
for (int q =1+1; i<=n; j+= i)
tabelaCj] = false;

// Conta o número total de primos na tabela e


// faz com que Tabel aprimos tenha esse tamanho
int total Primos = 0;
foreach (bool bit i n tabela)
{
i f (bit == true)
-hftotalPrimos;
}
TabelaPrimos = new int [total Primos] ;

// Copia todos os primos para TabelaPrimos


int indexTabelaPrimos = 0;
for (int 1=0; i<=n;
i f (tabelafil == true)
TabelaPrimos[indexTabelaPrimos++] = i;

public void MostraTabelaQ

if (TabelaPrimos != null)

foreach (int primo in TabelaPrimos)


Console. Write("{0,6}\t", primo) ;
Console.WriteLineQ ;
}
else
Console.writeLine("Tabela vazia") ;

cl ass Exempl oCap8_5


const string NOME_FICHEIRO = "primos.dat";
const int MAX_PRIMO = 50000;
static void Main(string[] args)
try
Primos calculadora = new PrimosQ;
cal cul ado rã . Cal cul ap ri mosAte (MAXlPRlMO) ;
_ cal culadora.GuardaTabelaFicheiro(NOME_FlCHEIRQ) ;

© FCA - Editora de Informática 317


C#3.5

Primos tabela2 = new PrimosQ;


tabela2.CarregaTabelaFi cheiro(NOME_FICHEIRO);
tabela2.MostraTabela();
}
catch (Exception e)
Console.WriteLine(e.Message);
Console.WriteLine(e.stackTrace) ;

Listagem 8.5 — Programa que escreve e lê um ficheiro binário (Exemplo Cap8_5.cs)

- Para ler de um ficheiro binário, basta usar uma instância de BinaryReader.


ARETER Esta instância é associada a uma Fil estream. Exemplo:
Filestream fs = new FilestreamCNOME, FileMode.open) ;
Ficheiros BinaryReader reader = new BinaryReader(fs);
binários
~ Para escrever para um ficheiro binário, basta usar uma instância de
BinaryWriter. Esta instância é associada a uma Fil estream. Exemplo:
Filestream fs = new Fi1eStream(NOME, FileMode.write);
BinaryWriter writer = new BinaryWriter(fs);
~ BinaryWriter possui versões do método WriteQ que permitem escrever
todos os tipos básicos do CTS.
" BinaryReader possui diferentes métodos que permitem ler todos os tipos
básicos do CTS. Esses métodos começam com a palavra Read. Por
exemplo: ReadstringO.ReadDoubleO, etc.
~ As mesmas considerações que foram feitas relativamente aos ficheiros de
texto também se aplicam (é necessário chamar c"! os e O e proteger o código
com um bloco try-catch).

8.4.3 SERIALEZAÇÃO DE OBJECTOS


Na secção anterior, vimos que é possível utilizar as classes BinaryReader e
BinaryWriter para escrever tipos elementares de dados para uma stream. No entanto,
estas classes não nos permitem escrever objectos directamente para uma stream. As
classes BinaryFormatter e x m l s e r i a l i z e r oferecem essa funcionalidade.

Serialização é o nome dado ao processo de converter um objecto numa forma que possa
ser guardada persistentemente. Isto, tipicamente, envolve guardar o seu estado interno.
Por exemplo, pode-se serializar um objecto para um formato binário, sendo possível
transportá-lo pela Internet, reconstruindo-o nurna outra máquina. Da mesma forma, é
possível serializar um objecto, guardando-o num ficheiro. Mais tarde, é possível voltar a
abrir o ficheiro e recuperar o objecto.

318 © FCA - Editora de Informática


CLASSES BASE

8.4.3.1 SERIAUZAÇÃO EM FORMATO BINÁRIO


Para indicar que é possível fazer a serializaçao de uma classe, utiliza-se o atributo
serializable. Por exemplo, se quisermos serializar objectos do tipo Empregado, basta
marcar a classe com este atributo:
[LSerl a iTzãFllQ .. ' . „ _._ " ~]
: cláss Empregado

' private string NomeEmpregado;


: private int IdadeEmpregado;

public EmpregadoCstring nome, int idade)


NomeEmpregado = nome;
IdadeEmpregado = idade; :

public override string ToStringC)


{
return "C" + NomeEmpregado + "," + IdadeEmpregado + ")";

Se tivermos um objecto deste tipo, ou mesmo uma tabela de objectos, então, é muito
simples escrevê-lo para uma stream. Suponhamos que queremos escrever um objecto
Empregado num ficheiro binário. Basta-nos criar uma instância de BinaryFormatter e
chamar nesta, o método serializeQ, com a stream que se quer utilizar:
Empregado emp = new Emprègado("Pedro Abreu","26);"

Filestream ficheiro = new FileStreamC"pessoa.dat", FileMode.Create);


BinaryFormatter formatador = new BinaryFormatterC);
formatado r .Se r i ai ize(f i chei rp, emp); . . . _ . _ _ . . :

Para recuperar o objecto, é apenas necessário realizar o processo inverso, utilizando o


método DeserializeC):
;FileStream ficheiro =~riew Fi1eStreamC"pessòa.dát", FileMode.õpen);

BinaryFormatter formatador = new BinaryFormatterC);


•Empregado emp = (Empregado) formatadpr^peserializeCficheiro); j

Ou seja, é muito simples guardar e recuperar objectos de streams. Note-se que é


necessário realizar uma conversão explícita para Emp regado, uma vez que oeseri ai i ze C)
devolve object.

Uma questão importante é que, caso o objecto contenha referências para outros objectos,
o sistema encarrega-se de guardar todo o grafo de objectos. No entanto, cada objecto que
se encontra num dado grafo apenas é guardado uma vez. O método DeserializeC)
encarrega-se de reconstruir o grafo de relações, tal como estas existiam, antes de o
objecto pai ser guardado. Obviamente, que para ser possível guardar todos os objectos
referenciados a partir de uma classe, é necessário que as classes correspondentes sejam
também marcadas com o atributo Serializable. Por exemplo, se em Empregado
existisse um campo que fosse do tipo Automóvel, então, a classe Automóvel também teria
© FCA - Editora de Informática 31 9
C#3.5

de ser marcada como serializable. Caso isso não acontecesse, ao serializar Empregado
iria surgir uma seri ai i zati onExcepti on.

A listagem 8.6 mostra um exemplo completo de serializacão e reconstrução de uma tabela


de empregados de uma empresa. Note-se que é necessário fazer a importação dos
seguintes espaços de nomes:

• System.Runtime.Seri ai i zati on.Formatters.Bi nary;

• System. Runti me. Sen" ai i zati on.

* Programa que ilustra serialização binária de objectos.

using System;
using System.IO;
usi ng System.Runti me.Seri ai i zati on.Formatters.Bi nary;
usi ng System.Runti me.Seri ai i zati on;
[Serializable]
class Empregado
private string NomeEmpregado;
private int IdadeEmpregado;
public Empregado(string nome, int idade)
NomeEmpregado = nome;
IdadeEmpregado = idade;

public override string ToStringO


return "C" + NomeEmpregado + "," + IdadeEmpregado + ")";

class Exemplocap8_6
const string NOME_FICHEIRO = "dados.dat";
static void Main(string[] args)
Empregado[] empresa = new Empregado[l
new Empregado("Nuno santos", 24),
new Empregado("Ricardo Rodrigues", 22),
new Empregado("Paulo Germano , 26),
new Empregado("Pedro Pereira", 23)
j>
try
// Escreve para ficheiro os empregados da empresa
Filestream ficheiro
= new FileStreamÇNOME_FICHElRO, FileMode.Create);

320 © FCA - Editora de Informática


CLASSES BASE

BinaryFormatter formatado r = new BinaryFormatter Q ;


formatador.Serialize(ficheiro, empresa);
ficheiro.CloseQ;
// Lê de ficheiro os empregados da empresa e mostra-os
ficheiro = new FileStreamCNOME_FiCHElRO, FileMode.open);
Empregado[] empLidos =
(Empregado[]) formatador.Deseri ai i ze(fichei ro);
f i cheiro. Glose () ;
foreach (Empregado emp i n empLidos)
Conso1e.WriteLine("{0}", emp);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
console.writeLine(e.StackTrace);

Listagem 8.6 — Serialização binária de objectos (ExempioCap8_6.cs)

Em todo este processo, existe um ponto subtil mas extremamente importante: apenas
informação que diz respeito aos objectos em si é guardada no processo de socialização.
Isto quer dizer que, por exemplo, uma variável estática de uma classe não é seríalizada.
Apenas variáveis que dizem respeito a cada objecto em si o são. Isto faz sentido, uma vez
que quando chamamos serial i ze(), estamos a pedir para serializar um objecto e não
uma classe.

Assim como os campos estáticos não são guardados no resultado de uma serialização, o
programador também pode definir outros campos que não devem ser guardados. Para
isto, basta marcar todos os campos a não serializar com o atributo Nonserialized. Por
exemplo, suponhamos que em Empregado existe um campo chamado UltimaLeitura que
representa o dia e a hora em que o objecto foi lido de ficheiro. Esta informação pode ser
utilizada para, por exemplo, ver se o objecto está actualizado ou não. Não faz sentido
serializar esta informação, uma vez que este campo é actualizado assim que o objecto é
lido de ficheiro. Assim, deve-se marcá-lo como Nonseri ai i zed:
[Serial i zabTe] ' " " " ~~
class Empregado
private string NomeEmpregado;
private int idadeEmpregado;
[NonSériaTizèd] '*
P ri vate pateTime Ul ti maLertu^ra;

Embora o mecanismo de atributos, com o serializable e Nonserialized, seja bastante


poderoso, por vezes, o programador tem de ter maior controlo sobre a forma como a
serialização é feita. Por exemplo, pode ser necessário guardar informação extra sobre um
© FCA - Editora de Informática 32. l
C#3.5 _

objecto que não se encontre nos seus campos ou realizar algum tipo de operações de
sincronização. Caso o programador queira especificar de que forma deve ser feita a
serialização, então, para além de marcar a classe como se ri ai i zabl e, terá de
implementar a interface iserializable. Vamos ver um exemplo simples que
implementa ise ri ai i zabl e para serializar manualmente os objectos de Empregado.

A interface iserializable requer que se implemente o método GetobjectoataQ. Eis a


declaração de iseri ai i zabl e:
interface System. Runtime.Sèrialization. iserializable ....... ;

; void GetObjectDataC
; Serializationlnfo info // os dados do objecto i
StreamingContext context // O contexto da stream destino ;
)i

Quando o programador implementa este método, tem basicamente de colocar em info a


informação que quer guardar, relativamente ao seu objecto, context contém informação
sobre a origem e destino da stream para a qual o objecto irá ser escrito. O método
GetObjectDataC) é chamado quando ocorre a serialização de um objecto, sendo
utilizado para obter o estado do objecto.

Quando um objecto está a ser reconstruído a partir de uma stream, é chamado o


construtor da classe que tenha como parâmetros uma referência serializationlnfo e
uma referência streami ngcontext. O programador é obrigado a declarar esse construtor.
Assim, para ser possível serializar manualmente Empregado, a classe terá de ter o
seguinte aspecto:
[serial i zabl e]
:class Empregado : iserializable
; // chamado por DeserializeQ para criar o objecto ;
' public EmpregadoCserializationlnfo info, StreamingContext context)

// chamado por SerializeQ para guardar os dados do objecto


public
void GetobjectDataCserializationlnfo info, StreamingContext context)

i."".. ........ . ..... .... . . .. :


A nossa implementação irá ser muito simples. Quando o método GetObjectDataC) for
chamado, i n f o irá receber o nome e a idade do empregado. Quando o construtor for
chamado, retiraremos essa informação de info. info permite que lhe sejam adicionados
pares {chave, valor] e que estes sejam consultados, É exactamente essa a abordagem
seguinte:

322 © FCA - Editora de Informática


CLASSES BASE

:[Seria~Iizable] ........ " ' ............... '


class Empregado : iserializable

: // chamado por DeserializeQ para criar o objecto


' public Empregado(serializationlnfo info, streamlngContext context)

i NomeEmpregado = info.GetString("nome") ;
; IdadeEmpregado = info.Getlnt32("idade") í
}
// chamado por SerializeQ para guardar os dados do objecto
public
void GetobjectData(Serializationlnfo info, streamingContext context)
// Coloca a infçrmação necessária em info
info.AddValue("idade" , IdadeEmpregado) ;
info.Addvalue("nome" , NomeEmpregado) ;

Relativamente a iserializable, falta ainda referir um último ponto. Caso o


programador implemente esta interface, todo o processo de seríalização é controlado pelo
programador. Nomeadamente, caso o objecto em causa referencie outros objectos, é da
responsabilidade do programador realizar a serialização desses objectos, assim como a sua
reposição. No entanto, tipicamente, o programador pode delegar essa responsabilidade.
Para isso, apenas é necessário chamar o método GetobjectoataQ sobre o objecto em
causa, no momento em que se serializa o objecto. Para reconstruir o objecto, apenas terá
de chamar o construtor apropriado. Isto é, o seguinte código é típico:
:[SerializabTe] * " ~
Iclass Xpto : iserializable

private int Valor; // Campo que é directamente serializado


! private Xpti Outro; // Referência para um objecto de uma classe :
// que IMPLEMENTA iserializable

public Xpto(SerializationInfo info, StreamingContext context)

Valor = info.GetString("valor");
Outro = new Xpti(info, context);
} ;
public
; void GetObjectData(Serializationlnfo info, StreamingContext context) '.
info.Addvalue("valor", Valor); :
: Outro.GetobjectDataCi nfo, context);
}

Claro que isto apenas funciona para classes que implementem iserializable. Para
serializar objectos de classes que não implementam esta interface, basta adicionar o
objecto a i nf o e mais tarde retirá-lo:
'[Serializable] ' " " " ' " "" " " " "
class Xpto : iserializable '.

© FCA - Editora de Informática 323


C#3.5

pn'vãtéThir"Valor; //campo que é directamente s è r i a T T z a d ò "


p n'vate Xpti outro; // Referência para um objecto de uma classe
; // que NÃO IMPLEMENTA ISerializable
public Xpto(Serializationlnfo info, streamingContext context)
valor = info.GetString("valor");
outro = (Xpti) info.GetValue("outro", typeof(Xpti));

public
void GetobjectData(SerializationInfo info, StreamingContext context)
info.AddValue("valor", Valor);
info.Addvalue("outro", Outro);

Aqui, o único truque é que é necessário utilizar o método GetVal ue() que permite retirar
um objecto que foi serializado para i nfo. Este método exige que se indique o tipo de
dados que se está a ir buscar.

Para terminar esta secção, resta-nos fazer uma chamada de atenção para o facto de o
atributo serializable não ser herdado. Isto quer dizer que, caso o programador declare
uma classe derivada de uma classe serializable, também necessita de marcar esta
classe como sendo Serializable. Caso a classe base implemente iserializable, então,
o método GetobjectDataQ dessa classe deverá ser marcado como vi rtual. Caso isso
não aconteça, não há garantia de que o método correcto seja chamado quando se está a
fazer a serialização de uma classe derivada. Finalmente, neste caso, também é necessário
criar o método GetobjectDataQ na classe derivada, assim como o construtor de
reconstrução do objecto. Em GetobjectoataQ, deverá ser chamado o método da classe
base para reconstruir o objecto base e no construtor, deverá ser chamado o construtor
base. O código seguinte ilustra a forma correcta de realizar estas operações.
irsérializabTe] " "" ;
xlass Base : iserializable
;{ ;
private int ValorBase;
• public Base(SerializationInfo info, StreamingContext context)
ValorBase = info.Getint32("valorBase");

[~ publiç vTrtuaT 7 ~ I ~ V l ~ L l
void GetobjecfDataCsèrializatioriinfò" info, s£réairnngContex~t context)
info.AddValue("valorBase", ValorBase);
;}

[Serializable]
class Derivada : Base
.{
!._.P.GVa.te int ValorDerivada;

324 © FCA - Editora de Informática


CLASSES BASE

public Base(Serializationlnfo info, StreámingContext context)


: base (info, context)
ValorDerivada = info.GetInt32("valorDerivada");

| public override *."."." "V". .L "" T " " "" . 1 _ 7 _ . l


void GetObjectData(Serializátionínfo info, StreámingContext context)

base.GetObiectData(infOj context);
info.AddValue( n valorDerivada", ValorDerivada) ;
}

Na maior parte das aplicações genéricas, não é necessário implementar iserializable.


Os atributos serializable e Nonserialized permitem obter controlo suficiente sobre o
processo de serialização para a grande maioria das situações. Ao mesmo tempo,
implementar iserializable pode tornar-se complicado e levar a problemas sérios.
Assim, o programador deverá considerar muito bem se necessita realmente de
implementar esta interface. Regra geral, tal não é recomendável.

ARETER " Serializar um objecto consiste em criar uma representação do seu estado que
possa ser guardada de forma persistente.
^
Serialização de ~ Todas as classes que possam ser serializadas têm de ser marcadas com o
objectos para atributo Serializable.
formato binário " Todos os campos que não devem ser serializados devem ser marcados com
o atributo Nonserialized.
~ Para serialízar um objecto para um formato binário, cria-se uma instância de
BinaryFormatter, chamando nesta, o método SerializeQ. Neste método é
necessário indicar a síream para a qual o objecto é escrito, assim como o
objecto em causa:
BinaryFormatter formatador = new BinaryFormatterO;
formatador.SerializeCstreamSaida, objecto);
~ Para recuperar um objecto, utiliza-se o processo inverso, chamando o
método DeserializeO- É necessário realizar uma conversão explícita para
o tipo de dados correcto:
BinaryFormatter formatador = new BinaryFormatterO ;
Xpto obj = (Xpto) formatador.Deserialize(streamEntrada);
~ O grafo completo de objectos referenciados pelo objecto indicado em
sen" ai i ze Q é guardado. Todas as classes envolvidas têm de estar marcadas
como sendo Serializable.
" Caso o programador deseje, pode controlar manualmente o processo de
serialização. Para isso, necessita de implementar a interface is e ri ai i zabl e.
Esta operação não ê em geral recomendada e, tipicamente, não é necessária.

© FCA - Editora de Informática 325


C#3.5

8.4.3.2 SERIALJZAÇÃO EM FORMATO XML


Tal como é possível serializar objectos para formato binário, também é possível
serializá-los para formato XML. Para isso, utiliza-se a classe XMLSerializer, do espaço
de nomes system.Xml. serial i zafion. Um objecto em formato XML consiste em texto
marcado por um conjunto de tags que descrevem cada um dos campos do objecto.

No entanto, ao contrário de BinaryFormatter, a classe XMLSerializer impõe certas


restrições ao tipo de objectos que consegue serializar. As principais restrições são:

• Apenas objectos cujas classes são publ i c podem ser seríalizados;

• A classe em questão tem de dispor de um construtor sem parâmetros ou, então, de


um construtor por omissão;

Apenas os campos e as propriedades públicas da classe são serializados.

Assim, no nosso exemplo de Empregado, é necessário declarar a classe como publ i c e


definir um construtor sem parâmetros. Ao mesmo tempo, a fim de que seja possível
serializar o nome e a idade do empregado, são criadas duas propriedades públicas que
permitem obter essa informação. Essas propriedades são automaticamente utilizadas por
XMLSerializer para obter uma visão do estado do objecto. Assim, a classe Empregado
fica:
•"[Se ri ãYfzábl e]
jpublic class Empregado
private string NomeEmpregado;
private int IdadeEmpregado;
public EmpregadoQ
NomeEmpregado = "";
IdadeEmpregado = 0;

public EmpregadoCstring nome, int idade) ;


NomeEmpregado ~ nome;
'• IdadeEmpregado = idade;

public override string ToStringO

return "(" + NomeEmpregado -i- "," + IdadeEmpregado + ")";

public string Nome


get { return NomeEmpregado; }
'. set { NomeEmpregado = value; }

. public int Idade

326 © FCA - Editora de Informática


CLASSES BASE

.get; { neturn idãdeÊmpregado; }


set { idadeEmpregado = vãlue; }

Caso o código seguinte seja executado:


Empregado emp = new Empregado("CarTa"Fonseca", 23); "
Frlestrearn .ficheiro = new Filestream(NOME_FlCHElRO, ..Fi
XmlSerlalizer fopmatador = new xmlserializer(typeof (Empregado).),;;

formatador.Serialize^icheiro, emp) ;

ficheiro-.Close()J ......
é criado um ficheiro XML em disco que contém o objecto serializado. O ficheiro tem o
seguinte aspecto:
<?xmT version="1.0"?> ,
<Empregado xmlns:xsd="http://www.w3.org/2001/XMLSchema 11
xml as :xs4="http://www. w3.org/2001/XMLSchema-instance11> . V '
<tfome>Carla Fonseca</Nome> • *
<Xdacie>23</ldade> ! - -. !
</Empregado>
Note-se que ao criar o objecto formatador, que é uma instância de Xml Serial i zer, é
necessário indicar o tipo de dados do objecto que irá ser serializado. Isto é conseguido
utilizando o operador typeof. Esta informação é necessária para formatador saber qual o
tipo de dados com que tem de marcar a raiz do documento.

Para recuperar urn objecto formatado em XML, basta realizar o processo inverso:
FileS-tream ficheiro = new FiTeStream(NOMÉ_F±CHEIRO, FileMode.Open);
XmlSerTaTizer formatador = new XmlSerial izer(typeof(Empregado);;

Empregado emp = (Empregado) formatador.Deserialize(ficheiro);

ficheiro^.CloseO J
Não iremos falar muito mais de serialização e de recuperação de objectos em XML. No
entanto, iremos indicar alguns ponteiros para leitores que queiram explorar este tópico
mais a fundo.

Em primeiro lugar, apenas o formato e a informação pública contida nos objectos são
guardadas por este processo. A definição do tipo de dados, assim como informação sobre
o assembly não é incluída. Por exemplo, ao ser feita a serialização de Empregado num
sistema e, posteriormente, a sua recuperação noutro sistema, não é garantido que o tipo de
dados (a classe) correspondente seja a mesma.

Os dados gerados por Xml se ri ai i zer são compatíveis com os tipos de dados definidos
pelo W3C (http://www.w3c.org) na XML Schema Definition Language (XSD) 1.0. Um
schema indica a forma como é feita a correspondência entre os campos de uma classe
(incluindo o seu nome e valor) e as tags que surgem num ficheiro XML. Em termos desta
correspondência, o programador pode seguir três abordagens:

© FCA - Editora de Informática 327


C#3.5

Pode utilizar o modo de funcionamento normal de Xml Serial i zer, em que os


elementos XML são derivados dos nomes dos campos e das propriedades. Esta
foi a abordagem seguida no exemplo acima;

Pode anotar os campos das classes, utilizando os atributos xml El ement e


XmlAttribute, indicando uma correspondência explícita entre a classe e os
elementos XML que irão surgir no ficheiro. De seguida, iremos mostrar um
pequeno exemplo deste processo;

• Finalmente, caso o programador disponha de um ficheiro XML com um schema


definido (um ficheiro XSD), então, pode utilizar o utilitário xsd.exe18 que gera
automaticamente as classes necessárias, a partir desse schema. Não iremos
discutir esta abordagem.

Existem diversos atributos que podem ser utilizados para controlar a serialização, em
XML, de um objecto. Nós não os iremos cobrir todos, iremos, apenas, ilustrar de que
forma é que se pode utilizar estes atributos para obter uma correspondência diferente
entre o ficheiro XML e os campos de uma classe.

Como vimos no exemplo Empregado, o ficheiro XML gerado tem o seguinte aspecto:
;<?xiTfl vérsTon="1.0"?> ~""
:<Empregado xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xml ns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Nome>Carla Fonseca</Nome>
<Idade>23</Idade>
i</Ernpregadg> _
A classe possui o nome Empregado, logo no ficheiro XML surge a tag "Empregado".
Existe um campo Nome e um campo idade, logo, são estes que surgem no ficheiro XML.

Suponhamos, que devido a uma imposição de um organismo externo19, Empregado tem de


corresponder à tag "TrabalhadorAssalariado", Nome a "NomeDaPessoa" e idade a
"IdadeDaPessoa".

Para isso, basta anotar a classe com dois atributos. XmlRoot que indica o nome do tipo
raiz que está a ser serializado. Xml El ement é utilizado para indicar outro nome para os
campos da classe. Assim, Empregado fica:
[serial i zabTéJ ~~ ~~~ " " '"' "" ~
[Xml RootC"Trabal nado rAssalariado")]
p u b l i c clas_s Empregado

18 Note-se que o utilitário xsd. exe é muito poderoso, permitindo, não só gerar classes a partir de um schema,
como gerar um schema a partir das classes, inferir um schema a partir de um ficheiro XML e, também,
converter schemas XDR em XSD.
19 Não nos podemos esquecer de que um dos grandes objectivos do uso de XML é a interoperabilidade.
Assim, para que dois sistemas se entendam, um ponto importante é que utilizem as mesmas tags. Isto é, o
mesmo schema.
328 © FCA - Editora de Informática
CLASSES BASE

J" " "" ~' ' ~ "" * "V *^£> ~" ~~ ~' "~
; CXmlETement("NomeDapessoa")] ' *^ ^*"
: public strlng Nome
get'{' return NomeEmpregado; } : ;
; set { NomeEmpregado ~ value; } i

; [xmlElementC"ldadeDapessoa")]
: publi.c -int idade
; get {, return IdadeEmpregado; }
::-{;- IdadeEmpregado = value; }

O resultado da seríalizacão é o seguinte ficheiro:


<?xnfl ,version="1.0"?> ;
<TrabalhadorAssa1ariado '' ;
xml ns: Xsd=" http://www, w3 .,o rg/2001/XMLSchema"
xmTns:xsi="http://www.w3.Drg/2001/XMLSchema-instance">
<NomeDaPessoa>Carl a FonseGã</NomeDaPessoa>
<ldadeDapessoa>23</ldadeDapessoa>
</TrabalhadorAssalariadp> =
Com isto, concluímos a discussão da serialização em XML. No entanto, alertamos para o
facto de estarmos apenas a apresentar a ponta do iceberg. O mundo do XML é bastante
vasto e a sua serialização em .NET envolve muitos conceitos e muitas possibilidades.
Nesta secção, não abordámos tópicos como quais os atributos existentes para controlar a
codificação em XML, de que forma pode o programador controlar manualmente a
serialização e de que modo a herança interfere no processo de serialização XML.

Finalmente, o leitor deverá ficar consciente de que, muitas vezes associada à serialização
de objectos em XML, é referido o protocolo SOAP (Simple Object Access Protocol). Este
protocolo permite realizar chamadas a métodos remotos, existentes noutras máquinas, em
que a informação é trocada em XML. Mesmo não considerando o uso do protocolo
através de uma rede, este tópico é muitas vezes discutido quando se fala de serialização
XML e de "envelopes SOAP" (que também são XML, mas com algumas características
especiais). Por exemplo, é possível criar um ficheiro com um envelope SOAP em XML.
Fica aqui o alerta para o caso de o leitor ver documentação que se refira a XML and
SOAP Serialization, tal como acontece com o MSDN.

- Para serializar um objecto em XML, utiliza-se a classe xmlserializer. A


ARCTHR sua utilização é semelhante a BinaryFormatter.
Serialização de - Na serialização para XML, existem algumas restrições:
objectos em XML - As classes a serializar têm de ser publ i c.
- Apenas os campos e as propriedades públicas de uma classe são
serializadas.

© FCA - Editora de Informática 329


C#3.5

" A classe tem de dispor de um construtor sem parâmetros ou do construtor


A RETER por omissão.
Seríalização de - Ao seríalizar um objecto para XML, é necessário indicar o seu tipo de
objectos em XML dados, Exemplo:
Filestream fs = new FileStream(FlCH, FileMode.Create);
XmlSerializer format = new XmlSeria"Hzer(typeof (Empregado));
format.sería"h"ze(fs, emp);
~ Para recuperar o objecto, o processo é o inverso. Exemplo:
Filestream fs = new FnleStreamCFICH, FileMode.Open);
XmlSerializer format = new XmlSerializerÇtypeof(Empregado));
Empregado emp = (Empregado) format.Deserianze(fs);
" É possível indicar uma correspondência explícita entre o nome dos campos
e das propriedades de uma classe e as tags que aparecem noficheiroXML.
Para isso, utilizam-se atributos como xmlRoot, XmlElement
XmlAttribute.

É possível criar schemas XSD a partir de classes e classes, a partir de


schemas predefinidos, utilizando o utilitário xsd. exe.

330 © FCA - Editora de Informática


9

Quando um programador desenvolve uma aplicação, algo central à escrita do programa é


o seu fluxo de execução. Isto é, existe unia sequência de instruções que vai manipulando
os objectos presentes no programa, de forma a obter um certo conjunto de resultados.
Hoje em dia, uma funcionalidade que todos os sistemas modernos suportam é a existência
de diversos fluxos de execução simultâneos num programa. A cada fluxo de execução
chama-se uma thread. Até agora, temos desenvolvido apenas aplicações com uma thread
(single threaded). No entanto, virtualmente, todas as aplicações escritas hoje em dia são
multithreaded, isto é, possuem diversos fluxos de execução simultâneos (figura 9.1).
O sistema operativo encarrega-se de comutar a execução entre as diversas threaãs em
causa, dando a noção de que todas elas se encontram a executar simultaneamente.

Aplicação Single threaded Aplicação Multithreaded

Figura 9.1 — Aplicações single threadedvs. multithreaded

Existem diversos motivos pelos quais é útil poder ter diversos fluxos de execução num
programa. Vejamos alguns. Suponhamos que temos um browser de Internet (por
exemplo, o internet Explorer ou o Firefox). Ao ir buscar uma página web à Internet, é
possível ir buscar primeiro o texto da página e depois, sequencialmente, cada uma das
imagens presentes na página. No entanto, tal processo é muito fastidioso do ponto de
vista do utilizador. Primeiro aparece o texto, a primeira imagem, a segunda imagem e
assim sucessivamente. É muito mais interessante o utilizador ver o texto e cada uma das
imagens aparecer mais ou menos simultaneamente. A utilização de threads permite
programar isto de forma simples. Ao receber o texto, o browser verifica as imagens que
existem na página e cria uma thread para fazer o carregamento de cada imagem. Cada
imagem irá ser carregada simultaneamente e mostrada à medida que vai chegando.
© FCA - Editora de Informática 331
C#3.5

Uma outra razão para o uso de threads é a performance. Imaginemos que é necessário ler
de um ficheiro, um certo conjunto de dados e processá-los. O acesso a disco é uma
operação lenta. Enquanto os dados são lidos do disco, o processador da maquina está
simplesmente à espera que os dados sejam lidos, estando sem ser utilizado. Após a leitura
dos dados, é feito o seu processamento e, durante esse tempo, poder-se-ia estar a ler os
dados seguintes. No entanto, se a aplicação apenas tiver uma thread, tal não é possível.
Esta situação é ilustrada na figura 9.2. Usando apenas uma thread, todo o processamento
é feito sequencialmente.

a) Execução apenas com uma thread. Processamento sequencial.

•l
b) Execução com duas threads. Enquanto uma lê os dados, a outra faz o
processamento dos dados existentes.

| | Leitura de disco
Hl Processamento

Figura 9.2 — Uso de threadspara aumento de performance

Pelo contrário, se utilizarmos duas threads, as leituras de disco podem decorrer em


simultâneo com o processamento de dados. Uma thread é encarregue de ler dados de
disco, enquanto a outra faz o seu processamento. Note-se que no caso de uma máquina
com apenas um processador, existe aumento de desempenho porque a thread que faz a
leitura de ficheiro não necessita praticamente de utilizar o processador. Esta thread fica
simplesmente à espera de que os dados sejam transferidos de disco. O sistema operativo
"bloqueia" a thread, não gastando ciclos de CPU, até que a leitura complete. Quando a
leitura termina, o sistema operativo coloca novamente a thread num estado disponível
para execução. Na figura 9.2, podemos ver que enquanto a aplicação multithreaded
consegue completar o processamento de três blocos de dados completos, a aplicação com
apenas uma thread apenas completou dois, estando ainda a ler os dados do terceiro bloco.

Note-se que as threads não estão realmente a executar simultaneamente. Ou seja, criar
três threads para realizar cálculos intensivos que envolvem a utilização do processador
não irá fazer com que a aplicação execute três vezes mais depressa. Ao criar diversas
threads, o sistema operativo limita-se a criar um stack para cada thread e a gerir o local
do código em que cada thread se encontra a executar. Periodicamente, o processador
interrompe uma thread e coloca outra a executar. Neste processo, o sistema operativo
guarda o local onde se encontra a thread em questão, assim como todas as variáveis
associadas à thread, e restaura o estado que a thread que irá executar a seguir tinha
332 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE

quando foi retirada de execução. Cada thread executa durante um certo período de tempo
(chamado time slicé) ou até bloquear por algum motivo. Por exemplo, se uma thread
tentar ler dados de um ficheiro, esta irá bloquear até que a leitura termine. Entretanto,
outras threads podem executar.

Neste capítulo, iremos explorar de que forma é que, na plataforma .NET, se desenvolvem
aplicações que utilizam diversas threads, assim como de que forma é que se sincroniza a
sua execução. Não iremos, no entanto, abordar aspectos mais avançados de programação
concorrente e de sincronização avançada, uma vez que tais tópicos são bastante
complexos, muitas vezes, envolvendo uma compreensão profunda de mecanismos
internos dos sistemas operativos.

9. l GESTÃO DE THREADS
Criar uma nova thread na plataforma .NET é extremamente simples. O programador
necessita de criar uma classe com um método que não possua parâmetros de entrada e que
retome void. Este será o método onde uma nova thread começará a executar e
corresponde à assinatura do delegate system.Threading .Threadstart:
'public delegate void Threadstart() ;.
Vamos, então, implementar uma classe chamada worker. Esta classe irá fazer algo muito
simples. Irá dormir durante um certo tempo e, quando acorda, imprime uma string no
ecrã. A implementação de Worker poderá ser:
class Worker
!{ - ' :

prjva-çe int Tempo;


privaste, string Msg;
public^WorkerCint tempo, string msg)
{ ,-
trqs,.Tempo = tempo;
this.Msg = msg;

public void Rim O


•C
while (true)
"i. * - . ' . " :
Thread.,sieep(Tempo) ; // Dorme durante Tempo miTissegtífijdos
• éò/iSlô^.WriteLineCMsg) I " ' - *>, ,f
j , "*'",. •'• - .'
j-.. ...." .... ... . . : '•"' " r .
Esta classe possui dois campos privados: a cadeia de caracteres a imprimir e o intervalo
de tempo que a thread dorme. O método que a thread irá executar chama-se Run C) • Neste
método, a thread dorme durante um certo tempo, usando o método estático Thread.
.si eep Q, e mostra a mensagem. Thread. si eep Q permite suspender a execução da
thread corrente, durante um certo período de tempo, especificado em milissegundos.

© FCA - Editora de Informática 333


C#3.5

Para criar uma nova thread que utilize este código, são necessárias duas coisas. Por um
lado, é necessário ter um objecto do tipo Worker. Ao mesmo tempo, é necessário criar um
objecto que representa a nova thread (o novo fluxo de execução). Esse objecto é do tipo
Thread. Na prática, faz-se:
Worker "dorminhoco = riew WorkerCLOOO, "oTáT")';"
Thread dorminhocojhread = new Thread (new Threadsta_rtCdorminhocp.Run));_
Neste caso, dorminhoco é um objecto do tipo Worker, que irá imprimir, de segundo a
segundo, a mensagem "Olá". dormi nhocoThread é o objecto que representa a nova
thread e é criado, especificando que esta irá executar o método RunQ de dormi nhoco.

Nesta altura, a nova thread ainda não está em execução. Para iniciar a sua execução, é
necessário chamar o método startQ no objecto que representa a thread:

Ao chamar este método, a nova thread começa a executar, imprimindo, de segundo a


segundo, a mensagem. Ao mesmo tempo, a thread principal do programa (a original)
continua a executar após a linha do startQ.

A listagem 9.1 contém um programa com duas threads. Uma das threads dorme durante
um segundo e imprime a sua mensagem, enquanto a outra dorme durante cinco segundos,
também mostrando uma mensagem.

/*
* programa que ilustra a criação de threads.
*/
using System;
using System. Threading;
class Worker
{
p ri vate int Tempo;
private string Msg;
public Worker(int tempo, string msg)
this.Tempo = tempo;
this.Msg = msg;

public void RunO


while (true)
Thread. si eep (Tempo) ;
Console.WriteLine(Msg) ;
}

class Exemplocap9_l
static void Mai'nÇstn'ng[] args)

334 © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

Worker dorminhoco = new Worker(5000, "Dorminhoco - zzzz - olá...");


worker activo = new WorkerClOOO, "Activo -- olá, olá, olá!");
Thread dorrninhocoThr = new Thread(new ThreadStart(dorminhoco.Run));
Thread activoThr = new Thread(new Threadstart(activo.Run));
dormi nhocoThr.StartQ;
activoThr.startQ;

Listagem 9.1 - Criação de threads(ExemploCap9_l.cs)

9. l . l ComROLQ DE THREADS
Como vimos, sempre que se quer criar urna thread, é necessário criar um objecto que
represente essa thread. Esse objecto é do tipo Thread e permite ao programador, controlar a
sua execução. Vejamos os principais métodos associados à utilização de uma thread Para
isso, iremos utilizar o objecto dorminhocoThread, anteriormente criado.

Para começar a execução de uma thread, utiliza-se o método startQ. No entanto, se a


certa altura da sua execução for necessário matar a thread, utiliza-se o método AbortQ.
Caso se chame dorminhocoThread.AbortQ, irá ser lançada uma excepção do tipo
ThreadAbortException, no fluxo de execução da thread. Tipicamente, o lançamento
desta excepção leva à terminação da thread em causa. Esta excepção pode ser apanhada
utilizando um bloco try-catch-finally, mas é novamente lançada automaticamente, no
final do bloco em causa. Isto faz com que o fluxo de execução da thread salte entre
blocos catch/finally até à sua terminação. Regra geral, esta forma de matar threads
deve ser evitada a todo o custo, pois pode deixar os objectos que a thread utiliza, num
estado inconsistente. Sempre que possível, é preferível ter uma variável lógica que a
thread examina, verificando se deve terminar por si mesma.

O método suspendC) permite suspender a execução de uma determinada thread até que
Rés ume C) seja chamado. Por exemplo, se fizéssemos:
dorminhocoThread.SuspendXl;..
a thread dormi nhocoThread iria ficar parada até que se fizesse um:
dorminhocoThread\Resumé0j. __
Fazer um suspend Q numa thread já suspensa não tem efeito.

Um outro método que é por vezes útil é o método i n t e r r u p t Q . Este método


interrompe a execução de uma certa thread, lançando uma excepção do tipo
ThreadlnterruptedException, no seu fluxo de execução. O programador deve apanhar
esta excepção no seu código, realizando as operações que desejar. Ao contrário de
ThreadAbortException, esta é uma excepção absolutamente normal. Este método é útil,

© FCA - Editora de Informática 335


C#3.5 ====^^=^^^^^^^==^===^^=^^^^=^.

por exemplo, em casos em que uma thread está bloqueada durante muito tempo ou que
tem de ser notificada, assincronamente, de certos acontecimentos.

Finalmente, existe mais um método bastante útil. O método 3oin() permite esperar que
uma outra thread morra, suspendendo a execução da thread corrente até que isso
aconteça. Vejamos um exemplo simples.

Suponhamos, agora, que o método Run Q de Worker irá simplesmente imprimir uma linha
com o identificador da thread a correr. O identificador da thread irá ser, simplesmente, um
inteiro que lhe é passado como parâmetro, no construtor. Ou seja, Worker irá ser:
cias s Worker '•
// Total de escritas a realizar :
const int TOTALJA/RITES = 6 0 ; :

// identificador da thread
private int id;
public WorkerCint i d)
this.Id = id; ;
public void RunQ
{ :
for (int i=0; i<TOTAL_WRITE5; i++)
console.Write("{0}", id) ;
} ;

Se nós tivermos o seguinte código no programa principal:


class TesteHoin
{
static void Main(string[] args)
Worker worker = new Worker(O);
Thread workerThr = new Thread(new Threadstart(worker.Run));
workerThr.startQ ;
Console.writel_ine("Fim do programa");

>
ao executar o programa, pode surgir:
Fim do programa
oooQooooppoooooooooopooooQpoopooppoopooooooooppooooooopooooo
Ou seja, "Fim do programa" surge antes da impressão dos zeros. Como ambas as threads
estão a executar ao mesmo tempo, de forma concorrente, neste caso, a thread origina]
imprimiu a sua mensagem antes da thread workerThr. Assim, a fim de garantir que a
thread original apenas executa após workerThr, basta chamar workerThr. Dói n Q:
class Testeaoin
{
static void Main(string[] args)

336 © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

Worker worker = new WorkerTO!); ----- -


Thread workerThr = new Thread(new Threadstart(worker.Run));
workerThr.StartC);
workerThr.3oinO ; // Espera pela mor^e de workerThr
console.WriteLine("Fim do programa");
; >
Neste caso, o resultado da execução deste programa será sempre:
oaoooooooooooooooooooooooooooo'00000'ooooooooooooouoGOoaooaoocr"
Fim do programa . _ __
Este é um ponto extremamente importante. A fim de que a execução de um programa
concorrente seja determinística, é necessário garantir uma sincronização correcta das suas
threads.

9.1.2 A CLASSE THREAD


A tabela seguinte sumaria os principais métodos da classe Thread. Note-se que estes
métodos manipulam o fluxo de execução de uma certa thread, para a qual se tem uma
referência.
MÉTODO/PROPRIEDADE [ DESCRIÇÃO
Thread(ThreadStart start) | Cria uma nova thread, indicando o método que esta deve executar.
static Retorna uma referência para a thread que. se encontra a executar
Thread CurrentThread
{ get; } (a thread que. chamou o método).
bool Verifica se a threadse. encontra viva.
isAlive { get; }
Permite manipular o estado de uma thread (background ou
bool forground}. Um programa só termina quando todas as threads
IsBackground { get; set; } forground terminarem. Uma thread background é idêntica a uma
thread forground, mas impede que um programa termine.
string Obtém ou altera o nome da thread. Caso set nunca tenha sido
Name { get; set; } chamado pelo programador, Name é nuTl.
Altera a prioridade de execução de uma thread. Tipicamente/ o
modo de escalonamento de threads implementado no sistema
operativo faz com que sejam sempre as threads fe prioridade mais
ThreadPriority elevada a executar, em detrimento das de prioridade mais baixa.
priority { get; set; }
Enquanto houver uma thread de prioridade mais elevada, uma
thread de prioridade mais baixa não executa. No entanto, esta
semântica não é totalmente garantida.
Threadstate
Threadstate { get; } Retorna o estado corrente da thread.
Lança uma ThreadAbortException no fluxo de execução da
void thread. Tipicamente, chamar este método leva è terminação da :
AbortQ ;
thread.
Interrompe a execução da thread. Isso é conseguido lançando uma
vold : ThreadlnterruptedException no seu fluxo de execução. O
XnterruptQ; programador pode apanhar esta excepção tomando medidas
adequadas.

© FCA - Editora de Informática 337


C#3.5

(com.)
l MÉTODO/PROPRIEDADE DESCRIÇÃO

void Bloqueia a thread corrente até que a thread sobre a qual se


Jo-inO; chamou aoinO termine.
void
ResumeC) ; Retoma a execução da ffireadapós esta ter sido suspensa.
static Suspende a execução da thread corrente durante time
STeepCint time) milissegundos.
startQ l
Coloca a thread a executar/ chamando o método especificado no
delegate do construtor.
SuspendC) Suspende a execução da thread'até que ResumeC) seja chamado.

Tabela 9.1 — Principais elementos da classe Thread

Uma thread representa um fluxo de execução dentro de um programa. Cada


A RETER
programa pode possuir diversas threads simultâneas.
Gestão O sístema operativo encarrega-se de comutar entre threads, fazendo com que
de threads pareça que estas executam simultaneamente. Cada thread executa durante um
período de tempo chamado time slice.
- Em certas ocasiões, uma thread pode bloquear à espera de um determinado
acontecimento. Enquanto unia thread está bloqueada, não utiliza CPU. Um
exemplo disto é quando uma thread bloqueia até que uma operação de
entrada/saída complete.
~ Para criar uma thread, é necessário criar uma instância de Thread e
indicar-lhe qual o método que será executado quando esta começar a
executar. Para isso, utiliza-se o delegate Threadstart. Exemplo:
Trabalhador trab = new TrabalhadorC);
Thread trabThr = new ThreadCnew ThreadStart(trab.Run));
- Para mandar executar uma thread, utiliza-se o método startQ. Embora não
seja recomendável, uma thread pode ser morta com o método Abort().
- E, ainda, possível suspender uma thread com Suspendo e retomar a sua
execução com Resume Q.
" O método joinO suspende a execução da thread corrente até que a thread
sobre a qual o método foi chamado termine.

9.2 SINCRONIZAÇÃO a

No exemplo que apresentámos na secção anterior, examinámos uma forma muito


rudimentar de sincronização. O método J o i n Q permite esperar que uma determinada
thread termine a sua execução. Este tipo de operação é necessário porque, havendo
diversas threads a executar simultaneamente, muitas vezes, é necessário coordenar a sua
execução. No exemplo apresentado anteriormente, a thread principal não deveria imprimir
a sua cadeia de caracteres antes da thread workerThr. Para garantir esta condição, foi
necessário sincronizar ambas. Este é um exemplo de uma sincronização simples. Vamos
examinar, agora, algumas outras formas de sincronização.

338 © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

9.2. l O PROBLEMA DA ATOMICIDADE


Vejamos, agora, outro exemplo. Suponhamos que temos diversos utilizadores num sistema
(classe utilizador), sendo cada utilizador representado por uma thread independente.
Esses utilizadores gostam bastante de escrever, imprimindo para uma impressora (classe
Impressora). A classe impressora é muito simples, dispondo apenas de um método
Imprime C):
class impressora
public void lmprime(int idutilizador, string msg)
Console. WriteLine(" ------ Trabalho de {0} ----- ", idutilizador) ;
Console.WriteLine(msg) ;
Console. WriteLineC"-- Fim do trabalho de {0} — ", idutilizador);
Console. WriteLineQ ;
}

A classe utilizador tem um construtor que leva como parâmetro o identificador do


utilizador e uma referência para a impressora que este utiliza. A única coisa que um
utilizador faz na vida é imprimir poemas.
class utilizador
private const int TOTAL_POEMAS = 20;

p ri vate int Id;


private impressora impressora;
public UtilizadorCint id, impressora impressora)

this.Id = id;
this. impressora = impressora;
}
public void RunQ
for (int i=0; Í<TOTAL_POEMAS; Í-H-)
// Escreve um poema
Thread. SleepCIO); .-,._,..,„
String poema = "Poema " + i + do utilizador id= + Id;
// imprime o poema
Impressora. Imprime(id, poema) ;

No código principal, são criados uma impressora (trocaTintas) e 10 utilizadores. As


threads dos utilizadores são colocadas em execução.
class Exemplocap9_2
private const int TOTAL_UTILIZADORES = 10;
static void Main(string[] args)
Impressora trocaTintas _= new_ Imp_resspra_Q; ..... _ _

© FCA - Editora de Informática 339


C#3.5

Utilizador[] pessoa = new Utilizador[TOTAL_UTILiZADORES];


Thread[] pessoaThr = new Thread[TOTAL_UTILIZADORE5];
for (int i=0; i<pessoaThr. Length; Í-H-)
pessoa[i] = new utilizador(i, trocaTintas);
pessoaThr[i] = new Thread(new ThreadStart(pessoa[i].Run));

foreach (Thread utilizador i n pessoaThr)


utilizador.startQ;
J . „_ _. . . .
Se executarmos este programa, de início tudo corre bem, aparecendo os trabalhos
correctamente impressos:
-----trabalho de o ------
ipoema O do utilizador id=0
: — Fim do trabalho de O —

' Trabalho de l
;poema O do utilizador id=l
'— Fim do trabalho de l —
Trabalho de O
:poema l do utilizador id=0
'.— Fim do trabalho de O —
Trabalho de 2
Poema O do utilizador id=2
— Fim do trabalho de 2 —

Note-se que as impressões das várias threads não surgem ordenadas, uma vez que cada
thread. está a competir por utilizar o processador. O sistema operativo da máquina
encarrega-se de comutar entre as diversas threads em execução, de acordo com a
prioridade de cada thread e com o tempo que esta já esteve a executar.

No entanto, se continuarmos a examinar a execução do programa, irão começar a surgir


linhas estranhas. Por exemplo:
;——- Trabalho de 4 ——-
: Poema l do utilizador id=4
Trabalho de 3
Poema l do utilizador id=3 :
— Fim do trabalho de 3 —
— Fim do.trabalho de 4.-.-.
Este resultado pode parecer surpreendente, mas é perfeitamente possível. Enquanto a
impressora estava a imprimir o trabalho do utilizador número 4 (ou seja, enquanto
pessoaThr [4] se encontrava a executar), o sistema operativo escalonou uma outra thread
para execução. Neste caso, a thread correspondente à pessoa número 3 (pessoaThr [3]).
No entanto, isto aconteceu após a escrita da linha "Poema l do utilizador id=4" e antes de
o trabalho ter sido dado por concluído. Entretanto, pessoaThr [3] mandou imprimir o seu

34O © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

trabalho. Quando esta thread terminou a sua execução, a thread correspondente à pessoa
4 voltou a executar, terminando a sua impressão: "— Fim do trabalho de 4 —".

Este não é o comportamento que se deseja para o programa. Um trabalho que se encontra
a ser impresso numa impressora nunca deverá ser interrompido a meio. (Imaginemos só a
confusão de folhas que sairiam, numa impressora real!).

Um bloco de código que não deverá ser interrompido a meio, em que apenas uma thread
o pode estar a executar em cada momento, chama-se uma secção crítica. Todo o código
presente numa secção crítica tem de ser executado de forma atómica, sem ser interrompido.
Resolver este problema no programa é muito simples. Basta utilizar a palavra-chave
lock:
public void Imprime(int idutilizador,l string msgj • • — - . - ........ . . . 5 ...

"lock" (this)..........~ ~ " ""' ..... " ~ ...... ...... " ..... "~ ~
{
console. writeLine(" ------ Trabalho de {0} ----- ", idutilizador) ;
Console.WriteLine(msg) ;
Console. writeLine("~- Fim do trabalho de {0} — " , idutilizador) ;
Console. writeuineQ ;
_ __ ___ __ _
y; "". ...... " "" :;"""". _:T:_________" . . ._T;:L:___:.:_____".;;:.. :r__::"~';"7~~
Sempre que se faz um lock sobre um objecto, apenas uma thread pode executar dentro
do bloco marcado como lock, para o objecto em causa. Todas as outras threads
bloqueiam à entrada do bloco de código, para aquele objecto, até que a thread que se
encontra a executar no seu interior saia. Ou seja, o bloco é executado atomicamente e
apenas por uma thread de cada vez.

Note-se bem que as threads têm de fazer o lock sobre o mesmo objecto. Se diferentes
threads executarem o lock sobre objectos diferentes, irá existir atomicidade na execução
do bloco de código em causa, mas apenas para as threads que façam lock sobre os
mesmos objectos. No caso do exemplo acima, o lock é feito unicamente sobre o objecto
da impressora em causa.

Se uma função pode ser invocada sem problemas, por diversas threads, diz-se que é
thread safe. A versão original de imprime Q não era thread safe. A listagem seguinte
apresenta a implementação deste programa, já com a protecção necessária, sendo thread
safe.

/*
* Programa que ilustra a utilização de lock.
*/
using System;
using System.Threading;
using System.IO;
class Impressora

© FCA - Editora de Informática


C#3.5

public void lmprime(int idutilizador, String msg)

lock (this)
Console.Writel_ine(" Trabalho de {0} ", idutilizador);
Console.WriteLine(msg);
console.writeLine("— Fim do trabalho de {0} --", idutilizador);
Console.WriteLineQ;

class Utilizador
private const int TOTAL_POEMAS = 20;

private int Id;


private Impressora impressora;
public utilizador(int id, Impressora impressora)
this.ld = id;
this.impressora = impressora;

public void RunQ

for (int 1=0; i<TOTAL_POEMAS; i++)

// Escreve um poema
Thread.Sleep(lO);
String poema = "Poema " + i + " do utilizador id=" + Id;

// imprime o poema
Impressora.imprimeCid, poema);

}
class Exemplocap9_2

private const int TOTAL_UTILIZADORES = 10;


static void Main(string[] args)

impressora trocaTintas = new ImpressoraQ;

utilizadorC] pessoa = new utilizador[TOTAL_UT!LlZADORES];


Thread[] pessoaThr = new Thread[TOTAL_UTILIZADORES];
for Cint i=0; i<pessoaThr. Length;
pessoa[i] = new Utilizador(i, trocaTintas);
pessoaThr[i] = new Thread(new Threadstart(pessoa[i].Run));

foreach (Thread utilizador in pessoaThr)


utilizador.startC);

Listagem 9.2 — Utilização del ock (ExemploCap9_2.cs)

© FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

9.2.2 SECÇÕES CRÍTICAS


Quando se desenvolve aplicações concorrentes, é necessário ter extremo cuidado.
Tipicamente, tudo o que envolve manipular variáveis partilhadas por diversas threads
constitui uma secção crítica, tendo de ser realizado de forma atómica (isto é, sem
interrupções). Vejamos dois exemplos simples que aparentemente não trariam problemas.

Imaginemos que temos uma classe ContaBancaria, com um método Levantamento C),
que permite retirar dinheiro da conta. Um levantamento apenas pode ocorrer se existir
saldo suficiente:
class ContaBancaria

p n" vate int Saldo;

public bool Levantamento (i nt montante)

i f (Saldo > montante)


{
Saldo -= montante;
return true;
}
else
return false;
}

Caso várias threads possam chamar simultaneamente Levantamento C), pode haver um
grave problema. Suponhamos que tendo duas threads, a primeira chama Levantamento Q,
sendo bloqueada após o teste da condição 1 f (saldo > montante). Neste momento, a
condição era verdadeira, preparando-se a thread para fazer o levantamento em. causa.
Entretanto a segunda thread é escalonada e faz também um levantamento. Infelizmente,
esta thread retira todo o dinheiro existente na conta. Ao voltar a executar, a primeira
thread irá continuar a sua execução, efectuando o levantamento e resultando num saldo
negativo. Como podemos ver} este código deveria ser executado em exclusão mútua. Isto
é, apenas por uma thread de cada vez:
p u b l i c bool Levantamento (int montante)
lock (this)
i f (saldo > montante)
{
Saldo -= montante;
return true;
}
else
return false;
............ " ••
Examinemos, agora, um segundo exemplo, aparentemente ainda mais simples. Admitamos
que duas threads chamam o método incrementaVendasQ da classe Loja:
class Loja

© FCA - Editora de Informática 343


C#3.5

p ri" vate "Trit Vendas" ;~


publlc void IncrementaVendasC)
-H-vendas;

!}_.„.. . „ . . . ... _ . ._
Aparentemente, é impossível haver problema: apenas estamos a incrementar o valor de
uma variável. No entanto, se verificarmos o código MSIL gerado pelo compilador,
encontraremos o seguinte1:
j.mêtfíòcl~ pubTicliidebysíg ~Trístance~void incrementãVendasQ cil managed
, // Code size 15 (Oxf)
.maxstack 3
IL_0000: ldarg.0
: IL_0001: dup ;
IL_0002: Idfld int32 Loja::Vendas
: IL_0007: l de.14.l
IL_0008: add
IL_0009: stfld int32 Loja::Vendas
: lL_OOOe: ret
.}..// e"d .pf method Loja::lncrementavendas
O ponto importante é que para incrementar a variável vendas, em termos de código
intermédio, são utilizadas quatro instruções:
"ÍU-0002:" Idfld ~~ int32 Loja: :Vendas //OBTÉM O VALOR DA VARIÁVEL DE
MEMÓRIA
:IL_0007: l de.n 4.l // CARREGA O VALOR l
ÍIL_0008: add // ADICIONA AMBOS
:Í_U=Q009.I _stfl_d . _ Jnt32. Loja:.:Vendas //COLOCA O RESULTADO EM MEMÓRIA
Se duas threads, por acaso, executarem este código mais ou menos simultaneamente,
pode acontecer o seguinte: uma thread carrega o valor da variável em memória (por
exemplo, 12) para um registo, após o qual é suspensa. A segunda thread faz o mesmo}
incrementa a variável (13) e coloca-a em memória. Finalmente, a primeira thread volta a
executar, incrementa a sua cópia da variável (com o valor antigo, 12) e coloca-a também
em memória. Resultado: duas threads executaram o método IncrementaVendasC) mas a
variável apenas surge incrementada uma vez (isto é, com o valor 13).

A lição a reter é simples: caso várias íhreads manipulem variáveis partilhadas entre elas,
é necessário garantir a atomicidade na sua actualização e manipulação.

Quando se trata simplesmente de garantir atomicidade na manipulação de um objecto


obj, basta fazer
Cobj) '•-
" _ , / / .B!oco de..códijo_que manipula obj de forma atómica

Para conseguir examinar o código MSIL de uma classe, utiliza-se a aplicação "i l dasm. exe.
344 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE

No entanto, muitas vezes é necessário garantir a atomicidade no acesso a mais do que um


objecto. Por exemplo, caso se esteja a fazer uma transferência de dinheiro entre duas
contas, é necessário garantir que o retirar do dinheiro de uma conta e o colocar numa
outra é atómico. Isso envolve dois objectos conta. É possível fazer vários lock
encadeados ou 1 ock dentro de um outro bloco l ock. Por exemplo:
class ContaBancaria
private int Saldo;
public bool Transferencia(int montante, ContaBancarià outraConta)
lock (this)
lock (outraconta)
i f (Saldo > montante)
outra. saldo += montante;
return true;
else
return false;

Sempre que é necessário fazer urn l ock sobre diversos objectos, é muito importante obter
o lock de cada objecto pela mesma ordem em todas as threaãs. Caso contrário, poderá
surgir uma situação de impasse (deadlock) em que nenhuma das threads consegue
avançar. Por exemplo, suponhamos que temos duas threads que fazem lock em dois
objectos diferentes por ordem inversa:
// threadA ..... // threadB
lock (objl) lock (obj2)
lock (obj2) lock (objl)
// utiliza ambos os objectos // Utiliza ambos os objectos

;,.>'" ......... ..............i'.".'_________._. . .


Neste caso, ambas as threads podem bloquear para sempre. Imaginemos que a threadA
obtém o lock sobre objl. Ao mesmo tempo, threadB obtém o lock sobre obj2.
Entretanto, a threadA tenta obter o lock de obj2. Como este pertence à segunda thread,
irá bloquear. Da mesma forma, threadB tenta obter o lock sobre objl. Este pertence à
threadA, logo, também bloqueia. Neste momento, ambas as threads estão bloqueadas e
sem possibilidade de sair desta situação. Tal problema desaparece se ambas as threads
fizerem l ock pela mesma ordem.

Para terminar, falta referir de que forma se pode conseguir atomicidade no acesso às
variáveis estáticas de uma classe. Neste caso, não se pode fazer um lock sobre um objecto,

© FCA - Editora de Informática


C#3.5

pois estas variáveis estão associadas à classe e não a um objecto em particular. Para se
obter atomicidade no acesso às variáveis estáticas de uma classe, deve fazer-se um l ock
sobre o objecto que representa o tipo de dados da classe. Isto é:
nock(typeof(lmpresso,ràj} v ' =; - ^
1 // Acesso atómico às>. var^áv^s ~es;M"t.1cas de Impressora
} ••••;.••• ;- .-•„.:•'.'*- " - • • .'
...... . . - : : " . . . " . ' - -. - - - . .. - - - - ~

permite aceder, de forma atómica, às variáveis estáticas da classe impressora.

Urna secção crítica representa um bloco de código que tem de ser executado
ARETER atomicamente por uma thread, sem ser interrompida.
&>*^
Atomicídade e Sempre que várias threads podem aceder simultaneamente a variáveis
secções críticas partilhadas, na maioria dos casos, tal acesso tem de ser feito em exclusão
mútua, numa secção crítica.
Se diversas threads fizerem lockCobj) { ... >, apenas uma thread
executa o bloco de código de cada vez. A execução é feita em exclusão
mútua.
Para obter exclusão mútua no acesso às variáveis estáticas de uma classe,
faz-se 1ock(typeof(Nomeoaclasse)) • £ • . . }
Para evitar situações de impasse, os 1 ock sobre diversos objectos devem ser
feitos pela mesma ordem, em todas as threads intervenientes.

9.2.3 ACUVSSEMUTEX
A classe Mutex representa um mutual exclusion lock. Ou seja, um objecto que permite
obter exclusão mútua na execução de secções críticas. A sua funcionalidade é idêntica ao
uso que demos até agora à palavra-chave l ock, só que sob a forma de uma classe.

Existem dois métodos principais nesta classe: WaitOneQ e ReleaseMutexQ, Ao chamar


WaitoneQ, uma thread bloqueia se outra já obteve o acesso ao Mutex ou, então, continua a
executar, caso seja a primeira a obter acesso. Ao chamar ReleaseMutexQ, a thread
informa que acabou a secção crítica que se encontrava a executar, podendo uma outra
thread adquirir acesso. Caso existam threads bloqueadas à espera de obter acesso, apenas
uma é libertada, sendo essa a que adquire o Mutex.

Para criar urn Mutex, basta fazer:


-MUtex "exclusaoMutua =.new Mutex_Q ; ____'_ ~_ ___ '_" \ .
Para obter atomicidade numa secção crítica, basta fazer:
çXteTíisaoMutua.WaitoneO; " "" -" -.'
K/ ^fcoas uma thread executa de cada vez este código
/•/|)/:;codigo é executado em exclusão mútua

346 © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

"exc]usapMutua.ReleaseMutex'Qj" " _~
Na verdade, este código é equivalente a escrever:
,// Criação de um objecto' para "servi r de Mutex
'object mutex = new objectO;

lock (mutex)
// Execução com atoml cidade

sendo, no entanto, muito mais elegante utilizar a classe Mutex.

Uma dúvida muito válida é perguntar porque é que não se faz simplesmente lock(obj)
quando se necessita de ter atomicidade no acesso a variáveis do objecto obj. A razão é
simples. Sempre que se executa uma secção crítica tendo atomicidade, apenas uma thread
executa de cada vez. Se numa classe, existirem diversas variáveis de instância não
relacionadas, mas que têm de ser acedidas com atomicidade, então, sincronizar todas as
threads da aplicação que têm de aceder a estas leva a uma notória perda dt performance.
Deve-se utilizar o mínimo de sincronização possível e em blocos de código breves. Neste
exemplo, deve-se criar diferentes objectos de sincronização para as variáveis não
relacionadas, sincronizando apenas as threads que estão a aceder às mesmas variáveis.
Para isso, cria-se vários Mutex, fazendo a sincronização com estes. Por exemplo, vejamos
a classe ContaBancarla, com os campos Saldo e Nomeei lente:
class contaBancarla

pnvate int saldo;


p n" vate string Nomecllente;

O método Levantamento () necessita de interagir com o campo Saldo, mas não com
Nomecllente. Por outro lado, uma propriedade cliente, que representa o nome do
cliente, apenas necessita de interagir com o campo Nomecllente. Se várias threads
podem interagir com esta classe, não deverão sincronizar todas no objecto. As que
invocam Levantamento C) apenas devem sincronizar entre si, assim como as que
interagem com a propriedade Cliente, apenas devem sincronizar com as que também o
fazem. Ou seja, o código para este exemplo pode ser algo semelhante a:
class ContaBancarla
. private Int saldo;
private string Nomecllente;
private Mutex SaldoMutex = new MutexQ;
: private Mutex NomeClIenteMutex = new MutexQ;

publlc bool Levantamento(1nt montante)

© FCA - Editora de Informática 347


C#3.5

bòoT l evãritamentoõk = false;


saldoMutex.WaltoneQ ;
if (saldo > montante)
{
Saldo -= montante;
levantamentook = true;
}
Sal doMutex . Rei easeMutexC) ;
return levantamentook;

publlc string cliente


get
string nome;
NomecTienteMutex.WaitOneQ ;
nome = Nomeei n ente;
Nomeei 1 enteMutex . Rei easeMutex C) ;
return nome;

set
{
Nomeei i enteMutex, WaitOneQ ;
Nomeei lente = value;
Nomeei i enteMutex . Rei easeMutex C) ;

" Os objectos de Mutex permitem obter exclusão mútua entre threads que
ARETHR executam concorre n temente.
_.
Classe „ .
Mutex " Para criar um Mutex, faz-se: Mutex mutex = new MutexQ;
- Para criar uma secção crítica, acedida em exclusão mútua, faz-se:
mutex.WaitOneQ;
// Secção critica
mutex.ReieaseMutex();
- A classe Mutex evita que seja necessário sincronizar todas as íhreads que
utilizam um certo objecto, quando utilizam variáveis de instância não
interrelacionadas.

9.2.4 MONJTORES
Como vimos anteriormente, lock permite executar um bloco de código em exclusão
mútua. Ao fazer lock(obj), a sincronização acontece a nível do objecto referenciado por
obj. No entanto, os blocos lock permitem fazer mais do simplesmente executar em
exclusão mútua.

348 © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

Cada objecto criado a nível do CLR possui um elemento de sincronização chamado


monitor. Um monitor é uma estrutura que permite acesso apenas a uma thread de cada
vez. Ou seja, num dado momento, apenas uma thread pode estar a executar dentro do
monitor. Todas as threads que tentam entrar no monitor ficam bloqueadas, sendo
adicionadas a uma lista de espera. Quando a thread que está a executar sai do monitor,
uma das threads em espera é colocada em condições de executar. Esta thread irá, então,
tentar entrar no monitor. Caso não exista nenhuma outra thread a executar no seu interior,
então, adquire o monitor. Caso contrário, bloqueia novamente. É de referir que a thread
acordada tem de tentar adquirir o monitor novamente, uma vez que entre o momento em
que a thread anterior sai do monitor e o momento em que esta é colocada a executar,
pode existir uma outra thread a tentar entrar no monitor. O CLR tem de garantir que
apenas uma thread consegue entrar no monitor, ficando outra bloqueada. Logo, ambas
têm de tentar o acesso.

Na prática, o que a palavra-chave l ock faz é instruir a thread que a utiliza, a tentar entrar
no monitor em causa. O código do bloco l ock corresponde à execução dentro do
monitor:
: lock(obj)
í // , _ , , . ,monitor
.
T,hre.ad a executar em exclusão mu^|||d||g^^do

Na plataforma ,NET, não é possível obter uma referência para um objecto que represente
explicitamente o monitor de um objecto. No entanto, existe uma classe Monitor. Esta
classe apenas contém métodos estáticos, permitindo à thread corrente, realizar operações
sobre um certo monitor. Na verdade, ao executar um l ock, tal como no código anterior, o
que é executado pelo CLR é sensivelmente:
Monitor. E h t e r C o b j ) ; ~ ........ ~
,// Thrfead a executar em exclusão. mútua dentro do monitor

: Mpnitorvex\t(pbj)_;__________________ _ . _ ..... ...... _. .„ .....


Os métodos estáticos Monitor. EnterQ e Monitor. Exi t O permitem a uma thread, tentar
entrar dentro do monitor de um objecto, assim como sair desse monitor. A palavra-chave
l ock é apenas um auxílio sintáctico.

A razão porque estamos a referir tudo isto é devido a existirem duas operações muito
importantes, para além de entrar e sair de um monitor, que uma thread pode fazer quando
se encontra dentro do monitor. Essas operações são Walt Q e pui se C). Apenas a thread
que se encontra a executar dentro do monitor as pode invocar.

A operação waitQ faz com que a thread que se encontra no monitor bloqueie e,
simultaneamente, liberte o acesso ao monitor. Note-se que caso existam outras threads
bloqueadas à espera do monitor, nenhuma é acordada. No entanto, uma thread que tente
entrar no monitor, consegue adquiri-lo.
© FCA - Editora de Informática 349
C#3.5

Ao fazer pulseQ, a thread que se encontra dentro de um monitor faz com que uma das
threads que se encontre bloqueada no monitor, fique em condições de executar. Depois de
esta thread sair do monitor (a thread que faz pulseQ), a thread que foi colocada em
condições de executar irá tentar obter o monitor. Caso não consiga obter o monitor, volta
a bloquear. Existe uma outra operação semelhante a esta chamada PulseAll Q.
A diferença entre pulseQ e PulseAll C) é que esta última faz com que todas as threads
que se encontram bloqueadas no monitor fiquem em condições de executar. Isto é, de
competir para entrar dentro do mesmo.

Na prática, um monitor é implementado internamente com duas filas de espera. Uma das
filas contém as threads bloqueadas à espera de entrar no monitor. O CLR encarrega-se
automaticamente, de dar acesso a uma thread^ sempre que a que se encontra no monitor o
abandone. A outra fila identifica as threads bloqueadas devido a waltQ. Sempre que
existe um waitQ, a thread em causa bloqueia, é colocada nesta fila e liberta o monitor.
Sempre que existe um Pulse Q, uma das threads que se encontra na fila de WaitQ é
colocada na fila de entrada do monitor, ficando, então, bloqueada até lhe poder ter acesso.
PulseAll C) coloca todas as threads bloqueadas devido a waitO, na fila de threads à
espera de entrar no monitor. A figura 9.3 ilustra o processo.

Caso o leitor nunca tenha feito programação concorrente, tudo isto pode parecer muito
complexo. No entanto, na prática, e após algum hábito, toma-se relativamente fácil de
utilizar. Vamos, agora, ver um exemplo prático do tipo de estruturas que os monitores
permitem implementar.

Sempre que uma thread


abandona o monitore
dado acesso à primeira
thread desta fila

Fila de liueads bloqueadas á


espera de acesso ao monitor

MONITOR
Apenas uma thread pode
executar no seu Interior

Fila de Ihceads bloqueadas


devido a Wail() / \e que a tluead no

Sempre que a thread no monitor


monitor faz WaitfJ, é faz Pu1se(). uma das threads da
bloqueada, liberta o monitor e fila do WatlQ é colocada na fila de
é colocada na fila do WaitQ acesso ao monitor.

Figura 9.3 — Funcionamento de um monitor

9.2.4.1 EXEMPLjO: PF2ODLTTOR/CONSUMÍDOR COM BUFFER RNITO

Em programas com várias threads, muitas vezes, surge a necessidade de ter uma ou mais
threads a enviar dados para uma ou mais threads que os irão tratar. Dado que a
35O © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE

velocidade a que as threads que produzem dados (produtores) pode ser bastante diferente
da velocidade a que as threads que os tratam (consumidores) os conseguem processar,
existe um buffer entre ambas. Se uma thread produtor não dispuser de espaço no buffer
para colocar um bloco de informação, bloqueia até que haja espaço. Caso uma thread
consumidor não disponha de dados para processar (buffer vazio), bloqueia até que haja
dados produzidos. A figura 9.4 ilustra a ideia.

Figura 9.4 —Produtores/consumidores utilizando um buffer intermédio

Para implementar este esquema, iremos definir uma classe chamada


Buffersincronizado. Nesta classe existirão dois métodos. O método colocaQ é
chamado pelos produtores para colocar dados. Caso o buffer esteja cheio, o método
bloqueia até que seja possível colocar o dado. O método Retira Q é chamado pelos
consumidores para obter itens a processar.

Em Buffersincronizado, existirá uma tabela de objectos que representa os itens


guardados (itens), um campo que indica o número de posições livres (slotsLivres) e
um campo que indica o número de posições ocupadas (slotsocupados). É também
necessário unia variável pôs Lei tu rã, que indica qual a posição do próximo item a ser
retornado por Reti raQ , e uma variável pôs Es c ri ta, que indica qual a próxima posição
a ser ocupada.

O construtor cria a tabela e inicializa as várias variáveis:


class •BufferSincronizadõ<Tltem> ' "' " -.- *" ** il"
' . { . • • " ' -•* •'- : .— -i t •
: p ri vate TItemC] Itens; • • ; - . - ; •/*•
; private int SlotsLivres; - - . . ' :. i
! p ri vate int slotsocupados; 'í; l^'-' ;
ppivate -int PosLeitura; :
private int PosEscrita;
public BuffersincronizadojCint tamanho) _ . ;

© FCA - Editora de Informática 351


C#3.5

Itens = new Tltem[tamanho];


: SlotsLlvres = tamanho;
Slotsçcupados = 0;
PosLeltura = 0;
PosEscrlta = 0;
}. _ . . . . . .. . ..
Uma possível implementação de colocaQ é:
vofd Coloca(TÍtem dado)
// Acede ao monitor
í lock (this)
// Bloqueia até que exista um slot livre
whlle (SlotsLlvres == 0)
Monitor.waltCthls);
// Temos um slot livre e estamos em
// exclusão mútua. Coloca o elemento.
—SlotsLlvres; •
++slotsOcupados;
Itens[PosEscrita] - dado;
PosEscrlta = (PosEscr1ta-i-l)%ltens . Length;
: // Notifica que existe mais um Item disponível
Monitor.puiseAll(this);
}

Imaginemos que uma thread chama colocaQ. A primeira coisa que acontece é um
pedido de acesso ao monitor. Caso exista alguma thread a colocar ou a retirar um
elemento, a thread irá bloquear até poder aceder ao monitor. Suponhamos que a thread
entra no monitor. A thread, então, verifica se existem posições livres na tabela. Enquanto
não existirem posições livres, a thread bloqueia (operação waltQ). Caso isto aconteça,
irá ser uma thread consumidor que lhe irá fazer um PulseQ, quando houver uma posição
livre.

Um aspecto importantíssimo é que quando se faz este tipo de testes, na condição testada,
é sempre necessário colocar um w h l l e e não um 1f. Tal como dissemos quando
descrevemos pulseQ, quando esta operação é feita, uma thread bloqueada é colocada
em condições de executar. No entanto, a thread irá competir pelo acesso ao monitor.
Neste exemplo, caso haja o azar de uma outra thread conseguir realizar um colocaQ,
antes da ihread que estava bloqueada em wal t () conseguir adquirir novamente o monitor
e continuar a sua execução, então, a posição da tabela que estava livre já não o estará. Ou
seja, si otsLl vres estará a 0. O ciclo whl l e garante que a thread só consegue continuar a
execução, caso slotsLlvres seja realmente diferente de 0.

Após o ciclo whlle, é garantido que a thread tem o monitor, logo, está em exclusão mútua,
e que existe uma posição livre. A thread coloca, então, o seu elemento. O buffer
apresentado funciona em regime circulai*, o que implica que ao chegar à última posição da

352 © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

tabela, a próxima posição a escrever terá índice 0. Isso é conseguido com a operação resto
de divisão com o tamanho da tabela.

A última operação realizada pela thread é um PulseAll C). Isto garante que caso exista
alguma thread consumidor à espera de itens, esta irá verificar a existência de novos itens.
Uma questão bastante importante neste exemplo é que é feito um PulseAll Q e não um
pulseQ. Como só estamos a utilizar um monitor, caso fizéssemos um PulseQ simples,
então, poderia ser colocada em estado pronto a executar, uma thread que se encontrava
bloqueada no waitQ de colocaQ e não uma thread consumidor. Ou seja, poderia ser a
thread errada a ir verificar a condição. PulseAllQ garante que todas as threads que
estejam bloqueadas devido a um waltQ irão verificar a suas condições, inclusivamente
as threads produtor. No entanto, estas últimas irão bloquear novamente, caso, entretanto,
não tenham surgido posições livres no buffer. A condição wh i l e garante isso.

A implementação de Reti raQ é simétrica a Gol ocaQ:


publlc Tltem RetlraO
TItem dadoARetornar;
// Acede ao monitor
lock (this)
* // Bloqueia ate. que exista um _, dado
_, -
para retirar
whlle (Slotsocupados == 0)
Monltor.Walt(thls);
// Existe um dado para retornar e estamos
// em exclusão mútua. Retira o elemento.
++SlotsLlvres;
--SlotsOcupados;
dadoARetornar = Itens[posteitura];
PosLeitura = (posi_e1tura+l)%Itens . Length;
// Notifica que existe mais um slot livre
Monitor.PulseAll(this);
• }
// Retorna o objecto
return dadoARetornar;
> -- ...... .
Uma thread que chame Reti ra() irá bloquear até que existam dados disponíveis. Sempre
que alguém coloca um dado, todas as threads bloqueadas em Walt C) irão verificar se
existe algum, item para elas. Note-se que todas elas irão fazer isso, mas que apenas uma
delas está a executar de cada vez, dentro do monitor. A sua execução é em série. Caso
exista um item disponível, em exclusão mútua (dentro do monitor), a thread irá retirá-lo
do buffer e irá colocá-lo numa variável temporária. Então, a thread actualiza as variáveis
relevantes e notifica que houve uma alteração no estado do buffer, através de um
M o n i t o r . P u l s e A l l Q . Isto permitirá que quaisquer threads bloqueadas à espera em
col oca Q voltem a executar, utilizando a posição libertada. Finalmente, a thread liberta o
monitor e retorna o valor.

© FCA - Editora de Informática 353


C#3.5

A listagem 9.3 mostra um programa que faz uso da classe Buffersincronizado. Existe
apenas um produtor e um consumidor. O produtor envia 20 inteiros não negativos para o
consumidor. No final envia -l indicando que o consumidor deverá terminar.
O consumidor lê cada valor do buffer e envia-o para o ecrã. Para além disso, dorme
500 milissegundos antes de voltar a retirar um novo valor. Ao executar o programa, é
possível ver que o produtor consegue colocar os primeiros 5 valores imediatamente no
buffer (isto porque o tamanho do buffer é 5). No entanto, após estes primeiros valores
estarem no buffer, apenas será possível colocar um novo valor no buffer de 500 em
500 milissegundos, o tempo que o consumidor demora a retirar um valor do buffer.

/*
* produtor/consumidor usando um monitor.
*/
using System;
using System. Th reading;
class Buffersincronizado<Tltem>
{
private TItem[] Itens;
p ri vate int slotsLivres;
private int SlotsOcupados;
private int PosLeitura;
private int PosEscrita;
public BufferSincronizadoCint tamanho)
Itens = new TItem [tamanho] ;
SlotsLivres = tamanho;
SlotsOcupados = 0;
PosLeitura = 0;
PosEscrita = 0;

public void Coloca(Tltem dado)


// Acede ao monitor
lock (this)
{
// Bloqueia até que exista um slot livre
while (SlotsLivres == 0)
Monitor.Wait(this) ;
// Temos um slot livre e estamos em
// exclusão mútua. Coloca o elemento.
— SlotsLivres;
++sl otsocupados ;
Itens[PosEscrita] = dado;
PosEscrita = (PosEscríta + l) % itens. Length;
// Notifica que existe mais um item disponível
Monitor.PulseAll (this) ;

public TItem RetiraQ


TItem dadoARetornar;

354 © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

// Acede ao monitor
lock (this)
// Bloqueia até que exista um dado para retirar
while (slotsOcupados =- 0)
Monitor. Wait(this) ;
// Existe um dado para retornar e estamos
// em exclusão mútua. Retira o elemento.
++SlotsLivres;
— SlotsOcupados;
dadoARetornar = Itens [posLeitura] ;
PosLeitura = (PosLeitura + 1) % Itens. Length;
// Notifica que existe mais um slot livre
Monitor.PulseAll (this) ;

// Retorna o objecto
return dadoARetornar;

class produtor

p ri vate const int TOTAL_ENVIOS = 20;


private BufferSincronizado<int> Buffer;

public Produtor(BufferSincronizado<int> buffer)

this.Buffer = buffer;
}
public void RunQ
for (int i = 0; i < TOTAL_ENVIOS ; i++)

Buffer. Coloca(i) ;
console.WriteLine("produtor: Coloquei {0}", i) ;
}
Buffer.Coloca(-l) ;

Console. WriteLine("Produtor a terminar!");


}

class consumidor
private Buffersincronizado<int> Buffer;
public Consumi dor (Buf f erSincronizado<int> buffer)

this.Buffer = buffer;
}
public void RunQ
int recebido;
do _

© FCA - Editora de Informática 355


C#3.5

recebido = Buffer. Reti raQ ;


Consol e. w H tel_i ne("Consumi do r: Recebi {0} " , recebi do) ;
Thread.Sieep(500);
} while (recebido != -1);
Console.WriteLine("consumidor a terminar!");
}

class Exemp1ocap9_3
{
const int TAMANHO_BUFFER = 5;

static void Main(string[] args)


BufferSincronizado<int> buffer =
new BufferSincronizado<int>(TAMANHO_BUFFER);
Produtçr produtor = new Produtor(buffer);
Consumidor consumidor = new consumidorCbuffer);
(new Thread(new ThreadstartCprodutçr.Run))).StartC);
(new Thread(new Threadstart(consumidor.Run))).StartC);

Listagem 9.3 — Produtor/consumidor usando um monitor (ExemploCap9_3,cs)

Como o leitor deve notar, fazer programação concorrente, utilizando threads não é trivial,
sendo possível existirem erros muito subtis. Há que usar de muita precaução.
Relativamente ao uso de monitores, existem duas regras-de-dedo que se aplicam na maior
parte das situações. Primeiro: ao testar uma condição para bloquear uma thread num
monitor, o teste da condição deve ser feito com while e não com i f. Isto garante que a
condição é efectivamente verdadeira após a thread retornar de WaitQ. Caso contrário, a
thread é novamente bloqueada. A segunda regra-de-dedo é que, normalmente, deve
utilizar-se PulseAllC) e não pulseQ. Tipicamente, isto implica que todas as threads
testam se a sua condição já se verifica. Se as condições estiverem protegidas com while,
apenas as threads em condições de executar irão continuar o seu processamento.

- Um monitor representa uma secção de código que apenas é executada por


ARETER uma thread de cada vez. Diz-se que a thread a executar se encontra dentro
do monitor.
Monitores
- Um monitor possui duas filas de espera. Uma que representa a entrada do
monitor e outra que representa threads que chamaram o método
Monitor.WaitQ •
" Cada objecto possui implicitamente um monitor associado. Para uma
thread aceder ao monitor de um objecto, faz "lock(objecto) { ... }.
- Quando uma thread tenta entrar num monitor, duas coisas podem
acontecer:
a) não existem threads na lista de espera do monitor e a thread entra;
b) existem threads na fila de espera do monitor e a thread é
bloqueada, entrando para a fila de espera.
356 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE

Sempre que uma thread liberta o monitor, o CLR encarrega-se de verificar


ARETHR a fila de espera do monitor e, caso exista alguma thread à espera de lhe
aceder, concede acesso a uma delas.
Monitores
Uma thread que se encontre dentro de um monitor pode chamar
Monitor.WaitO , Monitor. PulseO e Monitor.PulseAll ().
Monitor.WaitO bloqueia a thread, liberta o monitor e coloca a thread na
lista de threads que chamaram WaitO • Estas operações ocorrem de forma
atómica,
Monitor.pulseO transfere uma thread, caso exista alguma, da lista de
threads que chamaram WaitO para a lista de threads à espera de entrar no
monitor,
O monitor não é libertado, continuando a thread que se encontra dentro do
mesmo a executar.
Monitor. pulseAll () transfere todas as threads que se encontrem na fila
de threads que chamaram WaitO, para a lista de threads à espera de
entrarem no monitor. O monitor não é libertado, continuando a thread que
se encontra dentro do mesmo, a executar.
Monitor.pulseO transfere uma thread, caso exista alguma, da lista de
threads que chamaram WaitO, para a lista de threads à espera de entrar no
monitor. O monitor não é libertado, continuando a thread. que se encontra
dentro do mesmo a executar.
• Monitor.pulseAll O transfere todas as threads que se encontrem na fila
de threads que chamaram WaitO, para a lista de threads à espera de
entrarem no monitor. O monitor não é libertado, continuando a thread que
se encontra dentro do mesmo, a executar.
• A operação de WaitO é tipicamente utilizada quando se pretende
suspender a execução da thread que dispõe do monitor até que se verifique
uma determinada condição. Esta condição é tipicamente colocada dentro de
um ciclo while:
lock (obj)
while ("condição)
Monitor.wait(obj);

' A s operações PulseO e pulseAllO são tipicamente utilizadas quando


existe uma alteração do estado do sistema, instruindo as threads
bloqueadas devido a WaitO, a voltarem a testar a sua condição. Isto
permite-lhes verificar se podem continuar a sua execução. A sintaxe é a
seguinte;
lock (obj)
// Alteração de estado (por exemplo: variáveis)

Monitor.puiseAll(obj);
>

© FCA - Editora de Informática 357


C#3.5

9.2.5 A CLASSE SEMAPHORE


No exemplo Produtor/Consumidor, vimos como é que, utilizando um monitor e um
pequeno conjunto de variáveis, é possível controlar num buffer circular. O que estava em
causa era: tendo um conjunto finito de recursos (total de slots num buffer\r que
fosse possível a várias threads, utilizá-los concorrentemente, enquanto existissem recursos
disponíveis (slots livres). A partir do momento em que não existiam mais recursos, as
threads que os tentassem utilizar teriam de bloquear, até que algum dos recursos ficasse
disponível. Este tipo de situação surge frequentemente, existindo uma construção que
permite facilmente programar este tipo de funcionalidade: o semáforo.

Um semáforo2 representa um conjunto de recursos que pode ser acedido concorrentemente


por várias threads. Um semáforo tem um valor associado, que é sempre maior ou igual a
zero, representando o número de recursos correntemente disponíveis. Os recursos podem
ser qualquer coisa: slots num buffer, impressoras, ligações a uma base de dados, etc. Um
semáforo possui ainda duas operações: espera e sinaliza. Sempre que uma thread
necessita de um recurso, realiza uma operação de espera. Se existirem recursos
disponíveis (i. e., o valor do semáforo é maior do que 0), então o valor do semáforo é
automaticamente decrementado e a thread continua a executar. Caso não existam
recursos disponíveis (o semáforo tem o valor 0), a thread bloqueia. A operação sinaliza
permite a uma thread, tipicamente diferente daquela que faz espera, indicar que um dos
recursos ficou livre. Sempre que sinaliza é chamado, o valor do semáforo é incrementado
e caso exista alguma thread bloqueada à espera de recursos, a mesma é desbloqueada.
Refira-se que caso existam várias threads bloqueadas, apenas uma é desbloqueada. A
thread que é desbloqueada irá então tentar novamente obter o semáforo (isto é,
decrementá-lo). Caso não exista nenhuma thread bloqueada quando o semáforo é
sinalizado, o valor do semáforo corresponde simplesmente ao incremento que foi feito.

Em .NET, os semáforos são representados pela classe semaphore. Semaphore possui um


construtor que permite criar um novo semáforo, indicando qual o número de recursos
actualmente disponíveis e qual o número máximo de recursos o mesmo suporta3. Por
exemplo:
Sèniaphòre Impressoras =
L.. Jiew.. SemaphpreCTOTAUJCMPRESSpRAS, .TpTALJCMPRESSORAS) ;

cria um semáforo que representa o conjunto de impressoras disponíveis numa sala de uma
reprografía, estando todas disponíveis. O primeiro parâmetro representa o valor inicial do
semáforo; o segundo, o valor máximo que poderá tomar (número de recursos).

Os semáforos são primitivas básicas existentes em todos os sistemas operativos modernos. Na verdade, os
monitores, assim como outros elementos de sincronização, são normalmente construídos à custa de
semáforos.
Esta forma de implementar semáforos difere radicalmente do que normalmente é encontrado nos sistemas
operativos. Tipicamente, os semáforos são criados indicando apenas o número de recursos actualmente
disponíveis. O número máximo de recursos associados é tipicamente "infinito".
358 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE

Suponhamos, agora, que existe uma rotina imprime C) que envia texto para uma das
impressoras disponíveis ou bloqueia até que possa imprimir, caso estejam todas ocupadas.
Esta rotina é executada concorrentemente por várias threads que tentam enviar os seus
textos. Uma implementação simplificada deste método será:
publlc void imprimeCstrlng texto)
// Espera que exista"uma impressora" livre
. impressoras.waitOneQ;

// --->>' IMPRIME <texto> NUMA IMPRESSORA LIVRE


//informa que existe uma impressora livre Ca que estava a"usar)
impressoras. Rei easeQ
.}~ """""" " ".' ' ~ ::".""
O método waitoneQ corresponde à operação espera, anteriormente discutida. ReleaseQ
corresponde à operação sinaliza. Assim, o que imprime Q faz é esperar que exista uma
impressora livre. Caso não exista, a thread bloqueia automaticamente. Quando existe uma
impressora livre, imprime o texto e, finalmente, informa que a impressora ficou livre. Isso
é feito, chamando ReleaseQ. Ou seja, esta última chamada irá libertar uma thread que
esteja eventualmente bloqueada em WaitoneQ, no início de Imprime C), à espera de uma
impressora livre.

Concentremo-nos, agora, no processo de impressão do texto. Imaginemos que existe uma


rotina Env1aParalmpressora(str1ng Texto, int i d ) , que envia um texto para uma
certa impressora física identificada por i d. Vamos assumir que esta rotina é thread-safe,
ou seja, pode ser executada concorrentemente por várias threads ao mesmo tempo. Ao
escrever a rotina imprime C), coloca-se o problema de saber qual o identificador de cada
impressora livre. Uma abordagem possível será guardar os identificadores das impressoras
livres numa lista de espera, impressorasLlvres, que será do tipo Queue<1nt>. Ou seja,
quando o sistema arranca, é corrido o seguinte código:
impressoras = new semãphoré(TOTAL_iMPRESSORAS 3 TOTÁL_IMPRESSORAS);
ImpressorasLlvres = new Queue<1nt>Q;
for (int 1=0; I<TOTAL_IMPRESSORAS; I-H-)
ImpressorasLlvres ._Enqueue(1) ; - .......
É criado um semáforo contendo um número de recursos correspondentes ao total de
impressoras existentes e é criada uma lista de identificadores de impressoras livres.
Inicialmente, todas as impressoras estão livres, pelo que a lista terá de conter todos os
identificadores necessários.

Assim, o código de imprime C) será:


public vbid-Imprime(strlng texto) . -:-••:-\
í "" • " " ' - • " • ' " '. ,, " '•-
! impressoras.wantoneO ; ;:-.,,
Int 1d.lmpressora;
loçk (ímpressorasLlvres)
idlmpresspra =_jEmpressorasLlyres.DequeueO^; - _ ;

© FCA - Editora de Informática 359


C#3.5

EnviaparalmpressoraCtexto, idimpressora);
Tock (impressorasLivres)
ImpressorasLivres.Enqueue(idlmpressora);
Impressoras . Rei easeQ ;
> _
Note-se que a obtenção de um identificador de impressora tem de ser feito em exclusão
mútua, pois podem existir diversas ihreads a executar ao mesmo tempo, após waitoneO-
Neste caso, o lock é feito sobre o objecto impressorasLivres. Após a impressão, é
também necessário libertar o identificador, tendo isso de ser feito em exclusão mútua.

A listagem 9.4 mostra o exemplo completo do uso de imprime Q. Este programa cria 5
threads, em que cada thread tenta imprimir simultaneamente um texto. Existem 3
impressoras disponíveis numa entidade Reprografia, para as quais os trabalhos são
enviados. Cada trabalho demora entre O e 15 segundos a ser impresso.

/*
* Exemplo de programação concorrente utilizando semáforos.
*/
using System;
using System.Threading;
using system.Collections.Generi c;
class Reprografia
private const int TOTAL_IMPRESSORAS = 3;
private Queue<int> impressorasLivres;
private Semaphore Impressoras;
private Random rnd;
/ftVf ir íVVí ft Vr A A fí ir Vr * A ft * íí * ií Aft* * íf -Í! A * * * * * * * ir ir * * * * * A /

public ReprografiaQ
impressoras = new Semaphore(TOTAL_iMPRESSORAS, TOTAL_IMPRESSORAS);
impressorasLivres = new Queue<int>O ;
for (int 1=0; 1<TOTAL_IMPRESSORAS; I-H-)
impressorasLivres.EnqueueCi);
rnd = new RandomC);

public void imprimeCstring texto)


Impressoras.WaitOneC);
int idimpressora;
lock ClmpressorasLivres)
idimpressora = impressorasLivres.DequeueC);
EnviaparalmpressoraCtexto, idimpressora) ;

36O . © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

lock (impressorasLlvres)
Impresso rãs Li vres.Enqueue(1dlmp ressona) ;
Impressoras . Rei easeQ ;

private void EnviaParalmpressora(str1ng texto, int i d)


Console. Wr1teLlne("[{0}] <impressora {!}> TEXTO: \"{2}\"",
DateTi me . Now , 1 d , texto) ;
Thread.SleepCrnd.NextQ % 15000);

}
class cliente
{
private Int Id;
private Reprografia Loja;
public Cl1ente(1nt 1d, Reprografia loja)
Id = I d ;
Loja = loja;

public void RunQ


for Çint 1=0; 1<3; i++)
Loja.lmpr1meC"o1á, texto do cliente: " -f Id) ;

}
class ExemploCap9_4
statlc void Main(string[] args)
Reprografia loja = new ReprograflaC) ;
for (int 1 = 0; 1<5; i++)
Cliente cliente = new Cl i ente (i, loja);
(new ThreadCnew ThreadStartCclIente.Run))) .startC) ;

Listagem 9.4 — Exemplo da utilização de semáforos (ExemploCap9—4.cs)

O resultado da execução deste programa é:


7/28/2005 15:24:131 <impréssorã 0> TEXTO: "Olá, cliente:
7/28/2005 15:24:13] <lmpressora 1> TEXTO: "Olá, cliente:
7/28/2005 15:24:13] <Impressora 2> TEXTO: "Olá, cliente:
7/28/2005 15:24:21] <impressora 1> TEXTO: "Olá, cliente:
7/28/2005 15:24:22] <Impressora 2> TEXTO: "olá, cliente:
7/28/2005 15:24:24] <Impressora 0> TEXTO: "olá, cliente:
[7/28/2005 15:24:28] <lmpressora 0> TEXTO: "Olá,, cliente:
[7/28/2005 15:24:29] <lmpressora 2> TEXTO: "Olá, texto do cliente:
C.,0

© FCA - Editora de Informática 36 !


C#3.5

É interessante observar que os três primeiros trabalhos começam a ser impressos


simultaneamente (15:24:13), pois existem três impressoras disponíveis. A partir daí, os
trabalhos vão sendo impressos, à medida que os trabalhos anteriores se completam.

Para concluir esta secção, apresentamos a classe Buffersincronlzado alterada,


utilizando, agora, semáforos e não monitores. Dado que BufferSincronizado tem um
conjunto de recursos finitos, cujo acesso tem de ser controlado, os semáforos contadores
representam uma forma muito natural de o fazer. Neste caso particular, irão ser
necessários dois semáforos: slotsLivres e slotscheios. slotsvazios é inicialmente
igual ao número de slots disponíveis; slotscheios é inicialmente zero. Sempre que
alguém tenta colocar um valor no buffer, um semáforo garante que a thread bloqueia até
existir um slot livre. Quando tal acontece, é introduzido o elemento, e incrementado o
valor de si otschei os:
ipiiblTc" voTd cõTócáCfítem dãdõj " "'
l SlotsLl vres .waitOneQ ;
lock (this)
: {
Itens [P9SEscn"ta] = dado;
PosEscrita = (posEscrita + l) % itens.Length;

Si otschei os. Rei easeQ ;


-} ..
Retirar um elemento é exactamente a operação oposta:
;pubTic~Títènf RêtiraQ
, Titem dadoARetornar;
Slotscheios.waitoneQ;
, lock (this)
{
dadoARetornar = Itens[posLeitura];
PosLeitura = (PosLeitura + 1) % Itens.Length;
SlotsLivres.ReleaseQ ;
return dadoARetornar;

Em co "l oca O, a operação slotscheios.ReleaseQ permite libertar eventuais threads


que estejam presas em RetiraQ, à espera de um dado para processar (i, e., em
Si otschei os. WaitOneQ). Por sua vez, em Reti raQ, slotsLivres. ReleaseQ permite
libertar eventuais threads que estejam bloqueadas em colocaQ, à espera de um slot
disponível. Finalmente, é de realçar que a manipulação do buffer tem de ser feita em
exclusão mútua, pois várias threads podem estar nesta zona de código simultaneamente.

362 © FCA - Editora de Informática


EXECUÇÃO CONCORRENTE

O código completo de Buffersincronizado passa a ser4:


class BufferSincronTzadb<tltêm>"~
private Titem[] Itens;
; private int PosLeitura;
private int PosEscrita;
private Semaphore SlotsLivres;
private Semaphore Slotscheios;
public Buffersincronizado(int tamanho)
Itens = new Tltem[tamanho];
PosLeitura = 0;
PosEscrita = 0;
Slotscheios = new SemaphoreíO, tamanho);
SlotsLivres = new Semaphore(tamanhOj tamanho);

public void ColocaCTltem dado)


SlotsLivres.WaitOneC) ;
lock (this)
Itens [PosEscrita] = dado;
PosEscrita = (PosEscrita + 1) % Itens. Length;
SlotsCheios.ReleaseC) ;

public Tltem Reti ra()


Tltem dadoARetornar;
SlotsCheios.WaitOneC) ;
lock Cthis)
dadoARetornar = ltens[posLeitura] ;
PosLeitura = (PosLeitura -t- 1) % Itens. Length;
Si ots Livres. Rei ease() ;
return dadoARetornar;

" Um semáforo é um objecto de sincronização que permite controlar o acesso


ARETER concorrente a um conjunto de recursos. Em .NET, os semáforos são
representados pela classe Semaphore.
Semáforos
~ Ao criar-se um semáforo, é necessário Indicar o seu valor inicial e o valor
máximo que o mesmo pode tomar. Por exemplo:
Semaphore impressoras=
new Semaphore (IMPRESSORAS_DISPONÍVEIS, TOTAL_IMPRESSORAS);
~ Um semáforo suporta duas operações principais: WaitoneQ e ReleaseQ.

4 O exemplo completo encontra-se no ficheiro ExernploCap9_5.cs.


© FCA - Editora de Informática 363
C#3.5

- waitoneQ verifica o valor do semáforo. Caso este seja positivo, decrementa


ARETER o seu valor e a execução da thread continua normalmente. Caso seja O, a
thread bloqueia ficando em lista de espera.
Semáforos
- A operação ReleaseO verifica o valor do semáforo e incrementa o seu
valor. Caso o valor inicial fosse O, se existir uma ou mais threads
bloqueadas, uma delas é desbloqueada. Essa thread irá tentar a operação
waitOneQ novamente. Caso o valor inicial seja positivo, a execução
procede normalmente.

9.2.6 OUTROS OBJECTOS DE SINCRONIZAÇÃO

Existem, ainda, outros objectos de sincronização relacionados com threads, na plataforma


.NET. Uma vez que são utilizados menos frequentemente, apenas faremos uma descrição
sumária de alguns deles. O leitor deverá consultar a documentação, para obter uma
descrição pormenorizada da sua utilização.

9.2.6. í CLASSESAuroRESETEvEfsrr E MANUALRESEJHVENT


Por vezes é necessário uma thread ou mais threads bloquearem até que um certo
acontecimento ocorra noutra thread. Ou seja, até que haja um evento numa outra thread5.
Quanto existe este tipo de necessidades, normalmente, utiliza-se a classe AutoResetEvent.
Diz-se que um evento se encontra sinalizado (signaled) se já ocorreu. Diz-se que um
evento se encontra não sinalizado (nonsignaled) se ainda não ocorreu.

Para criar um objecto do tipo AutoResetEvent, é necessário indicar o seu estado inicial.
true indica que o evento se encontra sinalizado, f ai se indica que ainda não ocorreu:
AutoResetEvent novosDádosDlsporijvens ="new AutoResetEvent(fãlse);
Uma thread que queira esperar que o evento seja sinalizado, apenas tem de chamar o
método waitOneQ:
novospadpsDlsponiveis^.WaltQneQ.j"" 7
A thread em causa irá bloquear. Uma thread que queira sinalizar o evento, apenas tem de
chamar set C):
novosDadosDl sponi yej s;. Set Q";"
Uma das threads que se encontra bloqueada em waitoneO é então desbloqueada, sendo o
evento colocado automaticamente no estado não sinalizado. Daí o nome AutoResetEvent.
Quando se coloca um evento como estando sinalizado, este irá permanecer sinalizado até
que uma thread faça WaitOneQ. Ou seja, o comportamento não é semelhante ao
Monitor, pui se (), em que pui s e C) apenas coloca uma thread em condições de executar.
Se não existir nenhuma, o pui se Q é perdido.

Note-se que estes eventos não estão relacionados com o mecanismo de eventos associado à programação
baseada em componentes.
364 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE

A diferença entre a classe AutoResetEvent e Manual ResetEvent é que no caso da


primeira, o evento é colocado automaticamente no estado não sinalizado, sendo apenas
uma thread libertada. No caso de Manual ResetEvent, o programador controla
explicitamente o estado do evento, tendo de colocar o evento, manualmente, no estado
sinalizado e não sinalizado. Manual ResetEvent funciona como uma barreira de threads.
Quando está sinalizado, a barreira encontra-se aberta e todas as threads que chamem
waitoneQ não bloqueiam. Quando está não sinalizado, a barreira encontra-se fechada e
todas as threads que chamem waitoneQ bloqueiam. setQ coloca um evento no estado
sinalizado, Rés et Q coloca o evento no estado não sinalizado.

9.2.6.2 CLASSETHREADPOOU
Embora as threads sejam muito úteis e possam levar a aumentos significativos de
desempenho, demoram algum tempo a criar. Em certas aplicações, como por exemplo
servidores de web ou bases de dados; normalmente, utiliza-se uma thread por cliente.
Para evitar andar a criar e a destruir threads, tipicamente, implementa-se um esquema em
que existe um conjunto de threads que é inicialmente criado e que ficam bloqueadas no
objecto de sincronização. Sempre que existe um cliente ou um pedido a tratar, é
desbloqueada uma das threads livres, sendo esta thread utilizada no processamento em
causa. Quando o processamento termina, a thread bloqueia novamente. A este tipo de
estruturas chama-se \rnis. pool de threads.

A classe Threadpool apenas dispõe de métodos estáticos e representa uma pool de


threads, fornecida pelo sistema operativo, à aplicação em causa. Note-se que não é
possível criar objectos desta classe criando diferentes pools de threads. Cada aplicação
apenas tem direito a uma destas pools.

O programador pode utilizar esta classe para escalonar trabalhos que devem ser
executados pela. pool Para adicionar um trabalho à. pool, utiliza-se o método estático:
Ta^^ cáljback) "" '
cal l back representa um delegote que encapsula o objecto e o método a executar, quando
existir uma thread livre para efectuar o processamento.

É de referir que a utilização desta classe não é suportada em todos os sistemas operativos,
sendo lançada uma excepção nos sistemas que não a suportam.

9.2.6.3 CLASSE READERWRTTERLjOCK

Um outro tipo de sincronização, que por vezes é necessário, corresponde à situação onde
existe uma ou mais threads a consultar um certo conjunto de dados (leitores) e, de tempos
a tempos, existe uma ou mais threads que actualizam esses dados (escritores). Enquanto
as threads estão a ler, todas as threads podem ler simultaneamente, não devendo estar a

© FCA - Editora de Informática 365


C#3.5 _

executar em exclusão mútua6. Quando existir uma thread a fazer uma actualização, esta
thread deverá executar em exclusão mútua. Não podem existir threaãs a fazer leituras,
nem outras threads a realizar actualizações. Este tipo de situações ocorre, por exemplo,
em bases de dados bancárias, em que várias sucursais consultam ao mesmo tempo o saldo
de um cliente e, de tempos a tempos, existe uma que actualiza o valor do saldo. Durante a
actualização, é necessário garantir atomicidade.

A classe ReaderwriterLock implementa directamente esta funcionalidade. Para criar um


objecto deste tipo, basta fazer:
^" " *1 _"
Todas as threads que queiram consultar um certo conjunto de dados, apenas têm de
identificar a secção de consulta com os métodos Acqui reReaderLockQ e
ReleaseReaderLockQ. Caso existam escritores a actualizar os dados, as threads que
chamam o método bloqueiam. Caso contrário, as threads possuem acesso para leitura de
dados, executando concorrentemente. No caso de AcqirireReaderLockQ, é possível
indicar o período máximo de tempo pelo qual se espera pela entrada na secção. O tempo é
especificado em milissegundos. Na maior parte das vezes, especifica-se um tempo
infinito (constante Ti meout. infinita). Assim, qualquer thread que queira fazer uma
leitura executa:
lacèsso.AquireReaderLockCTimeout.infiriite);

-// Todas as threads aqui dentro executam concorrentemente


,// fazendo a leitura dos dados - »•:--- ^ ,

acesso. Rei easeReaderLockQ ;

Quaisquer threads que queiram fazer escritas necessitam de utilizar os métodos


AcquireWriterLockQ e ReleaseWriterLockQ:
: acesso.Àqui reWriterLock(Timeout.Ínfihite) ; "
y/ Apenas uma thread executa aqui dentro, estando em exclusão mútua.
;// Esta thread faz a actualização dos dados :

•acesso. Rei easeWriterLockQ ;

Ao chamar Aqui rewriterLockQ, todos os novos leitores são bloqueados, ao chamarem,


AquireReaderLockQ- Quando o último leitor chama ReleaseReaderLockO, é então dado
acesso a um escritor, executando este ern exclusão mútua. Quando o escritor chama
Rei easewriterLockQ, pode, então, ser dada permissão para um novo escritor entrar ou,
então, a um ou mais leitores para executarem.

Se as threads apenas estão a ler, então, podem executar concorrentemente. Os problemas só acontecem se
existir uma thread a actualizar valores enquanto outras lêem.
366 © FCA - Editora de Informática
EXECUÇÃO CONCORRENTE

No caso de serem especificados intervalos de tempo máximos de espera, as propriedades


ReaderWriterLock.lsReaderLockHeld e ReaderWriterLock.IsWriterLockHeld
permitem testar se a thread conseguiu ou não adquirir o lock pretendido.

9.2.6.4 CLj\SSETlMER

A classe Tl me r não é propriamente uma classe de sincronização, mas sim uma classe que
permite chamar, periodicamente, um certo método num objecto. O construtor de Timer
mais utilizado possui o seguinte protótipo:
íTimeTCTi'merCallbãck^clJJ"b>íKZIP&"êct .estado^ int Jnjc1qL Int período) ;

callback representa um delegate que é chamado periodicamente. O delegate callback


representa um método que possui uma referência para um objecto corno parâmetro e que
não retorna nada:
:delegate void TimerCalJbli^ "_""_'[
Sempre que callback é chamado, é-lhe passado estado como parâmetro de entrada,
estado representa um objecto de configuração, definido pelo programador, que é
utilizado para parametrizar a chamada, estado pode ser n u l l . O objecto estado que é
passado na chamada do delegate é a referência que foi indicada no construtor do Timer.

O parâmetro i n i c i o indica, em milissegundos, quando é que o Timer criado deverá


começar a executar. O indica imediatamente, T i m e r . i n f i n i t e indica que objecto de
Timer é criado suspenso. Finalmente, o parâmetro período indica de quanto em quanto
tempo é que cal l back deve ser chamado.

Em qualquer altura, o programador pode chamar Timer.changeQ para alterar i n i c i o e


período.

Uma questão importante é que a thread utilizada em Timer é uma thread de background.
Ou seja, quando não existir nenhuma outra thread a executar no programa, o programa
termina.

O código seguinte mostra um programa em que, de segundo a segundo, é impresso o


número de segundos que já passaram, desde que o programa começou a executar. Este
programa executa até que algo seja lido do teclado. O console.ReadLineQ é necessário
para evitar que a thread principal do programa termine, terminando a execução do
programa de imediato.

/*
* Programa que ilustra o uso de Timer.
*/
using System;
using System.Threading;
class Mostrador

© FCA - Editora de Informática 367


C#3.5

// Número de segundos que decorreram desde que o


// objecto foi criado
private int segundos;
public MostradorQ

Segundos = 0;

// Método que é chamado de segundo a segundo


// estado não é utilizado
public void RunCobject estado)

Console.writeLine("Já passaram {0} segundos.", Segundos);


++Segundos;

class ExemploCap9_6
const int SEGUNDO = 1000; // l s = 1000 ms
public static void MainQ

Mostrador mostrador = new MostradorQ;


Timer t
= new TimerCnew TimerCallback(mostrador.Run), null, O,
SEGUNDO);

Console.ReadLineQ ;

Listagem 9.5 — Exemplo da utilização deTimer (ExemploCap9_6.cs)

~ As classes AutoResetEvent e Manual ResetEvent são utilizadas para notificar


diferentes threads que um certo acontecimento ocorreu, podendo estas
retomar a sua execução.
Outros Objectos
de Sincronização ~ A classe ThreadPool representa um conjunto de threads disponíveis para
realizar trabalho. As threads de ThreadPool encontram-se bloqueadas
enquanto não existe trabalho. Caso existam mais trabalhos do que threads,
os trabalhos são guardados numa fila e executados à medida que existam
threads disponíveis.
" A classe ReaderWriterLock permite sincronizar acessos entre threads leitor
e threads escritor que acedem a uma certa informação. Quando não existem
escritores, os leitores executam simultaneamente, lendo os dados em causa.
Sempre que surge um escritor, este bloqueia até que não existam leitores a
ler. Então, actualiza os dados. Caso exista mais do que um escritor, cada um
deles executa em exclusão mútua.
" A classe Timer permite executar um método de um objecto, de forma
periódica.

368 © FCA - Editora de Informática


l o - ACESSO À INTERNET
Neste capítulo, iremos explorar algumas das classes existentes na plataforma .NET que
permitem a um programa, aceder a uma rede de dados. Em particular, iremos examinar as
funcionalidades disponíveis para aceder à Internet. Iremos explorar de que forma é
possível carregar ficheiros HTML presentes em servidores web, como é que se escrevem
web sei-vices e de que forma é que se utiliza o protocolo TCP/IP.

No entanto, todos os tópicos mencionados constituem apenas uma pequena parte dos
serviços de acesso à rede, disponíveis na plataforma .NET. Em particular, a arquitectura
para partilha de objectos remotos entre aplicações, chamada Remoting, não será coberta
neste livro. Para os programadores de Java, o Remoting é sensivelmente o equivalente ao
RMI (Remote Method Invocatioii). Dada a sua extensão, não é possível abordar a sua
utilização, num capítulo de acesso à rede.

As classes de acesso à rede existem principalmente em dois espaços de nomes:


System.Net e System.Net.sockets. No primeiro, existem as classes relacionadas com
protocolos de alto nível e operações do ponto de vista de um cliente. Por exemplo,
carregamento e envio de ficheiros a partir da rede, pedidos HTTP a servidores web,
resolução de nomes de máquinas e outros. E um espaço de nomes geral. Em
System.Net.sockets, encontram-se as classes relacionadas com a utilização do protocolo
TCP/IP.

l O. l ACESSO A RECURSOS NA INTERNET


10.1.1 CLASSE WEBCLJENT
O acesso a servidores web é extremamente simples de conseguir. Basta utilizar a classe
System.Net.webclient. Por exemplo, suponhamos que se quer fazer o download da
página web em http://www.dsi.uc.pt/, guardando-a num ficheiro loca] chamado índex.
.html. Para isso, basta fazer:
'Webçlient çliepte =r i e w'Webcl i ent O ;'" " - ' . ' •
.çVtente/pownl.q_adFileL^ 5 ndex_.htm]");
No entanto, normalmente, as aplicações irão processar os dados recolhidos de uma página
web e não guardá-la directamente num ficheiro. O método openReadQ desta classe
permite abrir uma streain para um certo recurso na web:

© FCA - Editora de Informática 369


C#3.5

webclient cliente = new webcTientQ;


Stream strcliente = cliente.OpenRead("http://www.dei .uc.pt") ;
Usando esta stream, é então possível fazer o processamento desejado. A listagem 10.1
mostra um pequeno programa que carrega uma página web e mostra o seu conteúdo na
consola.

* programa que carrega uma página web e a mostra na consola.


*/
using System;
using System.Net;
using System.IO;
class ExemplocaplO_l
static void Main(string[] args)
const string WEB_SITE = "http://www.dei.uc.pt/";
try
Webclient cliente = new WebdientQ;
Stream strcliente = cliente.OpenRead(WEB_siTE);
StreamReader pagina = new streamReader(strdiente) ;
string linha = null;
do
{
l i n h a = pagina.ReadLineQ ;
i f (linha != null)
Console.writeLine(linha);
} while (linha != null);
strcliente.Close();
}
catch (Exception e)
Console.writeLine("Houve um problema ao ler {0}", WEB_SITE);
Console.writeLine(e.Message);

Listagem 10.1 — Programa que carrega uma pagina web e. a mostra na consola
(ExemploCaplO__l.cs)

Note-se que é utilizado um bloco try~catch para proteger o caso de haver uma excepção.
Existem várias razões para a leitura falhar: o servidor pode não ser conhecido, a página
pode não estar disponível, o utilizador pode não ter permissões para ler a página, a rede

© FCA - Editara de Informática


ACESSO À INTERNET

pode falhar, entre outras. É sempre necessário proteger este tipo de código por um bloco
try-catch 1 .

Assim como é possível receber informação de um servidor web, também é possível enviar
informação para um servidor. Para enviar um ficheiro local para um servidor web,
utiliza-se o método UploadFileQ:
const string FICHEIRO_LOCAL = "index.html";
Webclient cliente = new WebdlentÇ);
cl i ente. Upl oadFi l eCJrttp: //wyvw. dei . u c.pt/i ndex. html", _ FlCHElRp_LOCAtO ;

Claro que para esta chamada funcionar, o servidor web tem de permitir realizar este tipo
de operações. O método uploadoataQ funciona de forma idêntica, mas com a diferença
de que a informação a enviar para a rede advêm de uma tabela de bytes especificada no
programa. Por exemplo:
byte[] imagem;
'// código que gera uma imagem na tabela imagem.

Webclient cliente = new WebclientQ;


cli ente.UploadData("http://www.dei.uc.pt/home.j pg", i magem);

Até agora, temos estado a utilizar a classe Webclient apenas para aceder a servidores
web. Isto é, servidores que suportam o protocolo HTTP, o que é indicado na localização
do recurso a utilizar, isto é, no Unifonn Resource Jdentifier - URI. No entanto, a classe
Webclient foi criada por forma a ser genérica e aceder a recursos de rede diversos,
identificados por diferentes URI. Por exemplo, servidores de FTP ou simples acesso a
ficheiros. Actualmente, esta classe apenas suporta URI começados por http, https e.file.

A tabela seguinte apresenta os principais elementos associados à classe webcl i ent.


MÉTODO/PROPRIEDADE DESCRIÇÃO
Webclient() Construtor sem parâmetros.
Propriedade que representa o URI base utilizado para
construir os endereços utilizados pela instância da classe.
string Caso seja feito o set desta propriedade, o parâmetro
BaseAddress { get; set; }
address, indicado nas rotinas abaixo, representa um
endereço relativo.
ICredentials Propriedade que representa as credenciais de segurança
Credentials {get; set;} utilizadas no acesso ao recurso de rede.
webHeaderCol 1 ecti on Propriedade que representa os cabeçalhos utilizados no
Headers { get; set; } pedido de acesso ao recurso de rede.
Nameval ueCol 1 ecti on Propriedade que representa pares <nome/ valor>, utilizados
QueryString { get; set; } no acesso ao recurso de rede.

1 Devemos referir que o código deste exemplo não é totalmente robusto. Caso exista uma excepção, a stream
não está a ser fechada. A fim de não complicar o exemplo, o tratamento de erros foi ligeiramente
simplificado, especialmente, porque o método Cl ose C) também pode lançar excepções.
© FCA - Editora de Informática 371
C#3.5

MÉTODO/PROPRIEDADE DESCRIÇÃO

WebHeaderCol 1 ecti on Obtém os cabeçalhos da resposta ao pedido feito ao recurso •


ResponseHeaders { get; } de rede. :
byte[] : Obtém informação de um certo recurso de rede identificado t
DownloadoataCstring address); por address.
void Obtém informação de um recurso de rede identificado por
Down1oadFile(string address,
string fileName) ; address para um ficheiro locai (fileName).
Streain Abre uma stream para leitura; associada ao recurso de rede
openRead(string address); identificado em address.
Stream Abre uma stream para escrita, associada ao recurso de rede '
Openwrite(string address); identificado em address.
Stream Idêntico ao método anterior, mas indicando um método de
OpenWrite(string address,
string method) ; . acesso (exemplo; POST).
byte[] Envia informação binária para o recurso de rede identificado
up"loadData(string address, em address. Retorna a resposta enviada pelo recurso de
byteCJ data); rede.
byte[]
Up1oadData(stn'ng address, Idêntico ao método anterior, mas indicando um método de
string method acesso (exemplo: POST).
byte[J data);
Envia um ficheiro para o recurso de rede identificado em
Up"loadFile(string address,
string fileName); address. Retorna a resposta enviada pelo recurso de rede.
byte[]
uploadFi lê (string address, Idêntico ao método anterior, mas indicando um método de
string method, acesso (exemplo: POST).
string fileName);

Uploadvalues (string address, : Envia um conjunto de pares <nome/ va[or> para o recurso
NameValueCoTlection data) ; : de rede identificado em address.

uploadvalues (string address, Idêntico ao método anterior, mas indicando um método de


string method, acesso (exemplo: POST).
NamevalueCoIlection data);

Tabela 10.1 — Principais elementos da classe WebCl i ent

Vamos ver apenas mais um exemplo de utilização desta classe. Imaginemos que
queremos fazer uma pesquisa no motor de procura Google (http://www.google.com'),
procurando a palavra "Computador". Para isso, é necessário fazer um pedido GET2,
usando o URI http://www.google.com/searcii e indicando que o campo "q" (de query)
deverá ser "Computador". Em termos do protocolo http, isso corresponde a fazer GET
utilizando a cadeira de caracteres: c'http://www.google.com/search?q=Computador". Na
prática, o programador não tem de codificar esta cadeira de caracteres manualmente.
Basta adicionar à propriedade Querystring os campos a enviar. No código seguinte, é

Não iremos entrar em detalhes sobre as formas como são feitos os pedidos aos servidores web. Apenas
importa saber que os principais pedidos são GET, que permite obter informação do servidor, e POST, que
permite enviar informação para o servidor. Quando se faz úrn GET, a informação a obter é codificada
directamente no URI,
372 © FCA - Editora de Informática
ACESSO À INTERNET

pesquisada a palavra "Computador" no Google, sendo o resultado guardado no ficheiro


"resultado.html":
Webcllent cliente = hew"WèbcTféntQ; " f~
cliente.Quepystr1nq.AddC"q", "computador"); '; •
dl ente.Downl qadfl l eC'http ://w\j/w. googl e. cpm/seafèéh" , " resultado. html "3 ;

ARETER " Para aceder a um recurso na Internet, utiliza-se a classe Webcl 1 ent.
" Os métodos Webcllent.DownloadFlleQ e WebClient.UploadFlleO
Acesso a
permitem obter e colocar ficheiros num servidor.
recursos " Os métodos Webdlent.OpenReadO e Webdlent.openWriteO permitem
Standardrda , obter streams para leitura e escrita num servidor.
Internet l
" Sempre que se acede a um recurso na rede, é necessário proteger as chamadas
com um bloco try-catch.

l O. l .2 CLASSES WEBREQUEST E WEBRESPONSE


Apesar de a classe Webcllent ser bastante útil e fácil de utilizar, por vezes é necessário um
maior controlo sobre a forma como são feitos os pedidos e obtidas as respostas dos servidores.
Como webcl 1 ent é genérica, podendo suportar qualquer protocolo, não estão implementadas,
em Webcllent, funcionalidades específicas de nenhum protocolo. Toda a funcionalidade é
abstraída em termos de operações realizáveis com URI. Caso o programador necessite de
maior controlo sobre a utilização dos protocolos de comunicação, então deverá utilizar as
classes WebRequest e WebResponse.

Vejamos como é que, usando estas classes, se pode obter uma página web e mostrá-la na
consola. Para criar um novo pedido, utiliza-se o método estático WebRequest.createQ
este método devolve uma instância de WebRequest específica do protocolo em causa.
O código seguinte obtém a página em http://www.dei.uc.pf:
WebRequest pedido = WebReciuèst.CreàteC"http:/7www'.de1 ~.uc.pt/"D; :
WebResponse resposta = pedldo.GetResponseQ;

StreamReader dados = new streamReader(resposta.GetResponsestream'CO;


strlng linha;
do
{
linha =* ^ados.ReadLlneQ; !
i f Çlinjha != null) f , !
consoje:Wr1teLine(11 nhã); " - "" •* '
} whlle Cllnha .!= nu"! "O; .. - . -

dados..cToseO ].- _ . . - . . . . . . _ . _ . . _ _ . . ' - ' ' ;


A razão porque é utilizado um método estático para obter uma instância de WebRequest
prende-se com flexibilidade. O que é devolvido de createQ não é verdadeiramente uma

3 Para maior simplicidade, o tratamento de excepções é omitido


© FCA - Editora de Informática 373
C#3.5 =^-3^=^^==:^=^===^=^^^=^^^-

instância de webRequest, mas sim de uma classe derivada desta. Essa classe representa a
implementação do protocolo específico em causa, sendo possível ao programador, se
necessário, fazer uma conversão explícita para a classe derivada. Por exemplo, ao ser
invocado WebRequest.create("http://www.dei .uc.pt/"), é devolvida uma instância
de HttpWebRequest 4 . Ao chamar createQ, este método examina a natureza do pedicjo
que lhe é passado, criando uma instância da classe concreta apropriada.

Caso o programador necessite de aceder a informação específica do protocolo HTTP, ou


outro protocolo de rede implementado na plataforma .NET, então, deverá utilizar as
classes WebRequest e WebResponse em vez de webcl 1 ent.

~ 10.1.3 CLASSES UTILITÁRIAS


Nesta subsecção, iremos falar de algumas classes e métodos úteis quando se fazem
acessos à rede. Nomeadamente, iremos ver como é que se configura um proxy, como é
que se obtém o endereço IP de uma máquina a partir do nome de uma máquina e de que
forma é que se manipula URI.

10.1.3.1 CONRGURAÇÃO DE PROXIES

Hoje em dia, tipicamente, um computador não faz o carregamento directo das páginas
web de um servidor. É cada vez mais frequente a existência de uma máquina intermédia,
chamada proxy, que é consultada antes de os pedidos serem feitos ao servidor web final.
O que um proxy faz é realizar os pedidos em nome dos clientes, guardando também as
páginas web resultantes. Caso o proxy receba um pedido para o qual já tenha armazenado
a página web em questão, então, em vez de redireccionar o pedido para o servidor web
real, pode responder imediatamente com a página web que tinha armazenado. Como um
proxy é utilizado por diversas máquinas, isto leva a um aumento significativo de
desempenho, uma vez que aceder a um proxy é tipicamente muito mais rápido do que
aceder a um servidor que se encontra em parte incerta na Internet.

Ao utilizarmos as classes da plataforma .NET que fazem pedidos à rede, caso não exista
nenhum servidor de proxy configurado, os pedidos são feitos directamente aos servidores
finais5. No entanto, é muito fácil configurar um proxy para ser utilizado para todos os pedidos
efectuados. Para isso, basta configurar a propriedade estática GlobalProxySelection.
. select. Por exemplo, suponhamos que, no início de um programa, queremos configurar
o servidor de proxy para http://proxy.dei.uc.pt:8080/t antes da classe webcl i ent ser
usada. Para isso, basta fazer:
[77~ Confi guração do proxy
IwebProxv proxv = new WebProxvC"http://proxv.dei.uc.pt:8Q80/"):

Para os feitores familiarizados com o conceito de design patterns, WebRequest é un\'àfactoiy.


5 Note-se que caso exista um proxy configurado globalmente para a máquina, por omissão, o proxy
configurado é utilizado pelas classes da plataforma .NET.
374 © FCA - Editora de Informática
ACESSO À INTERNET

BT1 pba l Proxyse i ectl on. s_ej eçt = .proxy;.

// Todos os cedidos passam a ser feitos via proxy.


Webcllent cliente = new WebclIentQ;
.cl 1 ente.Uploadpil e("http://www,_de1. uc. pt/1 ndex. html", "1 ndex.html ") ;
A classe WebProxy permite encapsular um determinado proxy. A forma mais simples de a
utilizar é criando uma instância em que no construtor é especificado o URI do proxy a
que se refere. Ao fazer Global ProxySelection.sei ect = proxy;, é configurado qual o
proxy a utilizar para todos os pedidos que serão feitos futuramente.

Caso o programador não queira configurar um proxy para ser utilizado globalmente, mas
apenas um proxy para ser utilizado num pedido, tipicamente, pode utilizar a propriedade
proxy da classe correspondente. Por exemplo, para fazer uni pedido via. proxy, utilizando
a classe webRequest, basta fazer:
'WebProxy proxy = new webproxyC"httpV//proxy.de1 ;uc.pt:8"0"80/") ;
WebRequest pedido = WebRequest.CreateC http://www.google.com/");
pedido.Proxy = proxy;

IO. 1.3.2 RESOLUÇÃO DE ENDEREÇOS

Na Internet, cada máquina tem um nome e um endereço associado. O nome é uma cadeia
de caracteres que representa a máquina em questão. Por exemplo, ivww.fco.pt é o nome
de uma máquina. O endereço da máquina representa a sua morada na rede, sendo o
mesmo utilizado para conseguir fazer chegar informação à mesma. Um endereço é um
número de 32 bits, sendo tipicamente representado como quatro grupos de dígitos, cada
grupo de dígitos podendo tomar valores de O a 255. Por exemplo, 195.22.2.66 representa
um endereço de uma máquina. A estes endereços chamam-se endereços IP. A
representação de um endereço IP em quatro grupos de dígitos denomina-se por endereço
na forma decimal.

Na Internet, existem servidores especiais, chamados Domam Name Servers (DNS), que
se encarregam de manter as tabelas com as correspondências entre nomes e endereços. Na
prática, sempre que é feito um pedido a um servidor, por exemplo, a http://www.fca.pt, é
necessário converter o nome do servidor num endereço. Só depois, pode o pedido ser feito
realmente ao servidor. Um DNS funciona como uma lista telefónica onde se pode
consultar o endereço de uma pessoa a partir do seu nome. Só dessa forma é possível fazer
chegar correspondência à pessoa.

Caso seja necessário manipular explicitamente nomes e endereços de máquinas, o


programador pode utilizar as classes iPAddress, ipHostEntry e Dns.

A classe IPAddress representa um endereço IP em particular. O endereço em si encontra-se


disponível na propriedade Address, que é do tipo "long. Ao chamar TostringQ, é
mostrado o endereço na forma usual, agrupado em quatro grupos de dígitos. É ainda

© FCA - Editora de Informática 375


C#3.5

possível converter uma cadeira de caracteres num endereço utilizando, o método estático
parseQ. O extracto de código seguinte ilustra o uso desta classe:
string endereço = "195.22.2 . 66" ;"" " "
: try

iPÃcfdrèssíp"""= ÍPÃdclress."pàrsêCenclerêco) ; ""// representação interna


long valorlp = ip.Address; // 3272999490
stnng textual = ip.TpStringQj // "195.22.2.66"
} " - - - - - - - - -
catch (Exception e)
console.Writei_ine("{0} não representa um endereço válido", endereço);
l . . . . . . ... . . . . . .
A classe iPHostEntry representa toda a informação associada a uma máquina. O nome
da máquina encontra-se disponível na propriedade HostName (uma string), e todos os
endereços IP associados à máquina encontram-se na propriedade AddressList. Esta
propriedade é vista como uma tabela de iPAddress. A propriedade Al i ases corresponde
a uma tabela de strings, representando outros nomes pelos quais a máquina é conhecida.
Esta classe é usada em conjunto com Dns, que veremos de seguida.

A classe Dns é utilizada para fazer pedidos de conversão nome/endereço ao DNS


configurado para a máquina onde o programa é corrido. Esta classe apenas dispõe de
métodos estáticos. O método GetHostByNameQ permite obter toda a informação
associada a uma máquina, utilizando o seu nome como chave de procura.
GetHostBVAddressQ permite obter a informação associada a uma máquina, utilizando
como chave de procura, um dos endereços IP da máquina. O método GetHostName()
permite obter o nome da máquina local. Finalmente, o método Resol vê C) permite obter a
informação associada a uma máquina, utilizando como chave de procura uma string, que
tanto pode ser o nome da máquina, como o seu endereço na forma decimal.

O programa seguinte recebe, na linha de comandos, o nome, ou endereço, de uma máquina,


apresentando toda a informação associada à mesma.

/*
* Programa que ilustra a utilização de um DNS.
*/
using System;
using System.Net;
class ExemploCaplO_2
static void Main(string[] args)
i f (args.Length != 1)
console.Writel_ine("Argumentos inválidos. Indique o nome " +
"da máquina.");
return;
string maquina = args[0];

376 © FCA - Editara de Informática


ACESSO À INTERNET

try
// obtém informação sobre a máquina
iPHostEntry info = Dns.Resolve(maquina);
Console.WriteLine("Nome principal:");
Console.Writeí_ine("\ {0}", info.HostName);
// Endereços da máquina
Console.writel_ine( Endereços da máquina:");
IPAddress[] endereços = info.Addresstist;
foreach (iPAddress endereço i n endereços)
Console.WriteLine("\ {0}", endereço);

// outros nomes da máquina


Console.WriteLine("outros nomes da máquina:");
string[] outrosNomes = info.Aliases;
foreach (string nome i n outrosNomes)
Console.Writel_ine("\ {0}", nome);

catch (Exception e)
{
Console.WriteLine(e);

Listagem 10.2— Programa que carrega uma pagina webe. a mostra na consola
(ExemploCaplO_2.cs)

Ao correr este programa, utilizando o nome wmv.yahoo.com, surge:


Nome principal:
www.yahoo.akadns.net
Endereços da máquina:
64.58.76.179
64.58.76.176
64.58.76.228
! 64.58.76.178
: 64.58.76.222
64.58.76.227
: 64.58.76.225
í 64.58.76.229
f 64.58.76.177
64.58.76.223
64.58.76.224
:outros nomes da máquina:
www.yahoo_, com.
É igualmente possível correr o programa sobre um endereço, como 64.58.76.179.

10.1.3.3 MANIPULAÇÃO DE URI


A classe uri encapsula ura URI, dispondo de diversos métodos que permitem extrair os
diversos componentes de um URI, de forma automática. A classe U r i BUÍ l der permite
criar novos Uri de forma simples, sem conhecer muitos dos pormenores utilizados na
codificação de um URI.

© FCA - Editora de Informática 377


C#3.5

Por exemplo, anteriormente, vimos que para fazer uma pesquisa no motor Google pela
palavra "computador", é necessário haver um URI com a forma:

http:/Avww.google.com/search?q~computador

Para criar um Uri com este endereço, que pode ser utilizado em classes como Webcl i ent,
basta fazer:
'Uri questão '-' new Uri CMnttpT//www. gooçfl é". còm/search?q=computádqrl_l) ',_ ,
Utilizando o objecto questão, é possível consultar partes do URI. Por exemplo, ao fazer:
!Uri
questão = new UriC n http://www.google.com/search?q=cõmputádòr");
Console.writeLine("{0}\n", questão.AbsoluteUri);

idonsole.writeLineCAbsolutePath: \ {0}" questão.AbsolutePath)


Console.WriteLine("AbsoluteUri: \ {0}" questão.AbsoluteUri);
;console.writei_ine("Host: \ {0}" questão.Host);
. ! Console.writeLine("port: \ {0}" questão.Port);
Console.WriteLine("Query: \ {0}" questão.Query);
:Console.writeLine("scheme: \ {0}" questão.Scheme);
surge:
:httpr//www.google.còm/search?q=computador

; Absolutepath: /search
AbsoluteUri: http://www.google.com/search?q=computador
•Host: www.google.com
Port: 80 \:

! Scheme: http _ __ _ i
Um ponto importante é que, ao criar um novo Uri, por omissão, é feita a codificação
necessária dos campos presentes6. Por exemplo, se existirem acentos, vírgulas, espaços,
ou similares, estes são automaticamente codificados de acordo com as especificações do
W3C. A propriedade AbsoluteUri mostra o Uri tal como ele é, incluindo os caracteres já
codificados. O método ToStríngO mostra o Uri numa forma perceptível, em muitos
casos, sem incluir esta codificação.

Um aspecto importante de uri é que os seus campos, após este ter sido criado, são apenas
de leitura. Caso seja necessário manipular os campos de um URI ou especificá-los
parcialmente, então, utiliza-se a classe uri BUÍ l der. Por exemplo, vejamos a construção
de um URI em que a expressão a pesquisar no Google se encontra numa string7
questaoDoUtil i zador:
77 String com" a questão' dó"utilizador"" """
string questaoooutilizador - "música clássica";

6 Existem construtores com um parâmetro que indica se deve ser feita a codificação ou não.
7 Note-se que não é necessário especificar todos os componentes do URI utilizando propriedades. É possível
especificar o URI completo, ou quase completo, como uma string no construtor. Não o fazemos aqui a fim
de mostrar algumas propriedades de Uri Bui! der.
378 © FCA - Editora de Informática
ACESSO À INTERNET

l/ constrói a questão" "" " ~


UriBuilder questão = new UriBuilderQ;

;questaò.scheme = "http";
.questão.Rost = "www.gooqle.com";
questão.path = "/search ;
jquestao.Query = questaoDoUtilizador;

;// obt.em Jo URI


júri quésHaóUrl •= questão.Uri;
;Conso.1.e.W.ríteLineC"Un' a usar na pesquisa:\ {0}",
: _ questapy ruAbsol utey r[)j

Para obter a instância de u n' final resultante da construção, é utilizada a propriedade


apenas de leitura u r i . Ao executar este código, surge:
'Uri a usar ha'pesquTsáT
'• http: //www. gopgl e... com/s.earçh?m%c3%BAsi ca%20cl%c3%Alssj ca •. ;
Como se pode observar, o uri encontra-se com os acentos e os espaços correctamente
codificados. No entanto, ao fazer console.writeLine(questaoUri) ;, surge:
•http://www..go6;gle.cpj^^ ...,__._" .." 7
Ou seja, o u r i , mas sem mostrar a codificação.

" Para configurar globalmente o servidor proxy utilizado em pedidos feitos à


Internet, cria-se uma instância de webproxy, com o proxy a usar, e altera-se
a propriedade Gl abai Proxysel ecti on. Sei ect. Exemplo:
Classes Webproxy proxy =
utilitárias para o new webproxy("http://proxy.dei.uc.pt:8080/") ;
acesso à GlobalProxySelection.Select = proxy;
Internet
" A classe iPAddress representa um endereço de uma máquina na Internet e
iPHostEntry contém toda a informação associada a uma máquina,
incluindo todos os seus endereços e nomes. A classe Dns permite consultar
um servidor de Dns para realizar a resolução de endereços. Para fazer uma
resolução de nomes, utiliza-se, principalmente, o método Dns. Resolve C).
Por exemplo:
IPHostEntry info = Dns.Resolve("www.dei.uc.pt")3
" A classe Uri permite, de forma simples, manipular identificadores de
recursos na Internet. Entre outras coisas, esta classe permite fazer, de forma
automática, a codificação das stríngs utilizadas para manipular os recursos.

TO.2 WEB SERVICES


O conceito de web service é simples de entender. Tal como o nome indica, um web
sei-vice representa um serviço disponível na web, acessível através de protocolos Standard
da Internet. Para invocar um web sei-vice, tipicamente, é utilizado HTTP como protocolo
de transporte e SOAP (Simple Object Access Protocol} como protocolo de invocação8.

Embora não seja frequentemente referido, é possível utilizar outros protocolos de transporte e invocação,
apesar destes serem maioritariamente baseados em HTTP.
© FCA - Editora de Informática 379
C#3.5

Uma das grandes vantagens dos web services é permitirem a um servidor na Internet,
publicar um conjunto de funções (serviço), sendo possível aceder a estes, utilizando
protocolos bem estabelecidos e que já são omnipresentes na Internet. Isto também permite
uma maior interoperabilidade entre sistemas existentes.

Ao utilizar o protocolo S O AP, as mensagens que são trocadas entre um cliente e um


servidor são encapsuladas em XML, permitindo que, independentemente das tecnologias
utilizadas no cliente e no servidor, ambos se consigam entender. Do ponto de vista do
programador, este apenas necessita de invocar métodos, num objecto que representa o
serviço no servidor. O ambiente de execução encarrega-se de encapsular a invocação num
envelope SOAP e de utilizar o protocolo HTTP para enviar a invocação, para o servidor.
O servidor, por sua vez, encarrega-se de transmitir esta invocação ao objecto que
representa o serviço ai existente e de enviar a resposta, novamente num envelope SOAP,
para o cliente, Novamente, o ambiente de execução encarrega-se de desencapsular a
resposta e de a retornar à thread que fez a invocação.

A grande diferença entre a utilização de web services e tecnologias como o CORBA, Java
RMI e DCOM é que, no caso dos web services, estes assentam numa estruturajá existente
na Internet: servidores web e XML. No entanto, apesar dos web services assentarem em
SOAP e XML, essa utilização é invisível para o programador, que apenas vê classes e
métodos.

Em termos tecnológicos, existe, ainda, uma peça importante dos web services: a WSDL
(Web Service Descríption Languagé). Para um programador conseguir utilizar um serviço
que se encontra num servidor web, é necessário que este conheça o serviço. Isto é, que
saiba os métodos e os tipos de dados envolvidos. Quando um web service é publicado,
conjuntamente, é publicada a sua especificação em XML. Essa especificação é escrita em
WSDL. Na prática, o WSDL é apenas a especificação do conjunto de tags, bem conhecidas,
que podem ser utilizadas quando se descreve um serviço. Mais à frente, iremos ver um
exemplo de uma destas especificações, mas, do ponto de vista do programador, tal não é
importante.

Existe uma pequena aplicação que se encarrega de obter a especificação de um serviço


em WSDL e de gerar uma classe que encapsula a utilização do serviço. O programador
apenas tem de criar e utilizar objectos dessa classe. Mesmo do ponto de vista de quem
desenvolve um serviço, o WSDL não é assim tão importante, pois este é gerado
automaticamente.

Agora que já vimos um pouco da teoria por detrás dos web services, vamos, então, ver de
que forma é que estes são criados e usados. Para criar web services, é necessário ter um
servidor web que suporte SOAP a executar. Os web services correm dentro deste servidor.
Em particular, na tecnologia Microsoft, é utilizado o Internet Information Server (ES).
Nesta secção, partimos do princípio de que o IIS se encontra a correr na máquina local.

38O © FCA - Editara de Informática


ACESSO À INTERNET

Ou seja, sem perca de generalidade, nos exemplos utilizamos o endereço http://localhost/*,


Caso o leitor disponha do IIS instalado em outra máquina, deverá utilizar o endereço
correspondente.

Para criar web seivlces na plataforma .NET, tanto é possível utilizai' o VisualStudio.NET
como realizar as operações necessárias manualmente. Embora o VisualStudio.NET disponha
já de uma excelente interface para criação de web seivices, nós optámos por mostrar os
passos envolvidos na criação manual de web seivices, evitando, assim, que surjam dúvidas
relativas a aspectos que o VisualStudio.NET escondo, do programador.

l O.2. l CRIAÇÃO DE WEB SERVICES


Como exemplo, iremos criar um web seivice que} uma vez publicado, permite aos seus
clientes executarem diversos algoritmos matemáticos. Por uma questão de simplicidade, o
web seivice apenas irá ter um método que permite adicionar dois valores, retornando o
resultado. Para implementar esta funcionalidade, basta criar um ficheiro, terminado com a
extensão "asmx", tendo o seguinte conteúdo:
<%@ Webservice" Lãhguage=Ilc#n cTass="Algontmos n %> """ " " " ;

.using system;
usnng System.web.Services;
publlc class Algoritmos : Webservice

. [WebMethod]
public int Soma(int valorl, int valor2)
return valorl+valorZ;

í. . .... ... . . . . .. _ . ... ;

O ficheiro de um web seivice tem de começar com uma linha contendo a directiva
@webservice. Isto indica que o ficheiro corresponde a um web seivice. Neste caso, é
indicado que a linguagem utilizada no serviço é C# e que a classe correspondente ao
serviço se chama Algo ritmos.

No ficheiro, encontra-se declarada a classe pública Algoritmos. A classe correspondente


a um web seivice tem de ser sempre pública. Esta classe deriva de webservice 10 .
Finalmente, existe um método somaO, que realiza a soma dos valores passados como
parâmetros, retornando o resultado. Para um método ser publicado num web seivice,
basta marcá-lo com o atributo WebMethod.

9 O leitor deverá ter algum cuidado com a utilização de proxies. Caso exista um proxy configurado, deverá
fazer o bypass do proxy para acessos locais. Caso não o faça, é provável que o endereço http://localhost
seja visto como representando a máquina ao proxy.
10 Não é obrigatório que a classe derive de WebServi cê, mas, normalmente, tal é conveniente. Ao fazer com
que a classe derive de Webservice, existe bastante informação sobre o cliente do web service que fica
disponível, como campos protected da classe base.
© FCA - Editora de Informática 381
C#3.5

E este é todo o processo. Para colocar o web service a funcionar, basta criar um directório
no servidor web e copiar para lá o ficheiro. Assumindo que foi criado um directório
"Matemática" na raiz do directório dos documentos do IIS (isto é, "wwwroot\, basta c

acima) para esse directório.

Para verificar se o web service se encontra realmente a funcionar, basta apontar o


browser de Internet para http://localhost/Matematica/Algontmos,asmx. Nesta página, é
possível ver as operações disponíveis no web service e, em particular, a operação
somaQ. Caso o leitor carregue no link "Soma" irá ser redireccionado para
http://localhost/M.atematica/Algontmos.asmx?op=Soma, vendo aparecer a página
mostrada na figura 10.1.
•3 Abaram "rtttnncr• tlltraslt IMMrt l
t* E* W- 'fala Ia* ti*
J • •?. i! > <g Mata £ • V C3 r

Algoritmos

Som»
T«l

HK fctljuíng a t twncfe SOA* Itawl *nd rtiBBim- The pliIchaMiri fi Md ta bt rt&ttHt Mti Ktud nlu«J-

FOÍT /K4tca»lc*/U«aitDKI.aK9I RTIT/l.t


Ka.ti taciUto»
'Cosctnc-TiT»! ttxe/ioli rtiir«t-utl-e

T r~HiigÍP"ne

Figura 10.1 — A operação SomaQ do web service Algoritmos

Usando esta página, é mesmo possível testar o web service, preenchendo os parâmetros
somai e soma2. Por exemplo, se o mesmo for testado com os valores 10 e 20, surgirá uma
nova página web com a seguinte informação:
<?xmT vérsiòh="1.0" encoding="utf-8" ?>
<1nt xmlns="http://tempun' .qrg/">3p</int>
Olhando com atenção, é possível ver o resultado 30. O leitor mais atento, certamente, que
se está a perguntar que representa http://tempuri.org/. Na verdade, deveria estar
especificado um nome único, associado ao produtor do web service, que funciona como
um espaço de nomes, http://tempuri.org/ é utilizado nos casos em que não é especificado
um espaço de nomes para o web service, representando este URI, um espaço de nomes de
desenvolvimento e teste. Na secção 10.2.3, veremos como é que é colocado o URI
correcto.

Como se pode ver, criar um web service é muito simples. Urn ponto bastante importante
de que o leitor deverá lembrar-se é que a interface web, que acabamos de mostrar, é
382 © FCA - Editora de Informática
ACESSO À INTERNET

apenas para teste de web services e para verificar se estes se encontrara disponíveis. Os
web seivices, propriamente ditos, destinam-se a ser invocados por código em aplicações
definidas pelo programador. Será isso que iremos mostrar de seguida.

1O.2.2 CLIENTES DE WEBSERVICES


Quando um web sennce é publicado no servidor web, é publicada, conjuntamente, a sua
especificação em WSDL. Isto é, a forma como pode ser feita a invocação do serviço:
nome e tipo dos parâmetros, métodos de acesso, etc. Caso um web sennce seja publicado
em http://servidor/Servicot então, ao chamar o serviço, colocando a cadeia de caracteres
"?wsdr no fim do mesmo, é retomada a especificação deste. Por exemplo, no exemplo
que estamos a construir, ao aceder a http://localhost/Matematica/Algorítinos.asmx?wsdl,
surge, entre muitas outras coisas:
<?xml versiorWl.O" encoding="utf-8" ?>

GO m pii
<s:sequence>
<s:element minOccurs="l" maxõccurs="i" nàme="va1orl""type-"s:int"
<s:element m1nOccurs="l"_maxoccurs=nl^_ name="valo_r2" type="s:int"
</s:sequence>
</s:complexType>
</s:element>
s:element narne== SpmaResppnse > _ _ _
<s: comp.l exType>
<s:seqúence>
<s:elemerit mihdccurs="l" maxõccurs="l" name="SomãRèsúlt"
type="s;int" _/>
</s:sequence>
</s:complexType>
</s:element>

Ou seja, a operação soma C) leva como parâmetros dois inteiros (int) de nome vai o ri e
valor2, resultando num inteiro (int) de nome somaResult.

Na prática, o programador não necessita sequer de olhar para a definição de um serviço.


O que é essencial saber é qual o URI do serviço. Utilizando o utilitário wsdl .exe é
possível gerar directamente uma classe que encapsula o acesso ao serviço. Ou seja, ao
correr este programa:
C:\Liyro\exempIos>wsdl rittp: //l ocal host/Matemati ca/Al goritmos. asmx?wsd1
ÍMIerosttf-te QO web Services Description Language Utility *
[Microsoft <R) .NET Framework, Version 1.0.3705.0] . - ; ; y.r
Copyright .CO Microsoft Corporation 1998-2001. Ali rights reserved.
writing file.'c:\Temp\Algoritmps.cs 1 .

© FCA - Editora de Informática 383


C#3.5

é criado um ficheiro "Algoritmos.es", que permite aceder directamente ao serviço.


O código seguinte mostra um pequeno programa que acede ao web semice:

* Programa que mostra o acesso a um web service.


*/
using System;
c lass ExemploCaplO_3
{
public static void Mal n C)
// Cria um objecto que representa o web service
Algoritmos algoritmos = new Al go ritmos C) ;

// Chama o web service no servidor


int resultado = algoritmos.somaClO, 20);
// Mostra o resultado
Console.writeLine("o resultado é: {0}", resultado);

Listagem 10.3 — Programa que ilustra o uso de um M^e.&íe/v/ce(ExemploCaplO_3.cs)

Este ficheiro tem de ser necessariamente compilado em conjunto com "Algoritmos.es".


Ao executar o programa, surge no ecrã:
ipVresuTtadq é 3p " _ . ' . . . _ " " " . ! . . _ " _ . i._l_/I . " . " ' . .
Ou seja, o programa cliente está correctamente a comunicar com o web sennce. Na
prática, toda a parte de SOAP e XML é escondida do programador.

Algo que não foi feito na listagem 10.3, mas que deverá ser feito em código robusto, é
proteger o acesso ao web service com um bloco try-catch. Existem diversas razões
pelas quais um pedido pode falhar, desde um problema na rede, até ao ficheiro "asmx" ter
sido apagado do servidor. Tipicamente, as excepções lançadas são do tipo System.
.Net.WebException, mas podem ocorrer outras.

1O.2.3 CONFIGURAÇÃO E INSTALAÇÃO


Nesta secção, iremos apresentar algumas notas práticas sobre a configuração e a instalação
de web services.

10.2.3.1 QUESTÕES DE CONFIGURAÇÃO


O primeiro ponto para o qual queremos chamar a atenção é que o utilitário wsdl .exe é
mais poderoso do que foi mostrado. É possível especificar diversos parâmetros que
permitem definir a forma como a classe gerada acede ao web sejvice. Por exemplo, caso
seja necessário gerar informação de segurança para autenticação com o servidor, é
possível fazê-lo com as opções /username, /password e /domain. Existem também

384 © FCA - Editora de Informática


ACESSO À INTERNET

parâmetros para especificar um proxy para acesso à Internet, ficheiros de configuração de


acesso, entre outros. O utilitário wsdl .exe cobre a grande maioria de casos de utilização
de web sennces. Caso seja necessária maior flexibilidade, o programador pode sempre
utilizar as classes disponíveis na plataforma .NET, para especificar completamente a
forma como é feito um acesso a um web sennce.

Quando um programador define um web seivice, pode decidir compilá-lo imediatamente,


criando um assembly. Isso permite uma melhor peifoniiance do que ter o serviço num
ficheiro "asmx". Para tal, deverá ser criado um directório chamado "bin", no directório
dedicado ao web seivice, onde será colocado o assembly compilado. A definição do
ficheiro "asmx" é alterada, removendo o código da classe do web servíce. Na prática, para
o exemplo que utilizamos, seria necessário:

Criar um ficheiro com o código fonte da classe Algoritmos. Tipicamente, esse


ficheiro tem o mesmo nome do que o ficheiro "asmx", incluindo a sua terminação,
mas com a extensão "es". Ou seja, "Algoritmos.asmx.es";

Compilar "Algoritmos.asmx.es" para um assembly, fazendo:


esc /t: l lb rary_ Yòut: Al 30 ri tmos. asmx. dl T Al.áp pi tmos .asmx. es

Criar uma directoria "bin" em "wwwroot\Matematica" colocando nesta


"Algoritmos.as mx.dD";

Alterar o ficheiro "Algoritmos.asmx", eliminando a definição da classe


Algoritmos. Ou seja, passando a ter o seguinte conteúdo:
<%@ Websépvice Lahg"uage="c#" class="Àlgóritmos" %>
Quando é necessário carregar a classe Algoritmos, o ES procura no directório "bin" se
algum dos assemblies presentes possui a classe definida. Caso o programador deseje,
pode ainda optimizar esse processo, especificando directamente em que assembly se
encontra a classe. Para isso, basta colocar à frente do nome da classe, separado por
vírgula, o nome do assembly:
<%@ Webserví cê . Language="c#" _cl ass=__"Al go ri tmos./Al. g o ritmos . asmx" %>
Caso o programador utilize o Visual.St.udio.NET, é habitual haver um atributo extra nesta
declaração: CodeBehind. Este atributo permite manter a coerência entre código fonte e
assemblies compilados, indicando qual é o ficheiro correspondente ao código fonte do
serviço. No entanto, é de notar que o ES não recompila automaticamente o assembly, caso o
código seja modificado, mesmo especificando este atributo. Eis uni exemplo da sua
sintaxe:
<°M WébServicê Language="c#" CodeBèhind="Algoritmos.ãslnx.es" .
clasg="Algoritmos J Alggntm_os.asmx l \%> . _ __ .._..-::..

© FCA - Editora de Informática 385


C#3.5

Finalmente, em conjunto com web sennces, existe um outro conjunto de ficheiros que
podem ser definidos e que, normalmente, são automaticamente criados pelo VisualStitdio.
.NET. No entanto, estes ficheiros não são essenciais.

Existe um protocolo chamado DISCO que permite a clientes remotos, descobrir quais os
web seivices presentes numa máquina. Ou seja, não basta publicar a especificação de web
sennce em WSDL, é necessário haver uma forma, de clientes remotos encontrarem essa
especificação. O protocolo DISCO permite exactamente isso. Se no directório de um web
service for colocado um ficheiro com o seu nome e com a terminação "disco" (por
exemplo: "Algoritmos.disco"), então, este ficheiro é utilizado para definir quais os web
sennces que são expostos pelo servidor e os schemas WSDL associados. Isso permite a
clientes remotos, fazerem a descoberta dinâmica do serviço1 .

Existe também um outro ficheiro Standard chamado "Web.config" que pode ser colocado
na directoria do web service. Este ficheiro permite definir configurações específicas para
o web sennce, como, por exemplo, parâmetros de autenticação.

1O.2.3.2 ESPAÇOS DE NOMES

Como vimos no exemplo anterior, o web service desenvolvido encontra-se a enviar


respostas semelhantes a:
<?xml versToh="1.0" encoding="utf-8" ?>
<int xm_lns="http://tempuri .org/">30</1nt>

Para alterar o espaço de nomes http://tempiLri.org/pam um espaço de nomes permanente,


basta utilizar o atributo webservice. Este atributo é utilizado para fornecer alguma
informação sobre um web sennce. Note-se que o nome do espaço de nomes a utilizar não
tem de ser um URI. No entanto, deve ser um nome unicamente associado à instituição
que controla o código. Assim, o URI da instituição é um bom candidato a espaço de
nomes. No caso de Algoritmos, pode-se alterar o espaço de nomes para
http://www.dei.uc.pt/da seguinte forma:
TWebSèrvicê(Namespace="http://www.den.uc.pt/")]'
publlc class Algoritmos : Webservice

[WebMethod]
public int soma(int valorl, int valor2)
return valorl+va~lor2;
}

11 Note-se que, no exemplo que apresentámos, o cliente sabe qual é o URI do web service. O que o DISCO
permite é evitar que o URI tenha de ser conhecido à partida, existindo um processo de descoberta
dinâmica.
386 © FCA - Editora de Informática
ACESSO À INTERNET

1O.2.4 INFORMAÇÃO DISPONÍVEL A UM WEB SERVICE


Apesar do modelo de programação de web sennces ser bastante simples, é necessário ter
algum cuidado porque o ciclo de vida destes serviços não corresponde ao ciclo de vida de
um objecto normal. Por exemplo, suponhamos que um programador tenta declarar um
web service que apenas indica o número de vezes que um certo método foi invocado:
public class 'Algoritmos : Webservice
p rivatè i nt Total Invocações = 0 ; ''
..,
"public int ContalnvocacoesO
í " ••
•H-Total Invocações;
return Total invocações; :
> !

Existe uma variável privada, chamada Total invocações, que é incrementada sempre que
ContalnvocacoesO é executado. Aparentemente, sempre que o método fosse invocado,
Total invocações seria incrementado, sendo o resultado retornado para o cliente. Tal não
acontece. Na verdade, sempre que existe uma invocação, é criado um novo objecto que é
utilizado para fazer a invocação. Não existe um objecto permanentemente no servidor a
atender pedidos remotos12.

Ao derivar a classe de um web service de webservice, é simples resolver este tipo de


problemas. A classe webservice disponibiliza um conjunto de propriedades importantes,
das quais as mais relevantes são:

Application, que representa a aplicação correspondente ao web service como um


todo. Não está associado a nenhum cliente em particular, nem a nenhuma sessão;

Context, que encapsula toda a informação HTTP específica do pedido que está
em decurso. Em particular, a informação do cabeçalho do pedido e o objecto de
resposta;

serve r, que representa o servidor em causa. Com esta propriedade é possível


obter o nome da máquina, a hora corrente e instanciar objectos COM, entre outras
coisas;

session, que representa a sessão corrente. Uma sessão corresponde ao conjunto


de interacções entre um cljente e um web seivice, desde o momento em que o
cliente invoca o primeiro método, até que quebre a ligação. Cada cliente possui
uma sessão, mas é possível um cliente ter mais do que uma sessão activa com um
web service;

12 Existe urn API chamado Remoting que permite ter esse tipo de funcionalidade.
© FCA - Editora de Informática 387
C#3.5 _

• User, que representa o utilizador a utilizar o web service, incluindo as suas


credenciais de segurança.

De todas estas propriedades, iremos, apenas, examinar de que forma se pode guardar
informação, relativamente a uma aplicação e a uma sessão.

No exemplo que apresentámos anteriormente, a forma correcta de resolver o problema de


contagem do número de invocações seria guardar essa informação em Application. Esta
propriedade dispõe de uma propriedade indexada que funciona como tabela associativa.
É possível guardar nela informação, assim como recuperá-la. Desta forma, a informação
sobrevive entre invocações, correspondendo às invocações de todos os clientes.
Concretizando:
ipiíblTc "clãs s Algoritmos "f Websérvice
: private const string TOTAL_INVOCACOES = "Invocações Algoritmos";
public AlgoritmosQ
1f (Appl1cat1on[TOTAL_INVOCACOES] == null)
Appl1cat1on[TOTAL_INVOCACOES] = 0 ;

: [WebMethod]
: public int ContainvocacoesQ
Int total invocações = (Int) Appl1cat1on[TOTAL_lNVOCACOES] ; :
• -H-total Invocações ; ;
Appl1cat1on[TOTAL_lNVOCACOES] = total Invocações ;
; return total Invocações; !

Neste caso, é definida uma constante chamada TOTAL_INVOCACOES que será utilizada
como chave da informação a guardar na aplicação. Quando o primeiro objecto é criado, é
colocado em Appl 1 catl on o valor O, correspondendo ao total de invocações já realizadas.
Tal é conseguido com:
rif CAppT1'caffon[tOTAL_ÍNVõCACOÉs] "== null) " " •-•--••
AppJ1cat1on[TOTAL^INVOCACOES] = 0 ; _,. ..

Um aspecto importante desta tabela é que antes de a primeira invocação ser realizada,
fazer Appl1cat1on[TOTALJCNVOCACOES] resulta numa referência n u l l , pois ainda não
existe nenhuma informação armazenada, correspondente a essa chave.

Sempre que containvocacoesO e invocado, é obtido o valor presente em


Application[TOTAL_iNVOCACOES], sendo o mesmo incrementado e colocado novamente
em Application.

Assim como é possível guardar informação relativa à aplicação global, é também possível
guardar informação relativa a uma sessão com um cliente. Por cada sessão com cada
cliente, existe um objecto diferente onde é possível obter e colocar informação. Para isso,

388 © FCA - Editora de Informática


ACESSO À INTERNET

utiliza-se o objecto session. Por omissão, os métodos associados a um web sei-vice não
suportam sessões. Para activá-las, é necessário colocar Enable5ess1on=true, no atributo
WebMethod. De resto, a sua utilização é similar a Application:
p u b l i c class Algoritmos : Websèrvlce
p ri vate const string TOTAL_INVOCACOES = "invocações Algoritmos";
public Algoritmos C)
i f (SessÍon[TOTAL_INVOCACOES] == null)
Session[TOTAL_INVOCACOES] = 0;
}
[WebMethod(EnableSessi on=true)]
public int ContalnvocacoesQ
int totalInvocações = (int) session[TOTAL_!NVOCACOES];
-H-total invocações;
Session[TOTAL_INVOCACOES] = totalInvocações;
return totalInvocações;
; >
Note-se que todos os métodos que necessitam de utilizar sessões devem ter
Enabl esession=true. No entanto, a sessão não é por método, é relativa ao web semice
como um todo. Enablesession=true apenas faz com que o objecto session seja
disponibilizado no objecto. Caso esta propriedade não seja colocada a true, Session será
uma referência para nul l .

Relativamente a estes exemplos, é importante chamar a atenção para o facto de a


invocação de um método de um web service não ser thread safe. Isto é, podem ocorrer
invocações simultâneas. O programador deve proteger devidamente o acesso a
informação partilhada. Na verdade, nos exemplos anteriores, o acesso a
Application[TOTAi__iNVoCACO£S] e session [TOTAL_INVOCACOES] deve ser feito em
exclusão mútua. Uma forma simples de conseguir isto é chamando os métodos LockQ e
UnLockQ sobre Application (ou session). Isso garante atomicidade no acesso:
rpublic class Algoritmos : WebService
; private const string TOTAL_INVOCACOES = "invocações Algoritmos";
public Algoritmos ()
tT~'JÃP^"lçat;íc)n'.LocRQ;^"~ I " ................ _ " '.'_"'.".".."" 7.1
i f (Âpplicâtiòn[TOTÂL_INVOCÃCOÉS] == null)
APPlÍ9a^_qnCTOTAL_INypCACOES]_= O J
catión.UhLbckQJ ""II _ ' ... _ .__ .._. _ ......... l
........ -......" - ^
[WebMethod]
' public int ContalnvocacoesQ
Applicatib"n.LpcRQy ' _ " ~ — .--^ -...........-
int total invocações = (int) Applicatiori[TOTAl_INVOCACOES] ;
++total invocações;. ..... . . . _ _ _ __ ...... ............____________....... . .

© FCA - Editora de Informática . 389


C#3.5

rcãtfòh[TOTAUlíNVOCACOÉS] = totalInvocações;
ÃppTi cati on. UnLocKjO:;
return totalInvocações;

E, no entanto, de referir que este método garante a exclusão mútua sobre todos os
elementos presentes na tabela associativa. Caso exista um grau elevado de concorrência
no acesso à tabela, haverá uma elevada serialização da execução das threads, levando a
uma perda de performance. Nesses casos, será melhor utilizar objectos de exclusão mútua
independentes, fazendo lock e unlock sobre estes.

~ Um web service representa um serviço disponível na web, acessível através de


ARHTHR protocolos Standard da Internet. Para invocar um web service, tipicamente, é
utilizado HTTP como protocolo de transporte e SOAP, como protocolo de
Web Services invocação. As mensagens trocadas entre clientes e servidores são
codificadas em XML, sendo os serviços disponíveis descritos em WSDL.
" Para criar web services, é necessário ter um servidor web com suporte
SOAP a executar. Em particular, na tecnologia Microsoft, é utilizado o
Internet Infonnation Server (IIS).
~ Para criar um web service, basta criar um ficheiro com a extensão "asmx".
Esse ficheiro deverá começar com a directiva @webservi cê indicando qual a
classe correspondente ao serviço. Por exemplo:
<%@ Webservice Language="c#" Class="Algoritmos" %>
~ Nesse ficheiro, deverá ser implementada a classe correspondente ao web
service que, regra geral, deverá derivar de Webservice, pertencente ao
espaço de nomes System.Web.Servi cês.
" Todos os métodos publicados pelo web service são marcados com o atributo
WebMethod.
" Para publicar um web service, basta copiar o ficheiro para um directório
virtual do servidor web, correspondendo esse directório, ao serviço que se
quer disponibilizar. O serviço fica disponível em:
http://nomesenndor/DirectorioServico/NomeSen>ico.asmx
A definição do serviço fica disponível em:
http://nomeservidor/DlrectorioSer\nco/NomeSennco.asmx?wsdl
~ Para um cliente utilizar o serviço, necessita de gerar urna classe que o
represente. Para isso, corre o utilitário wsdl . exe sobre o URI onde se
encontra o serviço disponibilizado:
wsdl http://nomeservidor/DirectorioSei-vico/NomeServico.asmx?\vsdl
" Tendo a classe gerada, a\ttilização do serviço é directa. Basta criar uma
instância da classe e invocar nesta, os métodos do serviço. As invocações
devem ser protegidas por um bloco try-catch.

39O © FCA - Editora de Informática


ACESSO À INTERNET

- Os web senrices podem ser pré-co m pilados. Nesse caso, os assembiies


ARETER correspondentes devem ser colocados no directório "bin" do directório do
serviço. O ITS encarrega-se de procurar nos assembiies existentes a classe
Web Services . ,. , no ncheiro
indicada ^ , • «asmx„. ^ E *também
u - possível
- i indicar
• i- T>
o assembly
coiTespondente à classe, directamente no ficheiro asmx:
<%@ Webservice Language="c#" class="Algoritmos,NomeAssembly" %>
" Quando um web service é colocado num servidor de produção, deve-lhe ser
atribuído ura espaço de nomes único, indicado no atributo Webservice:
[WebService(Namespace~ n http://www.dei.uc.pt/")]
" Para cada invocação de um web sennce, é criado um novo objecto. As
variáveis de instância do objecto são únicas à invocação em causa.
" Para guardar informação de sessão e informação relativamente à aplicação,
utiliza-se as propriedades session e Application. Estas classes funcionam
como tabelas associativas. Por exemplo:
Session ["Nome da Pessoa11] = "Marilia oliveira";
- Para o objecto Session ficar disponível a um método, é necessário colocar o
atributo EnableSessionatrue: [WebMethod(Enablesession=true)]
" As invocações de web servlces não são thread safe, logo, é necessário ter
cuidado com a questão de acesso concorrente a recursos partilhados. No
caso de Session e Application, pode fazer-se LockQ e unlock C) desses
objectos, embora tal não seja recomendado, devido a questões de
performance.

\3 UTILIZAÇÃO DO PROTOCOLO TCP/IP


3

Até agora, temos estado a examinar de que forma é possível utilizar serviços
disponibilizados na Internet. No entanto, estes serviços e protocolos associados, como o
HTTP, partilham de uma característica comum: são implementados sobre o conjunto
protocolar TCP/IP. O TCP/IP é a base que permite a troca de informação entre quaisquer
duas máquinas na Internet. Embora haja uma tendência cada vez maior para o
programador utilizar protocolos de alto nível, e para os API disponíveis esconderem o
que é utilizado para troca de informação na rede, muitas vezes, ainda é necessário
implementar serviços puramente baseados em TCP/IP.

Iremos, agora, examinar de que forma é que se pode utilizar este protocolo na plataforma
.NET.

1O.3.1 PROTOCOLO TCP


O protocolo TCP (Transmission Trcmsport Protocol} é orientado à ligação e suporta
entrega por ordem de dados, assim como retransmissão automática de dados perdidos na
ligação.

© FCA - Editora de Informática 391


C#3.5

Sempre que duas aplicações comunicam, utilizando o protocolo TCP, é criada uma
ligação entre elas. Cada um dos pontos terminais da ligação, um em cada aplicação,
possui um número associado, chamado porto. Quando uma aplicação cliente se liga a uma
aplicação servidor, necessita de saber qual o endereço em que a máquina servidor se
encontra e em que porto é que a aplicação servidor se encontra à escuta.

Para criar uma aplicação servidor que fica à escuta de ligações, utiliza-se a classe
TcpListener. Uma aplicação que se queira ligar a um servidor utiliza a classe
Tcpcl i ent. Ambas as classes existem no espaço de nomes system. Net. sockets.

Vamos, primeiro, examinar o processo do ponto de vista do servidor.

1O.3.1.1 SERVIDORES TCP


Para um servidor ficar à escuta num determinado porto, basta criar uma instância de
TcpListener, indicando o porto onde escutar. Quanto pretende começar a receber
ligações, invoca o método startQ:
const irit; PÕRTÕ_SÈRVIDOR = 7400;
TcpListener servidor = new TcpListener(PORTO_SERVIDOR);
•servi dor.Start.Q;
Para escutar por uma nova ligação, é necessário chamar o método AcceptTcpclientQ.
Este método bloqueia até que um cliente se ligue ao servidor. Quando isso acontece, é
retornada uma referência para um objecto do tipo Tcpcl i ent:
Tçpcliejit umclienté = ligacaoSeryidor.AcceptTcpcTientO;
Após ter-se este objecto, é, então, possível obter &stream associada à ligação, podendo-se
trocar informação com o cliente. Ao invocar GetStreamQ, a stream retomada é do tipo
Networkstream, mas é possível associá-la a streams com maior funcionalidade:
Networkstreám streamLigacao = umdiente.GetStreamO ;
streamWriter escrita = new streamWriter(streamLigacao);
StreamReader leitura = new streamReader(streamLigacao);
escrita.writeLine("olá, como está?");
escrita. FlushQ;
.strinq resposta = leitura.ReadLineQ ;
Console.WriteLine("0 cliente respondeu: {0}", resposta);
O método FlushQ permite esvaziar o buffer de dados pendentes, enviando-os para o
cliente quando é feita a invocação.

Quando se pretende fechar a ligação, basta fechar as streams e em seguida a ligação:


escrita, dose O ; - - - - - - -
leitura.closeQ;
umcliente.CloseQ;

392 © FCA - Editora de Informática


ACESSO À INTERNET

Antes de mostrarmos a implementação de um servidor completo, existem alguns pontos


práticos para os quais convém chamar a atenção. Em primeiro lugar, normalmente, um
servidor atende diversos clientes simultaneamente. Ou seja, tipicamente, as aplicações
servidoras são multithreaded. Ao retornar de AcceptTcpclientQ, é usual criar-se uma
thread que trate do cliente em causa. A thread principal volta a chamar
AcceptTcpdientQ, esperando pelo próximo cliente:
//"Para todo ò sempre.".".' atende pedidos
while (true)
{
: Tcçclient umcliente = servidor.AcceptTcpClientQ;
Cliente cliente = new Cli ente(umcli ente);
> ...... . . . .
Neste caso, a classe cl i ente cria internamente uma nova thread e trata da ligação que lhe
foi passada como parâmetro.

O segundo ponto importante é que em aplicações que envolvam comunicação por rede, é
habitual haver falhas. Isto quer dizer que os métodos associados a recursos de rede
lançam diversas excepções. Estas devem ser tratadas. Neste aspecto, pelo facto de haver
um problema com um cliente, uma excepção que ocorra nesse cliente não deve afectar
outros, ou o funcionamento do servidor. Há que ter cuidado com esse aspecto.

Apresentamos, de seguida, um servidor multithreaded que atende diversos clientes


simultaneamente. Cada cliente envia mensagens para o servidor que este mostra no ecrã.
Para o cliente, é enviada uma mensagem de "Ok", indicando o número do cliente.
Quando o cliente envia a palavra "fim", a ligação com esse cliente é terminada.
7*
* servidor TCP multithreaded.
V
using system;
using System.Net.sockets;
using system.Net;
using System.Threading;
using System.IO;
using System.Text;
// classe que representa um cliente do servidor
class cliente
private const string FIM_LIGACAO = "fim";
private TcpClient LigacaoCliente; // Ligação do cliente
private int id; // identificador do cliente
private static int contaclientes; // clientes que já se ligaram
public clienteGrcpClient ligação)
LigacaoCliente = ligação;
Id = ContaClientes++;
// A cada cliente corresponde uma nova thread
Thread thrdiente = new Thread(new Threadstart(this.AtendePedido));
thrCliente.StartQ ;

© FCA - Editora de Informática 393


C#3.5

// Atendimento de um pedido de um cliente / thread desse cliente


p n" vate void AtendePedidoQ
// streams de leitura e escrita para o cliente
StreamWriter escritacliente = null;
streamReader leituraCliente = null ;
try
// cria streams para leitura e escrita do cliente
NetworkStream streamLigacao = Li gacaocl i ente. GetStreamQ ;
escritacliente = new StreamWriter(streamLigacao) ;
leituraCliente = new streamReader(streamLigacao) ;
// Envio automático das mensagem e impressão da mensagem inicial
escritacliente. AutoFlush = true;
escritacliente. WriteLine("Bem vindo, cliente {0}.", Id) ;
// Lê dados do cliente até que a ligação seja fechada ou receba
// a palavra FINLLIGACAO. É enviado um OK para o cliente.
string linha;
do
{
linha = leituracliente.ReadLineQ ;
if (linha != null)
console. writei_ine("cliente {0}: {!}", Id, linha);
escritacliente. WriteLine("ok {O}", Id) ;
} while ( (1 i nhã != null) && (linha 1= FIM.LIGACAO)) ;
}
catch (Exception e)
Console. Writel_ine("Problema com cliente: {0} / {!}",
Li gacaocl i ente , e) ;
}
f i nal l y
if (escritacliente != null)
es c ri taci i ente. Cl ose () ;
if (leituraCliente != null)
l ei tu racl i ente . Cl ose () ;
i f (Li gacaocl i ente != null)
Li gacaocl i ente . Cl ose() ;

// Classe gue representa o servidor


class Servidor
{
public const int PORTO_OMISSAO = 7500;
public servidor(int porto)
TcpListener LigacaoServidor == null ;
try
// Abre a ligação no servidor
_ LiqacaoServidor = new TcpListener(porto) ;

394 © FCA - Editora de Informática


ACESSO À INTERNET

Li gacaoseryl dor . start C) ;


Console. Writel_ine("Servidor a correr no porto {0}", porto);
// Para todo sempre, aceita novos clientes
while (true)

TcçClient ligacaodiente = LigacaoServidor.AcceptTcpClientQ ;


Cliente novocliente = new cliente(ligacaodiente) ;

catch (Exception e)
{
console.WriteLine(e.Message) ;
Console.writeLine(e.StackTrace) ;
}
final l y

if (LigacaoServidor != null)
LigacaoServidor. stopQ ;

// Programa principal
class ExemploCaplO_4

public static void Main(string[] args)

Servidor servidor = new Servidor(Servidor.PORTO_OMl5SAO);


}

Listagem 10.4 —Servidor TCP muitíthreaded

Para testar este programa, pode-se utilizar o programa telnet. Este programa permite
ligar directamente a um porto de um servidor. Assim, basta colocar o programa a executar
numa janela e, noutra à parte, fazer:
telnet localhost 7500 " ' " ---
O resultado é apresentado na figura 10.2, após terem sido enviadas algumas frases para o
servidor.

Figura 10.2 —Teste do servidor utilizando o programa telnet


© FCA - Editora de Informática 395
C#3.5

1O.3.1.2 QUENTES TCP

Para criar uma aplicação cliente, utiliza-se a classe Tcpclient. Ao criar um objecto desta
classe, indica-se o nome do servidor e do porto onde se ligar, sendo estabelecida a ligação.
Após esse momento, pode-se obter a stream associada e comunicar com o servidor13:
itcpcTient ligação = new Tc~pClient("localhost", 7500);"
;NetworkStream streamLigacao = ligação.GetStreamC);
iStreamReader leitura = new streamReader(streamLigacao);
'StreamWriter escrita = new streamWriter(streamLigacao);

:escrita.Writel_ine("olá servidor!");
;escrita.Flush() ;

string resposta = leitura.ReadLineQ ;


iConsole.WriteLine("0 servidor respondeu: {0}", resposta);

No final, é também necessário fechar as streams criadas e a ligação ao servidor.

ARETER " O protocolo TCP é orientado à ligação e suporta entrega por ordem de
dados, assim como retransmissão automática de dados perdidos na ligação.
Utilização do ~ Para desenvolver um servidor TCP, cria-se uma instância de TCPListener,
protocolo TCP indicando o porto a escutar, e faz-se startQ para activar o servidor. O
método AcceptrcpcIientO bloqueia a thread que o chama, até que um
cliente se ligue ao servidor, retornando uma instância de Tcpclient que
representa o cliente ligado. É, então, possível obter uma stream de dados
associada a esse cliente.
" Para desenvolver um cliente TCP, cria-se uma instância de Tcpclient,
indicando-se o nome do servidor e do porto onde o cliente se irá ligar.
Após isto, é possível obter uma stream associada ao servidor, chamando
GetStreamC) no objecto criado.
" Após qualquer comunicação entre clientes e servidores, as stream e as
ligações utilizadas devem ser fechadas.
- Deve-se proteger as invocações de objectos que envolvem uso da rede com
um bloco try-catch.

IO.3.2 PROTOCOLO UDP


No contexto da Internet, o protocolo de transporte mais utilizado é o TCP. No entanto,
existe um outro protocolo com alguma utilização relativa; o protocolo UDP (User
Datagram Protocot).

Ao contrário do TCP, o protocolo UDP é utilizado para trocar mensagens curtas, não
sendo orientado à ligação. O protocolo UDP também não fornece garantia na entrega de

O tratamento de excepções é omitido por simplicidade.


396 © FCA - Editora de Informática
ACESSO À INTERNET

mensagens, estas podem-se perder, sendo da responsabilidade da aplicação, recuperar


disso. Também não existe garantia de ordem entre mensagens (isto é, as mensagens
podem não chegar pela ordem enviada). Tipicamente, o protocolo UDP é utilizado para
trocar informação não crítica. Por exemplo, difusão da hora corrente numa rede local. Se
uma das mensagens se perder, as máquinas podem sempre receber a próxima, em geral,
sem grandes consequências práticas.

Uma forma interessante de perceber os protocolos TCP e UDP é através de analogias.


O protocolo TCP funciona como "um telefone". Existe uma ligação entre um cliente e um
servidor. Tudo o que o cliente diz é entregue no servidor e pela ordem que foi dita. No
máximo, o que pode acontecer é existir uma pequena demora até que a mensagem
chegue, como acontece numa ligação internacional, ou a ligação cair como um. todo. As
mensagens do protocolo UDP podem ser vistas como cartas. Cada uma é independente de
outra. Em geral, duas cartas enviadas de seguida chegam pela mesma ordem, mas tal não
é garantido. Também podem existir cartas perdidas, sem que o receptor tenha consciência
desse facto.

1OJ3.2.1 LhTUZAÇÃO DO PROTOCOLjO UDP


Para utilizar o protocolo UDP, existe apenas uma classe: udpclient. Esta classe permite,
tanto enviar, como receber mensagens.

Para receber uma mensagem, basta criar uma instância de udpcl i ent, indicando em que
porto é que se quer escutar, e chamar o método RecelveQ. Este método necessita de um
objecto do tipo iPEndPoi nt, que é utilizado para obter o endereço da máquina que enviou
a mensagem, e retorna a mensagem recebida. A mensagem recebida é uma tabela de
bytes. O código seguinte ilustra o processo, mostrando a mensagem recebida em texto:
// cria um cliente ÚDP ^associado ao" porto "PORTO - • - - • • •
const int PORTÇ = 7500;
Udpclient servidor = new Udpclient(PORTO);
-// Recebe uma mensagem de qualquer endereço
iPEndpoint cliente = new iPEndPointCnPAddress.Any, 0);
byte[] mensagemRecebida = servidor.Receive(ref cliente);
// Descodifica e mostra a mensagem . - .-
string mensagemTexto = Encoding.ASCii.Getstring.(mensagemRecebida);
Console.WriteLine(mensagemTexto); . _ _
Note-se que, ao criar IPEndpoint, é indicado que o endereço correspondente é "qualquer
um" (iPAddress.Any), e o segundo parâmetro, que representa o porto de envio, é nulo,
que representa qualquer porto. Isto permite receber mensagens de qualquer endereço. No
final da invocação de Recei vê (), cl i ente irá conter o endereço da máquina que enviou a
mensagem. O endereço pode ser extraído, fazendo cliente.Address, que retorna um
objecto do tipo iPAddress, que já examinamos. Dado que a variável é utilizada como
parâmetro de entrada e de saída, esta é passada por referência (ref).

© FCA - Editora de Informática 397


C#3.5 _

Para enviar uma mensagem, basta possuir uma instância de udpclient e chamar o
método send(). Este método possui como argumentos a mensagem a enviar (uma tabela
de bytes), o número de bytes a enviar, o nome da máquina destino e, finalmente, o porto
destino. Para enviar uma mensagem de texto para um servidor, basta fazer:
"privâté" const int PORTO = 7 5 0 0 ^ ....................
; private const string SERVIDOR = "localhost";

// Cria uma nova mensagem para enviar


jbyte[] mensagem = Encoding. ASCII. GetBytes("olá servidor!");
|// Envia a mensagem :
lUdçdient cliente = new udpdientQ;
mensagem. Lengthj _SERyippR, PORTO) ; „ ..... .........

Tanto o construtor como os métodos SendO e ReceiveQ possuem diversas variantes que
podem ser utilizadas da forma mais conveniente ao programador, No entanto, a
funcionalidade base é a que foi apresentada.

As duas listagens seguintes apresentam um programa para enviar uma mensagem, usando
UDP e um programa para a receber.

* programa que ilustra a recepção de uma mensagem UDP.


*/
using System;
using System.Net.Sockets;
using System.Net;
using System.Text;

class ExemploCaplO_5
public static void Main(string[] args)

const int PORTO = 7500;

try
Udpclient servidor = new Udpclient(PORTO);

// Recebe uma mensagem de qualquer endereço


IPEndPoint cliente = new IPEndPoint(IPAddress.Any, 0);
byte[] mensagem = servi dor.Receive(ref cliente);

// Descodifica e mostra a mensagem


string mensagemTexto = Encoding.ASCII.GetString(mensagem) ;
Console.writeLine(mensagemTexto);
catch (Exception e)
Console.WriteLine(e.Message);

Listagem 10.5 - Programa que ilustra a recepção de uma mensagem usando UDP
(ExemploCaplO_5.cs)
398 © FCA - Editora de Informática
ACESSO À l NTERN ET

/*
* Programa que ilustra o envio de uma mensagem UDP.
*/
using System;
using System.Net.Sockets;
using system.Net;
using System.Text;
class ExemploCaplO_6
{
private const int PORTO = 7500;
private const string SERVIDOR ~ "localhost";
public static void Main(string[] args)
{
try
// Cria uma nova mensagem para enviar
byte[] mensagem = Encoding.ASCII.GetBytes("Olá servidor!");
// Envia a mensagem
UdpClient cliente = new UdpcIientQ;
cli ente.Send(mensagem, mensagem.Length, SERVIDOR, PORTO);
}
catch (Exception e)

Console.WriteLine(e.Message);

Listagem 10.6 — Programa que ilustra o envio de uma mensagem usando UDP
(ExemploCaplO_6.cs)

O protocolo UDP é orientado às mensagens, sem garantias de entrega por


ARETER ordem de dados e sem retransmissão de mensagens, em caso de extravio
das mesmas.
Utilização do
protocolo UDP Para utilizar o protocolo UDP, existe apenas uma classe: UdpClient. Esta
classe permite, tanto enviar, como receber mensagens.
Para receber uma mensagem, basta criar uma instância de udpclient,
indicando em que porto é que se quer escutar e chamar o método
ReceiveC). Este método necessita de um objecto do tipo iPEndpoint, que
é utilizado para obter o endereço da máquina que enviou a mensagem, e
retorna a mensagem recebida. A mensagem recebida é uma tabela de bytes.
Para enviar uma mensagem, basta possuir uma instância de udpclient e
chamar o método Send (). Este método possui como argumentos a
mensagem a enviar (uma tabela de bytes}, o número de bytes a enviar, o
nome da máquina destino e, finalmente, o porto destino.
Deve-se proteger as invocações de objectos que envolvem uso da rede com
um bloco try-catch.

© TCA - Editora de Informática 399


11 * INTRODUÇÃO À LJNQ
Na versão 3.0 do C#, foi introduzido um conjunto importante de novas funcionalidades,
como inferência automática de tipos, tipos anónimos, métodos de extensão, expressões
lambda e expressões de consulta. Embora cada uma dessas características seja útil por si
só, na verdade, o seu poder só é realmente patente quando usadas, de forma integrada, na
LINQ. A LINQ (Language Integrated Queiy) é uma linguagem integrada de consulta
que permite tratar de forma uniforme dados de diferentes origens (por exemplo, bases de
dados ou ficheiros XML).

A ideia da LINQ é resolver um dos maiores problemas das aplicações actuais orientadas aos
objectos. Por um lado, os objectos são excelentes para serem manipulados nas linguagens
de programação. Por outro lado, actualmente, o armazenamento de dados faz-se,
tipicamente, em bases-de-dados relacionais e ficheiros XML, o que vem dificultar a sua
manipulação. A LINQ consiste numa linguagem declarativa, perfeitamente integrada em
C#, que minimiza a distância que existe entre dados e objectos. Consideremos o exemplo
apresentado no capítulo 7:
Vá r ãTuhoQúery = "" ' , --^-,~ t " ' " :
from aluno In alunos ' ""
: where aluno.ld == -j d
• select new { aluno. Npme^ a] uno. Apeado, _ ai. u no._ idade J;
Este código irá produzir um resultado em alunoQuery, consistindo num conjunto de
objectos, contendo o nome, apelido e idade dos alunos que têm um certo identificador.
Caso a variável ai unos represente uma tabela numa base-de-dados, esta é automaticamente
acedida. Caso esta variável represente dados num ficheiro XML, a mesma coisa
acontecerá. Se alunos representar uma simples tabela de objectos, tal como foi
apresentado no exemplo original:
var alunos = new Ãlu"hõ[y~ ~ -—-=—
new Aluno { id=l, Nome= "Maria", Apelido= iiit Carvalho"
new AlUno à Id=2 j Nome= 'Pedro", Apel1do= nMartins", Idad&=2S J;,
new Aluno í Id=3, Nome= 'Ana" , Apelido= n Ferreira" Idade=ZÕ J,
new AlUnq -[ Id=4, Notne= 'Maria", Apelido= n Cardoso" , =25. J-,
new Á1 ó iío' { Id=5 , Nome- 'doao", Apel i do=Abreu" ,

© FCA - Editora de Informática 40 t


C#3.5

então, apenas os dados em memória serão usados. Como se pode ver, independentemente
da fonte de dados, o sistema de execução da LINQ permite tratá-los de forma uniforme e
transparente em C#.

Em virtude da arquitectura LINQ ser demasiado abrangente, é impossível falar, de forma


exaustiva, de todos os aspectos relacionados com esta. Na verdade, poderia escrever-se
um ou mais livros apenas sobre LINQ. O leitor interessado deverá, possivelmente,
adquirir um livro especificamente sobre este tópico. Neste capítulo, iremos abordar de
forma mais detalhada, apenas as expressões de consulta e alguns componentes associados
ao ambiente de execução (por exemplo, LINQ para SQL e LINQ para XML). Dada a
relevância de algumas funcionalidades do C# para o LINQ, antes de ler este capítulo,
sugerimos uma leitura rápida das seguintes secções do capítulo 7:

Tipos Anónimos;

Expressões de Consulta;

Inferência em Expressões de Consulta.

11.1 EXPRESSÕES DE CONSULTA


As expressões de consulta têm como objectivo, fornecer uma forma integrada, para
consultas semelhantes a linguagens como o SQL ou XQuery. A tabela seguinte sumariza
as principais palavras-chave usadas em expressões de consulta.
| EXPRESSÃO | DESCRIÇÃO

f rom Especifica uma fonte de dados e uma variável local que representa
cada elemento da colecção.

where Especifica critérios de restrição da consulta, seleccionando resultados •


que satisfaçam uma expressão lógica.
s el e et | Especifica os valores que devem resultar da pesquisa.

group Agrupa os resultados de uma consulta, de acordo com uma


determinada chave.

•into Fornece um identificador que pode servir como referência aos


resultados de uma cláusula join, group ou select.
orderby | Ordena, de forma ascendente ou descendente, os resultados.
join Combina duas fontes de dados, usando um critério de
correspondência entre eles (por exemplo, igualdade de dois campos). .

let
Introduz uma variável local para armazenar os resultados de uma
sub-consulta.

Tabela 11.1 — Palavras-chave que podem ser usadas numa expressão de consulta

4O2 © FCA - Editora de Informática


INTRODUÇÃO Â LINQ

Nas próximas secções, iremos ver cada uma destas expressões com mais detalhe. No
entanto, para o leitor que queira ver um número elevado de exemplos, sugerimos o
seguinte apontador ("Visual C# Developer Center-101 LINQ Samples"}:

http://msdn.microsoft.com/en-iis/vcsharp/aa336746.aspx

Esta é uma referência essencial para os programadores de LINQ. Atrevemo-nos a dizer


que, talvez, seja a forma mais prática de aprender a usar a linguagem de forma produtiva.

11.1.1 EXPRESSÃO FROM


Qualquer expressão de consulta começa obrigatoriamente por f rom. Esta palavra-chave
especifica qual a fonte de dados envolvida na pesquisa. Obviamente, esta terá de ser
iterável (por exemplo, do tipo lEnumerable). Considerando ainda o exemplo:
,var alunos = new Áluno[] ,
new Aluno { id=l, Nome= "Maria", Apêlido= , Idade=25 },
new" Aluno { ld^2, Nome='Pedro", Apelido= 'Martins 1 1 , Idade=23 .},
new.Aluno { ld=3, Nome= 'Ana", Apelido^ 'Ferreira", Idade=20 },
new Aluno { ld=4, Nome= "Maria", Apelido= 'Cardoso", Idade=25 J,
new Aluno { ld=5, Nome= "Doao", Apelido= "Abreu", Idade=25 }

var alunoQuery =
| f ròm aluno i ri alunos _ - - - -- - - ..- |
where" aluno.Id == id
sel.ect new { aluno.Nome, ai uno. Apelido, aluno.idade };
ai unos encontra-se nesta condição, uma vez que representa uma tabela.

É ainda possível especificar mais do que um f rom na mesma expressão de pesquisa. Por
exemplo, admitindo que possuíamos uma outra tabela com as avaliações dos alunos:
class Avaliação
{
public int Id { get; set; } // Identificador do aluno
1 public int[] Notas { get; set; } // Notas dos testes

v a r avaliações = n e w AvaliacaofJ - . . .
' new^V/lTa-cao { Id=l, Notas=new int[] {12, 13, 14, 13} }> '-,.'., , f
• new^A^a-liatao { ld=2, Notas=new int[], {15, 14, 16, l?}. }, ', ; ' V - : . ;
neW y\'v|liacao { id=3, Notas^new int[] {10, 12, 14, lèí >, •"'-.'. '
new Avéfecao { id=4, Notas=new ,int[] {15, 18, 17, 181 - 3-, y.. ; - ; , - ,
,- neWfA^W^cao { id=5, Notas=new int[] {19, 19, 18, 17Í } í ~< :S* '.
1; - •'- ........ . . ........ . ' ;t ---'w-v
torna-se possível escrever uma expressão que retoma a informação do aluno, associada às
suas notas. O seguinte código:

© FCA - Editora de Informática 4O3


C#3.5

var alunoNotas =
from aluno i n alunos
from notas In avaliações
where aluno.ld — notas.Id
select new_t aluno^Npme, ai uno. Apelido., aluno.Idade, notas.Notas };
foreach (var aluno i n alunoNotas)
{
Console.writei_ine("{0} {l}", ai uno. Nome, ai uno. Apelido) ;
foreach (var nota i n ai uno.Notas)
console.Write("{0} ", nota);
Console.WriteLineQ ;
J . . . . ... . ..
imprime as notas de cada aluno:
Maria Carvalho
12 13 14 13
Pedro Martins
15 14 16 15
Ana Ferreira
10 12 14 16
Maria Cardoso
15 18 17 18
João Abreu
:19 19 18 17

Na expressão de consulta:
var "alunoNbta"s "="
; from aluno i n alunos :
from notas i n avaliações
where aluno.ld == notas.Id
[ __select__new. { alunq.Npme, aluno.Apelido^ notas.Notas };
indica-se, como fonte de informação, duas colecções (alunos e avaliações). Em
seguida, garante-se que ao considerar cada elemento presente em cada uma das colecções,
os seus identificadores são idênticos (aluno.ld == notas.Id). Finalmente, emite-se um
novo tipo anónimo contendo o nome, o apelido e as notas correspondentes a cada aluno.

Um ponto importante é que dentro de uma expressão from é possível referenciar dados de
outro from. Por exemplo, a seguinte expressão:
var boasNotãs" =
from aluno i n alunos
from notas i n avaliações _ _ _
T "from nota" i n notas.Notas" ~ """ "" " ~ ' " ~
l _ where. nota > 15
• where aluno.ld == notas.id " " " " "
' select new { aluno.Nome, aluno.ApelidOj nota }; ;

permite obter uma colecção com todos os alunos que tiram notas superiores a 15 valores
em alguma avaliação. Neste caso, está a tornar-se directamente visível na variável nota, a
classificação associada a cada elemento presente em notas.Notas. Por outro lado,
notas.Notas representa cada elemento que terá de ser iterado em avaliações. Vale a
pena estudar esta expressão com alguma atenção. O resultado da execução deste código:

© FCA - Editora de Informática


INTRODUÇÃO À UINQ

forêach "£?yar"boas i n "boasNõtasJ " ~""" ' "~ " „ • [ ' . . '
_ conso]e;WnteUneC"{0} _{l}\t{2}"_ í boas..Nome, .boas^Apelido, boas:."nota)
será
pedno Martins "" 16""" ~" ' > ".
Ana Ferrei rã 16
Maria Cardoso 18 :
Maria Ca.rdoso
Maria Cardoso
Do.ao'
3'oao
3oao Abreu
João Abreu

" Numa expressão de consulta, from permite especificar as fontes de dados a


ARETER usar.
~ from especifica uma fonte de dados (tipo enumerável), e uma variável que
LINQ - from representa um elemento dessa fonte de dados. Por exemplo:
from aluno i n alunos
" Numa expressão de consulta, pode-se utilizar diversas expressões com from
simultaneamente.
" É possível numa expressão from, utilizar um campo de uma variável
introduzida noutra expressão from. Por exemplo:
from aluno i n alunos
from notas i n ai uno.Notas

11.1.2 EXPRESSÃO WHERE


A expressão where, que temos estado a usar, permite filtrar o resultado, usando uma
expressão lógica. Por exemplo:
var alunossenior =" ~ " " " ' " ' " " ;. ..< ,., . _ :
from âlupo i n alunos ' - . ' " ' '
w|iej;e a,luno.idade > 2 3 . . / , . . .
select aluno;
permite encontrar todos os alunos com mais de 23 anos. É ainda possível especificar
várias expressões where, afectando diferentes partes da pesquisa:
var boasNotas = " " " • ,. * - *
Cl uno "i n alunos . . ~^ '^

ou combiná-las na mesma expressão lógica:

© FCA - Editora de informática 4O5


C#3.5

Como iremos ver adiante, existe uma outra expressão chamada group que permite
agrupar resultados, de acordo com um certo critério (por exemplo, calcular uma média de
notas, agrupando-as por idades). A expressão where pode aparecer antes ou depois de
uma cláusula group, dependendo se o objectivo é efectuar a filtragem dos elementos
antes ou depois de eles serem agrupados.

Em termos de compilação, a palavra-chave where é convertida numa chamada ao método


whereQ (na verdade, Enumerable.whereQ) do espaço de nomes System.Linq. Este
método tem como objectivo, efectuar filtros numa sequência de valores baseados num
predicado.

Numa expressão de consulta, where permite filtrar os resultados a obter de


ARETER acordo com um determinado critério (expressão lógica). Por exemplo:
where aluno.ldade>23
LINQ - where Numa expressão de consulta, pode-se utilizar diversas expressões where
simultaneamente. Alternativamente, pode combinar-se as várias expressões
numa condição lógica.

11.1J3 EXPRESSÃO SELJECT


Numa expressão de consulta, select especifica a forma e o tipo de valores que serão
produzidos quando a expressão for executada. O resultado é baseado na avaliação de
todas as expressões anteriores e nas expressões existentes no próprio select. Uma
expressão de consulta tem de terminar com sei ect ou group.

Os exemplos anteriores permitiram-nos, já, ver duas formas importantes de sei ect. Em:
var aTunosSenior = ~ " "" "' " "
from aluno i n alunos
where alunç.Idade > 23
í ~ select aluno: - - - . . . . . . . . ... - ~l
são retomados directamente todos os elementos aluno, depois de devidamente filtrados.
Não existe qualquer definição de novos tipos de dados. No entanto, em:
var" aluhoNotâs =
from aluno i n alunçs
from notas i n avaliações
where aluno.Id == notas.id
select new { aluno-Nome, aluno.Apelido, notas.Notas };
é criado um novo tipo de dados anónimo, contendo três campos. Ambas as formas de
utilização são perfeitamente lícitas. É ainda lícito emitir um tipo de dados pré-existente,
utilizando os campos disponíveis na expressão de pesquisa. Por exemplo:
var nomescompletos =
from aluno i n alunos
select ai uno.Nome + " " + ai uno.Apelido;
irá criar uma lista de alunos com o seus nomes completos.
4O6 © FCA - Editora de Informática
INTRODUÇÃO À LINQ

Em termos de compilação, a palavra select é convertida numa chamada ao método


selectC) (Enumerable. select Q) do espaço de nomes System. Linq.

" Numa expressão de consulta, select permite especificar quais são os dados
A RETER (elementos) a ser produzidos como resultado da pesquisa. Por exemplo:
. . . select aluno;
LINQ - select ... select new { ai uno. Nome, ai uno. Idade };
~ Um select pode emitir, tanto os dados da consulta, após filtrados, como
outros tipos de dados. Neste último caso, especifica-se um tipo de dados
pré-existentes a partir dos campos da consulta, ou cria-se uni novo tipo de
dados anónimo, contendo os campos de interesse.

1 1 . l .4 EXPRESSÃO GROUP
A expressão group permite agrupar elementos da pesquisa, de acordo com um certo
critério. Esses elementos podem ser previamente filtrados ou não. Vejamos um exemplo
simples:
var, animosidade' =
fróm aluno tn alunos
group aluno by aluno. idade;
irá retornar uma colecção correspondente aos alunos agrupados pela idade. Cada elemento
da colecção terá uma chave (a idade), contendo uma lista de alunos correspondendo a essa
idade. O seguinte código:
foreach- (var alunosPorídade In alúnõsidade)
corisoTè.Wri.teLineC"idade:
. {0}
Consol e. wríteLlne ("===—=====—=======================") ;
fdreéCch (vâr a] uno ~in alúnospòriclade)
Console. WríteLiheC" {0} {!}", aluno. Nome, aluno. Apelido) ;
Consol e. WriteLineQ ;
.} ' _ ..... .. ................ __________
irá imprimir os alunos nesse agrupamento:
Idade: 25
Maria -Carvalho
Maria Cardoso
aoao Ab-reu
Idade': 23
• Peâr,o .Martins
Idade: 20
Ana Ferreira

© FCA - Editora de Informática 4O7


C#3.5 _

Note-se que foi necessário utilizar dois foreach encadeados. O primeiro está a iterar
todas as idades encontradas. O segundo está a iterar todos os alunos correspondentes a
uma certa idade. Para aceder a cada uma das idades, utiliza-se a propriedade Key. Neste
caso: alunosPorldade.Key.

Vejamos mais um exemplo, este um pouco mais complexo. É possível especificar mais
do que uma chave para realizar o agrupamento. Para tal, é necessário utilizar um tipo
anónimo. O seguinte código:
vãr álunosNomeldáde - " " "" "" ......... ..... "" '
from aluno i n .alunos __ _ _ _ _ _ _ _ _ '
j gfpup.laTunõ. __by MW riaJujTo.í^ _II_ ITl"!_".Jl'.'_r.7 ~ ]
foreach (var alunosPorNomeldade i n alunosNomeldade)
{
Console. WriteLine("Nome: {0} " , alunosPorNomeldade. Key) ;
Console.WriteLi ne ("====================================") ;
foreach Cvar aluno i n alunosPorNomeldade)
Console. writel_ine(" {0} {!}", aluno. Nome, aluno. Apelido) ;
Console.WriteLineQ ;
'l .. ........................... .... . ... _ ........ . ..... .. ........ .
agrupa os alunos que têm simultaneamente a mesma idade e o rnesmo primeiro nome. O
resultado é:
Nome: {"idade" = 25, Nome = Maria } ""'
Maria Carvalho
Maria Cardoso
Nome: { idade = 23, Nome = Pedro }
Pedro Martins
Nome: { idade = 20, Nome = Ana }
Ana Ferrei rã
Nome: { idade = 25, Nome = João }
João Abreu
Em termos de compilação, a expressão group é convertida numa chamada ao método
GroupByQ (Enumerable.GroupByQ) do espaço de nomes system. Linq.

4O8 © FCA - Editora de Informática


INTRODUÇÃO À LINQ

~ Numa expressão de consulta, group permite agrupar o resultado de uma


ARETEK consulta, usando uma certa chave. Por exemplo, agrupar todos os alunos
por idade:
LINQ-group group aluno by aluno.Idade
~ Caso se queira agrupar usando uma chave composta, é necessário definir
um tipo anónimo que inclua todos os campos a introduzir na chave
composta. Exemplo:
group aluno by new { aluno.Idade, aluno.Nome }

• O resultado de uma operação de agrupamento consiste numa colecção de


elementos agrupados. Para cada elemento resultado, existe uma chave,
correspondente ao agrupamento, acedível usando a propriedade Key. Para
aceder aos elementos do agrupamento, é necessário iterar o elemento
correspondente à chave. Exemplo:
foreach (var grupoDeUmaldade i n alunosPorldade)
Console.writeLine(grupoDeUmaldade.Key);
foreach (var aluno in grupoDeUmaldade)
Console.Writel_ine(aluno.);

1 1 . 1 .5 EXPRESSÃO iisrro
Em muitos casos, ao usar-se LINQ, é necessário encadear pesquisas ou realizar
sub-pesquisas. A palavra-chave into permite definir uma variável temporária onde
resultados parciais podem ser armazenados. Tal é útil quando necessitamos, mais tarde, de
os utilizar numa outra expressão sei ect, group ou joi n, ou mesmo quando é necessário
realizar filtragens intermédias.

A título de exemplo, a seguinte expressão permite agrupar os alunos por idade, filtrando,
no entanto, todos os grupos que tenham menos de 3 pessoas:
-var a-lunosldade = '.........."
fronralurio in alunos _ -
group aluno by ai uno. Idade íntçTa] un_b_sN_QGrupp
, where --alunosNoGrupo.countQ >= 3
i select alunpsNoGruppj ________ ______ ...... _ _ ....... ,.
Obviamente, é possível combinar expressões criando consultas mais complexas. No próximo
exemplo, os alunos são agrupados por idade, considerando-se, apenas, os grupos que têm
uma representatividade superior ou igual a 10%. Após terem sido criados os grupos,
apenas se obtém o resumo de cada grupo, criando um tipo abstracto contendo a idade
(chave do grupo), total de pessoas com essa idade e a percentagem correspondente.
A/ar é^taiti^ticaldades"^' ~ "" ..... ' "" """ ....... " .- *
n alunos ..' X , ' /'
by ai uno. Idade into grupo - , .
!countO >= 0.10*alunos.CountO , ' . •
seTecfe-iTew
Idade = _grypp.-!<£y,
© FCA - Editora de Informática 4O9
C#3.5

Totalpessoás = grupo:.,cou.ptO 5 ' "•


Percentagem = l'OÕ,0^grõi)ò.'Co"untO/aTunos.,CountO
'' •
Ao executar:
:Cbhsòlé".wr[tèLineC"ídàdè"\ # \ %")"; " ~" "~"
Xonsole.WrlteLlneC11—====—====—"======'') ;
•foreach (var grupo In estatisticaldades)
Console.Wr1teLlne("{0} \ {1} \ {2}%",
grupo.Idade, grupo.Total pessoas, grupo.Percentagem);

surge:
:ídade -r %
•25 3 60%
,23 1 20%
2Q 1 20%
A utilização de into em conjunto com group é apenas necessária, quando se quer
executar operações de consulta adicionais sobre cada grupo.

Numa expressão de consulta, 1 nto permite armazenar os resultados de uma


ARETER consulta numa variável temporária. Consultas subsequentes podem usar
essa variável como se fosse uma fonte de dados normal.
LINQ - into i nto é usada quando é necessário usar dados intermédios noutra expressão
select, group ou join, ou mesmo quando é necessário realizar filtragens
intermédias.

11. l .6 EXPRESSÃO ORDERBY


A palavra-chave orderby permite ordenar os elementos que estão a ser seleccionados
numa expressão de consulta, de forma ascendente ou descendente, usando uma
determinada chave de ordenamento. Por exemplo:
var alunõsordenados'=
from aluno i n alunos
orderby aluno.Nome
'_- select, aluno; _ _ _ _
ordena todos os alunos por ordem ascendente de primeiro nome. Conjuntamente com
orderby, pode-se utilizar as palavra-chave ascending e descendi ng. Como o nome
indica, estas permitem especificar a forma como é feito o ordenamento. Usando o
seguinte código:
var" alúnosordenados = " " ~
from aluno In alunos
orderby ai uno.Nome descending
select aluno; __ __
os alunos seriam ordenados por ordem descendente de apelido.

410 © FCA - Editora de Informática


INTRODUÇÃO À U NQ

É também possível especificar diversas chaves de ordenação, separando-as por virgulas.


Por exemplo, pode-se ordenar os alunos por apelido e em seguida por nome:
.var alunosordenados;"= '~~ """"
frqm aluno in : alunos ^
'ordenby' álUno.Apelido, aluno.Nome as;i
s el e'ct /aluno; ^ -.....
Em termos de compilação, orderby é convertido numa chamada ao método orderByQ
(Enumerable.orderByO) do espaço de nomes system.Linq. Caso estejam presentes
múltiplas chaves, estas são convertidas em chamadas ao método TheneyO
(Enumerable.ThenByQ).

- Numa expressão de consulta, orderby permite ordenar os resultados de


ARETER uma pesquisa, usando urna determinada chave. Os resultados podem ser
ordenados de forma ascendente (ascendi ng) ou descendente (descendi ng).
LINQ - orderby Por exemplo;
orderby aluno,Nota descending, aluno.Apelido ascending
~ Por omissão, a expressão orderby ordena os elementos de forma
ascendente.

11.1.7 EXPRESSÃO JOIN


Num exemplo anterior, vimos que era possível combinar informação de diferentes fontes
de dados. Ao escrever-se:
var val:unoNotas =: "
from aluno i n alunos
frorn' notas i n avaliações
- whéne::aluno..ld == notas. id
seleet. new { aluno^Nome., ...aluno.. Apelido , ai uno.; Idade , notas. No^as };

obtém-se uma colecção contendo, para cada aluno, as suas notas correspondentes. Isto,
apesar dessa informação estar armazenada em duas tabelas diferentes (alunos e
aval i acoes). A este tipo de operação chama-se vima. junção.

Estritamente falando, a expressão de consulta acima não deve, regra geral, ser escrita
desta forma. Deve ser escrita usando uma expressão joi n:
var . . .
from aluno i n_ alunos ___ _ _ ____ ' **'*'"'_'_"
[ " jòin nota 'J n" avaTiacQ|s _pjn aljJQpI^Id ecfuàTs~ nota', í ã \. ,........" _ _
selecÇ new/{ aluno, Nome, aluno. Apelido, alunò.ldade, nota. Notas };

O resultado da impressão de ai unoNotas, tal como anteriormente, é:


Maria 'CaWalho ~ "" ' " " ..... ~~' ~ *" '• * - V-
12 13 14 13-
Pedro Martins
15 14 16 15 - -
Ana Ferreira
10 12 14 16 ......... _________ ........ _. „. . _____ ._ ..... .

© FCA - Editora de Informática


C#3.5

Man'à "Cardoso" , ~ - .-
15 18 17 18 ,,,-•-
•aoap Abreu '' v^*"
:i9 19.18..17 „. • . _ - i . . . - „_ . '
Embora o resultado final seja idêntico ao da primeira expressão, esta última é tipicamente
muito mais eficiente. O compilador sabe explicitamente que se está a cruzar dados de
duas fontes diferentes. Note-se que o formato de uma expressão j o i n é:

join variável -i n fonteDeDados on chovei equals chave2

tal como quando escrevemos:


join nota in avaliações on aluno.id equals nota.ld

Na expressão, variável representa a instância de fonteDeDados sobre a qual queremos


fazer a junção, chovei representa uma propriedade da fonte de dados originária de f rom;
chave2 representa a variável da segunda fonte de dados (fonteDeDados').

Existem três formas principais de junção em C#:

Equijunção. Uma equijunção junta elementos de duas fontes de dados diferentes,


usando um critério de igualdade entre elementos. Na primeira fonte de dados, tem
de existir um campo (chave) que identifica cada elemento, associando-o à
segunda fonte de dados. Esta segunda fonte de dados tem. de possuir um elemento
com uma chave do mesmo tipo. Todos os elementos cujas chaves sejam idênticas
são incluídos no resultado. Elementos que estejam presentes apenas numa das
fontes de dados são excluídos. O exemplo anteriormente apresentado é uma
equijunção^ para cada aluno existe um registo de avaliações;

• Junção de grupo. Uma junção de grupo permite criar um resultado hierarquizado


em categorias (isto é, grupos). Imaginemos que cada aluno possui um identificador
extra que especifica uma disciplina que frequenta. Existe ainda uma outra tabela
que contém a informação relativa às características da disciplina (nome, sala, etc.)
Uma junção de grupo permite obter uma listagem dos alunos por disciplina.
Alternativamente, se um aluno pertencer a várias disciplinas, permite obter uma
listagem de todas as turmas a que um aluno pertence. O resultado de uma junção
de grupo é essencialmente uma colecção de tabelas de objectos. (Para os leitores
experientes em SQL, este tipo de junção pode parecer algo estranha. Na verdade,
não tem correspondência no modelo relacional.) Ao realizar-se uma junção de
grupo, caso uma categoria não possua elementos, a colecção correspondente
estará vazia;

412 © FCA - Editara de Informática


INTRODUÇÃO À LINQ

Junção externa esquerda1. Numa junção externa esquerda, todos os elementos


da primeira fonte de dados são retornados, mesmo quando não exista um
elemento correspondente na segunda fonte de dados. Isto é útil, por exemplo, se
estivermos a obter uma listagem das disciplinas a que todos os alunos estão
inscritos. Se um aluno não estiver inscrito em nenhuma disciplina, mesmo assim,
gostaríamos que o mesmo aparecesse na listagem final. Isso não acontece se
estivermos a fazer uma equijunção.

A fim de ilustrar os dois tipos de junção que ainda não vimos (junção de grupo e junção
externa esquerda), consideremos as seguintes colecções:
vãr alunos = new~ATúrió[] urión
new'. Al uno Notne= 'Maria", Apel i do="carval no" , Idade=25, Ic
ldDisc=l},
new Aluno Nome= 'Pedro", Apelido="Martins", 3:dàde=23, ic
new Alunô.{ld=3, Nome= 'Ana" , Apelido="Ferrei rã" , ldade=20J Ic
new Aluno, Nome= "Maria", Apelido="Cardoso", id,ade=25J ÍdDisc=3},
Ic
new Ali u no Nome= 'João", A P e Ifl^jP^^htl^-U^' ' , Idã.de=25} Xe
XdDisc=3},
new Aluno Nome= 'Rita", Apellíffi^^^pQZ11 , ldade=35, Ic
ldDisc=9}

var disciplinas = new Disciplina[]

new Díjáciplina { Id=l, Nome-"Mateniatica"


new Djãc-iplina { ld=2, Notné="Fis-ica"
new Disciplina { Id=3, Nomè="Quitnica" },
new^D-j^ciplina { id=4, No*nie.=" Historia" },
, néW' Disciplina { id=5, Nome^" Biologia" },

Note-se que a tabela D i s c i p l i n a contém disciplinas às quais os alunos não estão


inscritos (História e Biologia). Existe também uma aluna (Rita Queiroz) que está inscrita
a uma disciplina que não está na tabela de disciplinas (idDisc=9). Finalmente, é de
salientar que a propriedade que identifica a disciplina chama-se idoisc, na primeira tabela
e id na segunda.

Suponhamos que queremos obter a lista de alunos inscritos a cada uma das disciplinas.
Para isso, usamos uma junção de grupo:
^vãr_alundsPprDlscjpJTna_=_ _"__ _'1~_'~_" _""~ l "~"I _ " ! " " _ _ i.
frõm"cíTsc'Tn "disciplinas
join aluno in alunos on disc.Id equals aluno.Idoisc into alunoNaDisc
sei ect. new í pisclpJJaaf=di>SjC.J^pmeJ_Al_unos=a_]unpNaplsç_iJ. - _
JVaV.disc i n alunosporoisciplina) . , . •
L •-
, dl se. Di sei pi i na) ; '-
"(var. aluno in disc. Alunos) "t
{0} {!}", aluno. Nome, aluno.ApelidoO';

1 Do inglês, "left outerjoin".


© FCA - Editora de Informática 413
C#3.5

Na expressão de consulta, começamos por obter a lista de disciplinas, adicionando-lhe a


informação sobre os alunos inscritos nas mesmas. À semelhança de uma equijunçao,
utiliza-se as chaves correspondentes e a palavra-chave equals. A única diferença, que
indica que esta consulta é uma junção de grupo, é a utilização da palavra-chave into. Ao
utilizar esta palavra-chave, os elementos são automaticamente agrupados, usando como
categorias, a primeira fonte de informação. O resultado da execução é:
sMatemati ca
: Maria Carvalho
, Física
: Pedro Martins
' Ana Ferreira
Quimica
• Maria Cardoso
! João Abreu
ÍHistoria
iBiolocjia _ ... .
Como se pode ver, trata-se de uma hierarquia de objectos. É de salientar que as colecções
correspondentes a "História" e "Biologia" encontram-se vazias.

No exemplo acima, existe ainda um aspecto curioso: a aluna Rita Queiroz, que se encontra
inscrita numa disciplina desconhecida, não surge nos resultados. Consideremos, então, uma
expressão de consulta que tenta listar todos os alunos e as disciplinas correspondentes:
iyar álunopiscfplina = _ " __ "_ ' _ "__ l
" "frõm"aluno i"ri""alunòs" " ~ " "
join disc i n disciplinas on aluno.ldoisc equals disc.ld
select new {_ aluno. Nome, ai uno. Apelido, _Disciplina=disc.Npme };
:foreach (var aluno i n alunooisciplina)
;{Console.WriteLine("{0} {1} \ {2}",
ai uno.Nome, ai uno.Apeli do, ai uno.Di sei pi i na);
J ...... _ . . . . .
O resultado é:
'Maria "Carvalho "Matemática"
Pedro Martins Física
•Ana Ferreira Fisica
Maria Cardoso Quimica
3oao Abreu Quimica
Ou seja, Rita Queiroz não aparece. Isso deve-se ao facto de estarmos a usar uma
equijunçao. Para se conseguir que a Rita surja na listagem, tem de se utilizar uma junção
externa esquerda. Este tipo de junções garante que mesmo que não exista um elemento na
segunda fonte de dados (fonte direita), todos os elementos da primeira fonte de dados
(fonte esquerda) são mostrados.

As junções externas esquerdas escrevem-se de forma semelhante às junções de grupo. A


principal diferença é a utilização do método DefaultifEmptyC). Este método permite
especificar qual o valor direito a usar quando a fonte esquerda não possui valores.
414 © FCA - Editora de Informática
INTRODUÇÃO À UNQ

Pode-se utilizar os tradicionais n u l l e O s quando ta] faça sentido, ou outro valor que se
queira. Regressando ao exemplo anterior, a junção externa esquerda poderia ser escrita da
seguinte forma:
Vá r aluhoDisciplina =
from aluno In alunos
join disc In disciplinas on aluno^ldoisc equals disc.Id into alunopisc
from ficha irí alunóDisc.DèfàultífEmptyC
new Discip]nna { id=-lA Nome = "—piscioVina Desconhecida--" })
select new {"aiuno.Nome, aluno.Apelido, plsciplinã-ficha.Nome };

Examinemos esta expressão com cuidado. Após ter-se feito a junção de grupo, cujo
resultado é colocado em ai unoDi se, é feita uma nova consulta que a utiliza como fonte.
Nessa consulta, ao fazer-se alunoDisc.oefaultlfEmptyC.. O sobre a fonte de dados,
todas as entradas vazias associadas irão ser substituídas pelo objecto que é passado como
parâmetro. Neste caso, uma disciplina com identificador -l e um nome desconhecido. Ou
seja, no fundo, funciona da mesma forma que uma equijunção mas em que especificamos
um valor a usar, caso não seja encontrado nenhum do lado direito. O resultado da
impressão de alunooisciplina será:
Maria Carvalho Matemática
Pedro Martins F1s1ca
'Ana Ferreira Fisica
Maria Cardoso Química
João Abreu Quimica
Rita Queiroz --Disciplina Desconhecida--
Como se pode ver, Rita Queiroz já se encontra na listagem.

Para terminar, existem ainda dois pontos importantes a considerar. Em primeiro lugar, ao
realizar-se junções, as chaves podem ser compostas. Isto é, a chave pode consistir num
conjunto de atributos que, como um todo, identifica univocamente um elemento. Por
exemplo, admitindo que todas as pessoas são univocamente identificadas por um par
[nome, apelido], sendo esses elementos parte de alunos e de avaliações, uma
equijunção simples poderia ser expressa por:
vár alunoNotas =
from aluno i n alunos
join nota i n avaliações
on riew { aluno.Nome, aluno.ApèTidò } |
equals new equals { nota.Nqmej nota.Apelido } . ._ _ |
select new { alúno._Nome.,_ ai uno ..Apelido,_ ai uno. Idade, nota. Notas }; . ;
Quando se usa chaves compostas, têm de ser definidos tipos anónimos que caracterizam
essas chaves. As propriedades associadas são comparadas uma-a»uma, pela ordem em que
estão definidas. Necessitam também de ter o mesmo nome, caso este exista.

O segundo ponto importante é que é possível fazer junções sobre um número arbitrário de
fontes de dados. Tudo o que é necessário, é colocar diversas expressões join. E também
possível realizar operações que não são equijunções. Por exemplo, se fosse necessário
classificar qualitativamente os alunos em termos das suas notas, havendo intervalos para
© FCA - Editora de Informática
C#3.5

"Muito Bom", "Bom", "Suficiente" e "Fraco", numa tabela separada, isso não seria
possível, usando uma equíjunção. Quando tal acontece, é necessário escrever a expressão
de consulta em termos de f rom, operações where e groupby, tal como foi discutido nos
primeiros exemplos. A expressão j cri n apenas existe de forma ao compilador poder
optimizar as equijunções e junções de grupo, que são os casos mais frequentes.

Em termos de compilação, a cláusula jcrin, quando não é seguida pela cláusula into, é
convertida numa chamada ao método Join Q ( E n u m e r a b l e . u o i n O ) do espaço de nomes
System.Linq. Quando é seguida pela cláusula Into, é convertida numa chamada ao
método G r o u p u o i n Q ( E n u m e r a b l e . G r o u p J o i n O ) do espaço de nomes System. Li nq.

Numa expressão de consulta, join permite realizar uma operação de junção


ARETER com outra tabela. A junção é baseada em igualdade de chaves. Por exemplo:
join nota i n avaliações on aluno.ld equals nota.Id
LI N Q - join Equijunções combinam, num resultado, duas fontes de dados, usando a
igualdade entre duas chaves. Correspondem a um join simples.
Junções de grupo combinam duas fontes de dados, usando igualdade entre
duas chaves, mas hierarquizando os resultados de acordo com uma
categoria. Correspondem à utilização simultânea de um join e uma
cláusula into.
Junções externas esquerdas são equivalentes a equijunções de grupo mas
tendo a particularidade de todos os elementos da primeira fonte de dados
serem incluídos no resultado, independentemente de haver uma chave
correspondente na segunda fonte de dados ou não. Para especificar o valor a
usar na segunda fonte de dados, caso não exista chave corresponde, utiliza-se
o método DefaultlfEmptyO-
As chaves usadas nas junções podem ser compostas. Para isso, define-se
tipos anónimos que especificam os campos a usar como chave.
Certas junções não podem ser descritas com a palavra-chave join. Nesses
casos, utiliza-se expressões where para realizar a unificação a partir das
diversas fontes de dados.

11. l .8 EXPRESSÃO LET


Nas expressões de consulta, por vezes, é útil armazenar os resultados de uma consulta para
ser utilizada em consultas subsequentes. Para tal, utiliza-se a palavra-chave l et. l et cria
uma variável local, inicializando-a, usando os resultados de uma consulta. Após a variável
ser inicializada, não poderá armazenar outro tipo de resultados. No entanto, poderá ser
usada nas consultas seguintes.

Consideremos uma expressão de consulta que imprime a média final das notas de cada
aluno:
ívar notasFinais =
416 © FCA - Editora de Informática
INTRODUÇÃO À LINQ

trom aluno in alunos


join nota In avaliações on aluno.Id equals nota.ld
select new
{ Nome=aluno.Nome, Apelido=aluno.Apelido,
Medi a=Math. Rpund(nota. Notas. AverageQ)__}; _
Console.writel_ine("Nome \ Apelido \ Média");
consol e. writei_ine("=====~=================—======") ;
foreach (var aluno In notasFlnals)
console.Wr1teLlneC"{0} \ {1} \ {2}",
ai uno.Nome, ai uno.Apel1 do, ai uno.Medi a);
} .„ . ... _.. . . . . . ..
O resultado a execução deste código será:
Nome Apelido Média
Maria Carvalho 13
Pedro Martins 15
Ana Ferreira 13
Mana Cardoso 17
João Abreu 18
Nesta expressão, o único elemento novo éMed1a=Math.Round(nota.Notas.AverageQ).
Chama-se a isto uma operação de agregação. Dada uma colecção, é feita uma operação
que calcula um resultado que usa todos os elementos associados.

Imaginemos, agora, que queremos ver, não apenas a média final, mas também as notas
classificadas de forma qualitativa. Existe uma tabela que, para cada intervalo de médias,
diz se o aluno é insuficiente, suficiente, bom, muito bom ou excelente:
var escalaQualitativa = new intervalo[J
new Intervalo { classe= 'Excelente", Inf=18, Sup=20 },
new Intervalo { classe= 'Muito Bom", Inf=17, Sup=17 },
new Intervalo { classe= "Bom", Inf=14, Sup=15 },
new Intervalo { classe= 'suficiente", Inf=10, Sup=13 },
new Intervalo { classe= 'insuficiente", Inf=0, Sup=9 }

Nesta definição, intervalo é uma classe trivial, possuindo apenas três campos: a classe a
que o aluno pertence, o limite inferior da classificação associada e o limite superior da
classificação da classe.

Embora seja possível alterar a expressão de consulta anterior, para formatar os alunos, de
acordo corn a tabela qualitativa, tal não é trivial. No entanto, usando a expressão l et, tal é
relativamente simples:
var notasFlnals =
from notas i n avaliações
l et media = Math.Round(notas .Notas .AverageQ)
from intervalo in escalaQualitativa
where (media >= intervalo.lnf) && (media <= intervalç.sup)
let registo = new {l^notas^Id,, Me_di>=mediaJ çlasse==intervalg.Classe}.
© FCA - Editora de Informática 41 "7
C#3.5

from aluno In alunos,/ * : - • - ' !^.\


! where registoAlun?"J:d = aluno.ld :
seTect new
{ aluno.Nome., aluno.Apelido, registqAl_uno.Media, registo.classe__}_;
Esta expressão tem três partes que, por uma questão de clareza, foram separadas por
linhas em branco. A primeira parte especifica com fonte de dados a tabela avaliações.
Para cada uma das entradas (notas dos alunos), é calculada a média arredondada às
unidades. Usando uma expressão l et, essa média é guardada numa variável media, que
será usada nas expressões posteriores. De seguida, encontra-se o intervalo correspondente
à média, usando como fonte de dados, a tabela escalaQualitativa. Isto consegue~se,
filtrando todos os elementos dessa tabela nos quais a média não está compreendida. Em
seguida, cria-se uma variável registo que contém três campos: o identificador do aluno
(notas. Id), o valor da média (variável medi a) e a classe de classificação em que o aluno
se encontra (intervalo.classe). O ponto interessante é registo representar uma
colecção que pode ser usada com fonte de dados. Assim, tudo o que falta fazer é uma
equijunção em que o identificador, média e classe de classificações é cruzada com a
informação do aluno, criando os registos finais. Assim, ao executar o código:
console.writeLine("Nome \ Apelido \t"Média \ classe");
;console.WriteLine("===========================================");
foreach (var aluno in notasFinais)
console.writel_ine("{0} \ {1} \ {2} \ {3}",
aluno.Nome, aluno.Apelido, aluno.Media, aluno.classe) ;:

surge:
Nome Apelido Medi a Classe
Maria Carvalho 13 suficiente
Pedro Marti n s 15 Bom
:Ana Ferreira 13 suficiente
Maria Cardoso 17 Muito Bom
João Abreu 18 Excelente
Embora a clausula l et seja bastante útil e, muitas vezes, permita escrever mais facilmente
expressões de consulta, esta deve ser usada com alguma contenção. Em muitos casos, este
tipo de expressões implica uma determinada ordem de execução e algumas restrições nas
operações que estão a ser feitas. Assim, torna-se algo mais difícil ao compilador,
optimizar as expressões para que executem o mais eficientemente possível. Tal poderá
não ser muito importante quando se trabalha com colecções em memória mas, quando se
usa bases-de-dados, a penalidade pode ser grande. As bases de dados possuem
optirnizadores de consultas que, ao utilizar-se variáveis l et, podem ter o seu papel
bastante dificultado.

418 © FCA - Editora de Informática


INTRODUÇÃO À LINQ

- Numa expressão de consulta, l et permite criar uma variável local que pode
ARHTER ser usada como fonte de consultas subsequentes. Por exemplo:
let media = alunos.Notas.AverageQ
L NQ - let _ 1jma Yez defino Q valor de uma variável, o mesmo não pode ser alterado.

11.1.9 OPERAÇÕES GENÉRICAS E DE AGREGAÇÃO


Uma funcionalidade importante introduzida em C# 3.0 foi métodos de extensão. Mas,
mais importante do que a funcionalidade em si, foi o facto da Microsoft ter incluído um
número bastante grande destes métodos em todas as APIs da plataforma .NET. Assim,
tornou-se possível realizar, da mesma forma, muitas operações genéricas, em todo o tipo
de objectos. Por exemplo, é bastante simples contar elementos, encontrar máximos,
mínimos e médias de valores presentes em colecções, assim como obter reuniões e
intercepções de conjuntos de objectos. Quando esta funcionalidade é usada em conjunto
com expressões de consulta, torna-se extremamente poderosa.

Para começar, consideremos um exemplo simples. Ao fazer-se;


int .total Al unos = alunos,CountO ; . . . . . .
console. wHteLine("Númerq .de alunos: {0}.",. totalAlunos) ; ,
obtém~se o número total de alunos presentes na tabela alunos. O método countQ é um
método de extensão.

Imaginemos, agora, que queremos obter a média de idades de todos os alunos presentes
nesta colecção. Uma abordagem possível seria iterar ao longo de todos os elementos da
colecção, acumulando as idades num contador, dividindo o total pelo número de alunos.
No entanto, em LINQ, é possível escrever simplesmente:
double medialdades =
Cfrom aluno i n .alunos select aluno, idade) .AverageQ ; . :
O método de extensão AverageQ permite calcular directamente uma média, a partir do
resultado de uma expressão de consulta (ou dos elementos de uma colecção).

Obviamente, estes métodos de extensão podem ser também usados dentro de expressões
de consulta. Ai reside o seu poder. Por exemplo, suponhamos que dada a tabela de alunos,
pretendemos saber quantos alunos existem por idade. Para tal, basta escrever:
var alunosPoridade =
from aluno i n alunos
group aluno by aluno. Idade into gruposldade
s_elect new { JEdade-grupps.Id_ade..Key^ jotal=gruppsidad_e..ÇpuntD
\ Alunos").;
consoTèvWríteLi nè ("========—=====") ;
r- grupo In alunosporldade)
- , . .
console. WríteLine("{0} \ {!}", grupo. idade, grupo. Total) ;
} .......... . _ _ . . . .
© FCA - Editora de Informática 41 9
C#3.5

Isto é, dado o conjunto de alunos, estamos a agrupá-los por idade. Após termos obtido os
grupos, estamos a criar um resultado contendo os pares [idade, número de pessoas com.
essa idade}. O resultado da execução será:
[idade " "Alunos
25 3
123 l
: 20 l
;35 . . . l .„
Dando um exemplo de uma consulta ligeiramente mais complexa, o código:
var resumoNÕtas =
from aluno In alunos
join nota i n avaliações on aluno.Id equals nota.ld
orderby aluno.Nome
select new
Nome=aluno.Nome, Apeli do=aluno.Apel1 do,
Max=nota.Notas.Max(), Min=nota.Notas.Min(),
Medi a=nota.Notas.Ave rage C)
ji
Console.WriteLine("Nome \ Apelido \ Máximo \ Mínimo \ Média");
Console.WriteLine("===========================-============—========='');
foreach (var aluno i n resumoNotas)
Console.wntel_ine("{0} \ {1} \ {2} \ {3} \ {4:F1}"(
aluno.Nome, aluno.Apelido, aluno.Max, aluno.Min, ;
ai uno.Medi a); :
l .. . .
permite obter uma listagem de alunos. Esta listagem mostra, para cada um deles, a nota
máxima, mínima e média. A listagem é ordenada pelo primeiro nome dos alunos:
Nome Apelido" Máximo " Mínimo Média
Ana Ferreira 16 10 13.0
goao Abreu 19 17 18.3
Maria Carvalho 14 12 13.0
Maria Cardoso 18 15 17.0
!pedro Marti ns 16 14 15.0
Uma outra operação bastante útil é a countQ. Sem parâmetros, permite contar o número
de elementos de uma colecção. No entanto, se levar um parâmetro, este permite
especificar uma condição para os elementos serem incluídos na contagem. Por exemplo:
Tnt boásMedfas = "
(from notasAluno in avaliações select notasAluno.Notas.AverageQ)
.CountCmedia =>. media > 17.0 ? true : false);
encontra o número de alunos que tiveram média superior a 17 valores. E de notar o uso de
uma expressão lambda, o que é bastante comum em chamadas whereQ e countO- Como
se pode ver, com uma sintaxe extremamente concisa, consegue-se exprimir operações
algo complexas.

42O © FCA - Editora de Informática


INTRODUÇÃO À LI NQ

Uma outra operação comum consiste em obter os elementos distintos de uma colecção.
Para tal, utiliza-se o método Di s ti nct Q. Por exemplo:
var apeTidos = (from .aTuno in alunos~seTect aiuncKApelido) .DistíriçtO;
pemiite obter uma lista dos apelidos únicos dos alunos.

A tabela seguinte mostra a lista de principais operações de uso comum em LINQ.


Recomendamos vivamente uma exploração mais profunda deste tópico, uma vez que
estas operações permitem simplificar imenso a vida do programador.
OPERAÇÃO | DESCRIÇÃO
Aplica uma função de agregação entre cada elemento de uma colecção e uma
Agregate
variável de acumulação, retornando o resultado.
Retorna true/ se todos os elementos da colecção satisfizerem uma condição
Ali
especificada,
Retorna true, se pelo menos um elemento da colecção satisfizer uma condição
Any
especificada.
Average Calcula a média dos valores da colecção.
Contai ns Verifica se a colecção contém um determinado elemento.
Conta o número de elementos de uma colecção ou o número de elementos que
count
satisfazem uma determinada condição.
DT s ti n et Obtém os elementos distintos de uma colecção.
Obtém o primeiro elemento da colecção ou o primeiro elemento que satisfaz uma
Fi rst
determinada condição.
Obtém o último elemento da colecção ou o último elemento que satisfaz uma
Last
determinada condição.
Semelhante a Count/ mas permite o tratamento de colecções extremamente
LongCount
grandes (usa Int64).
Max Retorna o maior elemento da colecção.
Min Retorna o menor elemento da colecção.
ofType Retorna todos os elementos gue pertençam a um certo tipo de dados.
Reverse Inverte a ordem dos elementos de uma colecção.
Single Retorna um elemento (qualquer) da colecção.
skip Ignora os primeiros N elementos de uma colecção, retornando os seguintes.
Ignora os primeiros elementos de uma colecção, enquanto uma determinada
SkipWhile .
condição especificada for verdadeira.
sum Calcula a soma de todos os elementos da colecção.
Take Retorna os primeiros N elementos de uma colecção.
Retorna os primeiros elementos de uma colecção, enquanto uma determinada
TakeWhile
condição especificada seja verdadeira.

Tabela 11.2 — Principais operações de uso comum disponíveis em LINQ

A tabela seguinte mostra as principais operações existentes, mas agrupadas por categoria.
Novamente, recomenda-se uma exploração aprofundada das mesmas.
© FCA - Editora de Informática 421
C#3.5

CATEGORIA OPERAÇÕES
Filtragem OfType, Where
Projecção Select, SelectMany
Partição Skip, Skipwhile, Take, Takewhile
Junção GroupJoin, 3oin
Concatenaçao Concat
Ordenamento OrderBy, orderByoescending, Reverse, Thensy,
ThenByoescendi ng :

Agrupamento GroupBy, ToLookup


Conjuntos Distinct, Except, Intersect/ Union
Conversão
ASEnumerable, AsQueryable, cãs t, ToArray,
ToDictionary, ToList
Igualdade SequenceEqual
Elementos
ElementAt, ElementAtOroefault/ First, FirstOrDefault,
Last, Lastoroefault, single, singleoroefault
Geração DefaultlfEmpty, Empty/ Range, Repeat ,;
Quantifícadores Ali, Any, contai ns
Agregação Aggregate, Average, Count, LongCount, Max, Min, Sum
Tabela 11.3 — Principais operações, categorizadas, em LINQ

" As operações de agregação realizam uma operação sobre todos os objectos


ARETEI de uma colecção, ou objectos que satisfaçam uma determinada condição.
Por exemplo: MaxQ, Min(), AverageQ, countQ.
LINQ-
Agregaçao
" Estas operações são implementadas como métodos de extensão, fazendo
parte da maioria das classes .NET.

11.2 ARQUITECTURA LJNQ


O LINQ é um modelo de programação que introduz expressões de consulta como
conceito fundamental. Um ponto forte do LINQ é poder tratar todo o tipo de dados, de
forma uniforme, independentemente da sua origem. O LINQ corresponde a uma sintaxe
declarativa de consulta que pode ser aplicada a colecções de objectos em memória,
documentos XML, base-de-dados SQL, ou outras fontes de dados. Na verdade, tudo o
que é necessário é existir um adaptador que relacione os tipos de dados LINQ com as
fontes de dados reais. A figura 11.1 ilustra o princípio.

42a © FCA - Editora de Informática


INTRODUÇÃO À LI NQ

Visual Basic (outras linguagens}

.NET LINQ (Language Integrated Query]

LINQ para LINQ para LINQ para LINQ para LINQ para
Objectos DataSets SQL Entidades XML

Figura 11.1-Arquitectura LINQ

Neste momento, existem os seguintes componentes implementados em LINQ:

LINQ para Objectos. Corresponde ao tipo de exemplos que temos estado a


examinar até agora. Quaisquer colecções, tabelas e conjuntos de objectos que
implementem a interface lEnumerabl e suportam automaticamente expressões de
consulta. Como tal, suportam LINQ;

LINQ para DataSets. ADO.NET corresponde a um conjunto de componentes que


permitem aos programadores, acederem, de forma simples, a dados persistentes, a
partir da plataforma .NET. A classe oataset é central à arquitectura ADO.NET,
representando, em memória, um conjunto de dados que, tipicamente, residem
externamente à plataforma (por exemplo, numa base-de-dados ou num ficheiro
XML). Em LINQ, existe um adaptador que permite escrever, de forma
transparente, expressões de consulta que têm subjacentes objectos oataset (parte
do sistema ADO.NET);

LINQ para SQL. Permite escrever expressões de consulta que têm subjacentes
dados presentes numa base de dados relacional. Actualmente, apenas é suportado
Microsoft SQL Server. No entanto, outras empresas encontram-se a escrever
adaptadores para as suas bases de dados;

LINQ para Entidades. Recentemente, houve um grande interesse numa área


chamada mapeamento objecto-relacional ("O/R Mapping"}. Basicamente, usando
ficheiros, é feito o mapeamento de tabelas e campos de uma base de dados para
objectos. Um exemplo bastante conhecido é a framework Hibernate. Os
mapeamentos que o programador define formam o que se chama EDM (Entity
Data Model). No entanto, esse modelo é lógico, não físico. A partir da versão 3.0
de ADO.NET, é possível expor uma vista conceptual dos dados, incluindo
relações, sendo estas definidas em termos de "entidades". Usando LINQ, é
possível pesquisar e manipular a informação presente nessas entidades;

© FCA - Editora de Informática 423


C#3.5

LINQ para XML. Permite escrever expressões de pesquisa que têm subjacentes
dados que se encontram armazenados em ficheiros XML.

Dada a extensão da arquitectura LINQ, é-nos impossível abordá-la em profundidade neste


livro. Nas secções seguintes, discutiremos apenas, de forma rnuito resumida, LINQ para
SQL e LINQ para XML.

11.2. l LJNQ PARA SQL


Para ilustrar o uso de LINQ para SQL, iremos utilizar uma base de dados muito simples,
chamada NorthWind. Esta base de dados é disponibilizada pela Microsoft para efeitos de
demonstração, podendo ser descarregada a partir de http://msdn.microsoft.com. O ficheiro
que iremos usar é o "NorthWnd.mdf. É ainda necessário ter o SQL Server instalado2.

Quando se usa LINQ para SQL, é necessário mapear as tabelas e relações existentes na
base de dados para classes, na linguagem de programação. Isso pode ser feito de quatro
formas diferentes:

Usando a ferramenta SqlMetal;

Usando o modo gráfico do VisuáLStudio.NET\o classes com atributos que especificam as

Usando ficheiros XML.

Nesta secção, iremos ilustrar estas quatro formas de interagir com a base de dados,
realizando uma consulta muito simples. Dados todos os clientes presentes na base de
dados NorthWind, encontrar aqueles cujo nome começa pela letra "A":
NòrthWrid nw = . . . // A classe NoTthWnd representa á"basè-dé-dãdos
var"'custò'râerfíaniês"= ~ "~" ~" " ~
from customer in nw.customers
where customer.CompanyName.StartsWlth("A")
seleçt eustomer,CqmpanyName;
foreach (var customer In customerNames)
Çonsole..Wr1teLineC"{0}_") _custqmer) ;
O resultado será:
Ana TrujilIo Emparedados y héladós
António Moreno Taquería
; Around the Horn

2 No nosso caso, utilizámos o Microsoft SQL Server 2005 Express Edition.


424 © FCA - Editora de Informática
INTRODUÇÃO À LINQ

Northwnd é uma classe que representa a base de dados, enquanto fonte de dados. Esta
poderá ser gerada manualmente ou automaticamente, das quatro formas diferentes
mencionadas. Ou seja, após sabermos escrever expressões de consulta, tudo o que é
necessário é conseguir ter uma ou mais classes que representem as fontes de dados. Nas
secções seguintes, discutiremos como fazer o mapeamento da base de dados, criando
portanto, a classe Northwnd. A expressão de consulta será sempre a mesma.

í 1 .2. 1 . l FERRAMENTA SQLMETAL


Quando não se utiliza uma interface gráfica, uma das formas mais simples de gerar as
classes de mapeamento é usando a ferramenta SqlMetal. Esta permite, entre outras coisas,
ligar a uma base de dados, gerando automaticamente o código necessário para a usar em
C#. Alternativamente, permite também gerar ficheiros XML de mapeamento de dados,
caso necessário.

No nosso exemplo, para gerar a classe Northwnd, utilizando o ficheiro da base de dados
directo ("NorthWnd.mdf}, tudo o que é necessário fazer é executar o comando:
^^ _* ...... " ~ " ~ _ ."..._. ...... . "... . .
Isto criará um ficheiro "nwind.cs", que contém todo o código de abstracção necessário para
usar a base de dados, de forma integrada com LINQ. Assim, o código necessário para
realizar a nossa consulta será apenas:
_ . _ ...... ~~\r custoft

from cUstomer -i n nw.customers


; wheré cUstomer . CompanyName ,
sel.éct customer._cpmpanyNatne;
Northwnd recebe como parâmetro a string de ligação à base de dados (connection string).
No nosso exemplo simples, é apenas o caminho para a instância que estamos a usar.
Estritamente falando, esta sfring deveria ser algo do tipo:
•@"Data : source=.\SQLEXPRESSY" +
:@"AttaGRDbFiÍenarne=C: \Livro\Northwind\NorthWnd.mdf;" + •
@"Iritegratea Security~True; cpnnect Tirneout=30; User lnstance=True" . i
Na documentação do SQL Seiver, pode ser encontrada mais informação sobre como
especificar estas ligações.

1 1.2.1.2 VlSUALSTUDIO

O VisualStitdio.NET possui um template chamado "LINQ to SQL Classes". Este permite


gerar os mapeamentos e classes necessárias, através do arrastamento visual de objectos da
base de dados para uma área de desenho. É ainda possível gerir os mapeamentos de fornia
manual. Este tipo de ambiente gráfico é extremamente personalizável, sofrendo no

© FCA - Editora de Informática


C#3.5

entanto, de uma desvantagem: não fornece nenhum mecanismo de regeneração automática


de classes, quando é efectuada alguma alteração no esquema da base de dados.
Para criar este exemplo, realize as seguintes operações:

Crie um novo projecto C#, do tipo Console Application, usando o VisiialStiidioi

Sobre o Project Explorei-, clique com o botão do lado direito do rato, adicionando
um novo item (Add-^New Item);

Na lista de itens disponível, localize e seleccione o nome "LINQ to SQL Classes".


Altere o nome do ficheiro para "NoríhWnd.dbml" e clique no botão Add. Será
apresentada uma superfície de desenho branca, para onde, mais tarde, irão ser
arrastados os objectos da base de dados.

Localize a janela "Server Explorer" (View^Server Explorei') e adicione uma


nova "Data Connection". Em "Database file name", introduza o caminho para o
ficheiro da base-de-dados ("NorthWnd.mdf');

• Abra o submenu Data Connections^NorthWnd.mdf-^Tables e arraste as tabelas


customer e order. Deverá obter algo semelhante ao que é mostrado na próxima
imagem.
SfrverExptorer -r O- X j HorthWnd.dbmirPrn0-am.ts fstwtPjijr
ls ^ n^ . . j
3 B" QJ Dota ConrtectBns ,A
_J
ffl C3 Database Dlagrams fãl
Order ® \ Propertles
è- CaTabtes
S Properties
5 ED CustomerCustomerDen»
B GD CuitomerDcmographics 7 S1 CustoraerlD
S" CompanyNime S1 CustomerID
ffi - S Customers
í? ContaclName iS bnployeelU
E- D EmptoyeeTerrttorfes S? ContaaTltíe )—^ Srurdífuflie
Éf Address S" RequItedDate
B- El OrderDetato
ffi [3 Orders Éfaty S" ShippedDflte
Q 03 Products S1 Regíon
S" PostalCode í? Ffdflht
Sf SWpName
ffl- ESHppers
n*Phone S1 SWpAddfess
• t ffl E3 SuppEas í
í? Fax
i . ffi d Territories í
S* ShipReglon
; È- C3 We«s
Êf ShtpPostalCííde
| É- C3 Stored Procedures !
S3 ShfpCountry
! É C3 FuncSom
i+f - f i svnonvms

Figura 11.2— Mapeamento de bases de dados para LINQ em VisuaIStudio.NET

Após guardar o ficheiro, o programa gera automaticamente todas as classes necessárias.


Em particular, será gerada uma classe chamada NorthWndoataContext. Este representará
a base de dados subjacente. Assim, o código necessário para realizar a consulta será:
íNortrMTclDatacontext nw = new NortPiWnclDatacontextOT" i
426 © FCA - Editora de Informática
INTRODUÇÃO À U NQ

var Gustomertfames =
from customer In nw.customers ' ; ' '
where.cus;tomer. CompanyName. StartsWl th (''A1') : > r ..
select' customer.CompanyName; - ,: t, .:
foreach.Çvar customer i n customerNames) • •-=. ;.'.
Console. WriteLineCíQ}",....customer);
É de notar que NorthwndDataContext não leva nenhum parâmetro de entrada. Neste
caso, a string de ligação é guardada no ficheiro "app.config", também gerado
automaticamente. O ficheiro pode ser alterado sempre que necessário.

Devemos também referir que NorthwndDataContext é uma classe derivada de


System.Data.Linq.oatacontext. Esta classe é extremamente importante em toda a
arquitectura LINQ para SQL. oatacontext actua como uma ponte entre a base de dados
e as classes das entidades mapeadas, abrindo e fechando ligações, realizando consultas,
actualizações, inserções, gestão transaccionai e, em geral, todos os aspectos de interacção
entre .NET e o sistema de gestão de dados. A classe possui métodos importantes que
incluem a execução de consultas e a resolução de conflitos entre registos. Pode afirmar-se
que esta classe representa o coração de LINQ para SQL.

1 1.2.1 3 MAPEAME>nO PORA7RÍBLTTOS

Uma outra forma possível de interagir com a base de dados é criando, manualmente, classes
que representam as entidades (tabelas) presentes, assim como as suas relações. Em seguida,
pode usar-se a classe oatacontext, do espaço de nomes system.Data. Linq, para que seja
feito o carregamento e mapeamento dos objectos no programa.

A título de exemplo, consideremos novamente a NorthWind. Esta possui uma tabela


customers, que representa os clientes, contendo diversos campos: CustomeriD,
companyName, contactName, Address, Phone, etc. Suponhamos que, no nosso programa,
apenas necessitamos de saber, para cada cliente, o seu identificador (customerio), o
nome da empresa (CompanyName) e o telefone correspondente (Phone). Assim, é possível
definir manualmente a seguinte classe, que representa um cliente. Note-se que apenas é
necessário indicar os campos que queremos utilizar:

© FCA - Editora de Informática 427


C#3.5 ==^===^^^^==^^^^^^^==—

; [Table~CNamé = "Customers"11)] "


class Customer
[Column]
; public string customerlD { get; set; }
[Column]
public string CompanyName { get; set; }

: [Column]
: public int Phone { get; set; }

0 atributo Tabl e permite especificar, a nível da base de dados à qual a tabela a classe se
refere. O atributo Column permite especificar que certas propriedades representam
colunas na tabela associada. É de notar que no atributo Tabl e usou-se
Name="customers". Isto acontece porque o nome da classe é diferente do nome da tabela.
Sempre que tal sucede, é possível especificar explicitamente o mapeamento a efectuar.
Caso contrário, o sistema procura automaticamente tabelas e colunas cora os nomes
presentes no código.

Tal como foi mencionado anteriormente, é necessário usar Datacontext para ir buscar os
dados à base de dados e gerir as ligações. Existem diversas formas de o fazer, com
diferentes implicações em termos de código. Por uma questão de coerência com os
exemplos que temos vindo a apresentar, iremos encapsular Datacontext em termos da
classe Northwnd, que teremos de criar. Note-se que esta é uma solução adequada neste
caso simples, podendo haver outras mais adequadas em casos mais complexos (múltiplas
tabelas, classes, etc.).
~clãss Nõrthwrid : Datacontext
1 public NorthWnd(string connectionString) :
base(connectionString)

public Table<Customer> customers


get
return GetTable<Customer>();

A classe Northwnd deriva de Datacontext, para que seja fácil carregar dados. Possui
apenas um construtor, que corresponde à string de ligação necessária para aceder à base
de dados. Obviamente, esse trabalho será delegado na classe-base: Datacontext. Possui,
ainda, uma propriedade pública que permite obter os elementos da tabela customers.
Assim, retorna Table<customer>, delegando esse trabalho no método GetTableQ. Este
método é herdado de Datacontext, permitindo, genericamente, obter qualquer tabela
presente na base de dados. A tabela a obter é especificada pela parametrização
428 © FCA - Editora de Informática
INTRODUÇÃO À LINQ

correspondente, neste caso, customer. Assim, ao executar-se o código principal do


programa:
cTass"~ÈxempToCápll_l'' """ "
static vold Main(string[] args)
NorthWnd nw = new NorthWnd(@ n c:\Livro\Northwind\NorthWnd,mdf n );
var customerNames =
from customer i n nw.Customers
where customer.CompanyName.startswith("A")
select customer.CompanyName;
foreach (var customer i n customerNames)
console.WriteLine("{0}", customer);

surgira:
;AnaTruj~illoEmparedados y hélãdõs "
António Moreno Taqueria
Arpund the Horn _. ......
A listagem seguinte apresenta o código completo deste exemplo.

/*
* programa que ilustra o mapeamento directo de campos
* numa base-de-dados.
*/
using System;
usi ng System.collecti ons.Generi c;
using System.Linq;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using system.Text;
[Table(Name = "customers")]
class customer
{
[Column]
public string CustomerID { get; set; }
[column]
public string CompanyName { get; set; }
[Column]
public int Phone { get; set; }

class NorthWnd : DataContext


public NorthWnd(string connectionstring) :
base(connectionstring)
{

© FCA - Editora de Informática 429


C#3.5

publlc Table<customer> Customers


get { return GetTable<Customer>O; }
}
class ExemploCapll_l
static vold Main(string[] args)
NorthWnd nw = new NorthWndC@"C:\Livro\Northwind\NorthWnd.mdf");
var customerNames =
from customer i n nw.customers
where customer.CompanyName.StartsWith C"A")
select customer.CompanyName;
foreach (var customer i n customerNames)
Console.WriteLlne("{0}", customer);

Listagem 11.1-Programa que ilustra o mapeamento directo de campos


numa base de dados (ExemploCapll_l.cs)

O espaço de nomes system.Data.Linq.Mapping contém todos os atributos necessários


para mapear bases de dados em classes de ura programa. A tabela seguinte apresenta um
pequeno resumo dos principais atributos que são usados.
j ATRIBUTO ~ ~ | Uso
Assoeiatnon Permite definir atributos como chaves primárias (prímary~key) e
chaves estrangeiras (foreign~key). __ „_ ,
C o l u m n l Permite definir atributos como colunas d e u m a tabela.
Database l Permite
J i 1^1 l l IILV- definir
<_J^l II III o
ij nome
l lUlllt. da
UU base
LJOJt; de
l_lt dados.
UOUWO,

Function • | Permite mapear métodos em funções e stored-procedures.


InheritanceMapping :| permite mapear relações hierárquicas.
Parame1:e_r | Permite especificar parâmetros a serem passados_a stored-procedures.
l .Provi _ r .._ l Pe/mite especificar qual p motor de base de dad_qs a usar.
Permite especificar o tipo do resultado de um stored'prpcedure._ _
'• \T^5.e l FeCm'le,especificar uma classe como correspondendo a_ uma tabela.

Tabela 11.4 — Principais atributos usados para mapear bases de dados em classes

Um aspecto interessante é tanto a ferramenta SqlMetal como o VlsualStudio.NET


utilizarem a classe Datacontext e os atributos anteriormente descritos para gerarem o
código das classes de mapeamento.

43O © FCA - Editora de Informática


INTRODUÇÃO À LINQ

11.2.1.4 MAPEAMENTO POR FICHEIROS XML


Em algumas situações, é útil não codificar directamente a forma como o carregamento das
tabelas e colunas é feito, mas usar uma especificação externa. Ou seja, ao definir a classe
customer, não indicar, em termos de código, quais as tabelas e colunas correspondentes,
mas colocar essa informação num ficheiro XML. Assim, se mais tarde, o esquema da base
de dados mudar, não será necessário alterar e recompilar o código fonte.

Em termos do nosso exemplo, a classe customer poderia ser definida como:


[cTass "cústòmér"
i public string ID { get; set; } // coluna CustomerlD
í public string Name { get; set; } // Coluna CompanyName
| public int Phone { get; set; } // coluna phone

Note-se que não é indicado qualquer tabela correspondente a esta classe ou as colunas
que serão usadas. Para ilustrar que não é necessário ter o mesmo nome nas colunas da
tabela da base de dados e na classe correspondente, modificámos o nome das
propriedades de customer. A forma como é feito o mapeamento da base de dados para
esta classe é especificada num ficheiro XML:
(<7xmT v e r s ~ i õ n = ' ' i r O ' i " " " ~ ~

<Database Name="NorthWnd"
xmlns="http://schemas.mi crosoft.com/li nqtosql /mappi ng/2007">
<Table Name="customers" Member="customers">
<Type Name="customer">
<Column Name="CustomerID" Member="ID" />
<Column Name="CompanyName" Member="Name" />
<Column Name="Phone" Member="Phone" /> ;
</Type> :
</Table> !

</oatabase> _ . ___ ___;


Finalmente, é necessário modificar a classe Northwnd para que use o ficheiro XML.
Devido ao uso de um construtor que necessita do mapeamento entre o ficheiro XML e os
objectos, Northwnd deixa de ser derivada de DataContext. DataContext passará a ser
uma variável de instância em Northwnd. O código correspondente é:
fcTãss"Nõfth~Wricr ~" "i
private DataContext DB; // Representa a ligação à base-de-dados
public NorthWnd(string connectionstring, string mapFile) :

// Lê^o_jnap_eamentp _do fÍcheT£o_XML_, _passandoj-q_ao..JDataCpn_text DB


"MappirTgSource map '= "" ""
xmlMappi ngsource.FromXml(Fil e.ReadAliText(mapFilê));
~}~z/^~^TZirriiriT.~r.Tiz]~"/ii^^^ ..—!
© FCA - Editora de Informática 431
C#3.5

public Tab1e<Customer> Customers


get
return DB.GetTable<Customer>() ;
T_

XmlMappingsource permite ler um ficheiro XML, contendo os mapeamentos a usar,


sendo essa informação passada ao construtor do Datacontext. O código do programa, já
com a expressão de consulta reescrita para usar os novos campos, fica:

new Northwnd(@"c:\Livro\Northwind\Northwnd.mdf", ©"NorthwndDB.xml");


var customerNames =
from customer In nw.Customers
where customer.Name.startswith("A")
select customer.Name;
foreach (var customer i n customerNames)

A semelhança do que acontece com o uso de atributos directamente no código, existe um


conjunto de elementos XML, que podem ser utilizados para explicitar a forma como são
feitos os mapeamentos entre as base de dados e o código fonte. O leitor interessando
poderá consultar a informação completa na documentação MSDN.

11.2. l .5 INSERÇÃO E ACTUALIZAÇÃO DE ELEMEMTOS


Um aspecto que não discutimos até agora foi como introduzir, actualizar e apagar registos
de uma base de dados. Na verdade, tal é bastante simples. Todo o princípio por detrás de
LINQ é que as tabelas nas bases de dados sejam representadas como objectos. Assim,
para introduzir um novo registo, basta criar um objecto apropriado, adicionando-o à
colecção que representa a entidade correspondente. De seguida, chama-se o método
submitchangesQ associado ao Datacontext correcto. Este fará com que as alterações
efectuadas sejam propagadas até à base de dados. O mesmo princípio aplica-se à
actualização e remoção de registos.

A título de exemplo, o código seguinte encontra a empresa cujo nome é "Around the
Horn" actualizando-o para "Around the Horn -~ The Chain Store":
•NorthwndbBDãtaContext "nw~= new NorthWndòBDãtaCbntéxtOj~ "~ " " " :
customer myCompany = !
(from customer in nw.Customers !
i where customer.CompanyName == "Around the Horn"
: select customer).singleQ; j
i
'mycompany.CompanyName = "Around the Horn — The Chain Store"; ;
'nw._Subnritchanges.Oj,. .... _ ;

432 © FCA - Editora de Informática


INTRODUÇÃO À LINQ

A única parte eventualmente menos evidente deste exemplo diz respeito ao uso do
método de extensão s i n g l e Q . Este obtém a instância seleccionada, fazendo uma
conversão explícita para o tipo de dados subjacente (neste caso, Customer). Isto
permite-nos actualizar o nome da empresa directamente, o que não aconteceria se
myCompany tivesse sido declarada, usando a palavra-chave var. Se tal acontecesse, a
variável poderia representar uma colecção, que teria de ser iterada manualmente. Tal é
ilustrado no seguinte exemplo, que coloca em maiúsculas, o nome de todas as empresas
presentes na tabela Custotners:
TíorthWndDBDataContèxt nw =™new" NbrthWndDBDãtaCõntextO ; " :
i j . . ' ' . • ' . • • ' . . . . ; .
çonarnes = ., ^ - . '
i n n w . Customers . • ' . - . ' . '
cústomer; ,'"'- ;
,' i , - ' ' • ' • • . : - *':-1; •
foreadi (g/á r' company In compames}
còjnp^nSc.companyName = company. companyName. ToUppérO ; :

Embora não o discutamos aqui, é ainda possível especificar o que acontece quando não é
possível realizar as actualizações, especificar actualizações que abranjam várias tabelas,
assim como a utilização explícita de transacções.

- A arquitectura LINQ permite consultar, inserir, actualizar e apagar registos


ARETER de bases de dados SQL. Para tal, é necessário mapear as tabelas da
base-de-dados em termos de objectos C#.
LINQ para SQL ~ O mapeamento pode ser feito de quatto formas distintas: 1) utilizando a
ferramenta SqlMetal\) visualmente, usando o VisualStudio.NET\) pela
utilização de atributos; 4) pela utilização de ficheiros XML.
- SglMetal permite gerar, tanto código fonte, como ficheiros XML de
mapeamento. Para isso, basta indicar-lhe as opções de geração necessárias e
a string de ligação à base de dados. Por exemplo:
Sqlnetal /code:nwind.cs Northwnd.mdf
" Em VisiialStiidio.NET, o mapeamento é feito de forma visual, usando o
item "LINQ to SQL classes".
- Ao fazer-se o mapeamento de forma directa, usando atributos, Table e
Col umn permitem especificar a tabela e colunas correspondentes, numa base
de dados. Por exemplo:
[Tab1e(Name="Custamers")3
class Customer

[Column] public strnng CustomerlD { get; set; }


Ccolumn] public string CompanyName { get; set; }
[Column] public int Pnone { get; set; }

" Á classe DataContext é usada para interagir com a base de dados,


permitindo introduzir, consultar, actualizar e apagar registos.

© FCA - Editora de Informática 433


C#3.5

Para realizar mapeamentos usando ficheiros XML, utiliza-se as classes


A RETER Mappingsource e XMLMappingsource. O mapeamento tem de ser passado à
instância de Datacontext que vá ser utilizada.
LINQ para SQL _ para jnserjr) actualizar e remover elementos da base de dados, realizam-se
essas operações sobre as colecções de mapeamento correspondentes,
chamando o método submitChangesQ sobre o oataContext correspondente.

1 1.2.2 LJNQ PARAXML


Em anos recentes, o formato XML ganhou uma importância fulcral para todos os
sistemas que têm de armazenar e trocar informação entre si. Tendo sido adoptado num
conjunto enorme de aplicações, é hoje possível encontrar, de uma forma ou outra,
documentos XML em servidores web, ficheiros de configuração, documentos de texto, e
bases de dados. Trata-se do formato universal da Internet.

No capítulo 8, vimos, brevemente, como seríalizar e recuperar objectos em formato XML.


Para isso, utilizámos as classes presentes em system.xml e system.xml . senalization.
Na verdade, o espaço de nomes system.xml possui um conjunto de classes que permite
tratar, de forma muito poderosa, XML. Infelizmente, essas classes não são, muitas vezes,
as mais fáceis de usar. Felizmente, quando a LINQ foi criada, houve a preocupação em
suportar directamente XML. Assim, à semelhança do que acontece com objectos e bases
de dados, é bastante fácil consultar e manipular ficheiros XML neste sistema. Na verdade,
devido à sua facilidade, muitos programadores da plataforma .NET estão hoje a adoptar
estar forma de manipular XML, em detrimento das APIs tradicionais. O suporte para
XML da linguagem LINQ encontra-se no espaço de nomes system.xml . Linq.

Consideremos um ficheiro XML simples, contendo um catálogo de CDs:


<?xml vers1on="l.Q" encod1ng="UTF-8"?>
<catalogo>
<cd id="0001">
<titu"lo>screaming Fields of Sonic Love</titulo>
<artista>Sonic Youth</artista>
<ano>1995</ano>
</cd>
<cd -id="0002">
<titulo>uh Huh Her</t1tulo>
<artista>P3 Harvey</arfista>
<ano>2004</ano>
<cd id="0003">
<titulo>The Mirror Conspi racy</fitu1o>
<art1sta>Thievery Corporation</artista>
<ano>2000</ano>
</cd>
</cata1oqo> ____
Listagem 11.2 — Um catálogo de CDs em XML (catalogo_cds.xml)

434 © FCA - Editora de Informática


INTRODUÇÃO À LI N Q

Todos os elementos (tags) XML são representados pela classe XElement. Esta classe
possui, ainda, atributos que permitem carregar e gravar ficheiros XML. Por exemplo,
escrevendo:
XElement catálogo = XETeme.ht.LoadC"cataio~go_cds.xnil") ;'
Cgnsole.WnteLineCcatalggp);
o ficheiro "catalogo_cds.xmr é carregado em memória e impresso. O resultado impresso
no ecrã é exactamente o que é mostrado na listagem 11.2, com a excepção da primeira
linha3. O primeiro elemento do ficheiro, chamado elemento raiz, corresponde à tag
catalogo.

Vejamos, agora, como escrever uma expressão de pesquisa simples. Se quisermos


imprimir os títulos de todos os CDs, basta fazer:
catalogor="XÈTémérit."LõãaCllcãta1ògò-qds."xml"DT '"' '
var títulos =
! from cd i n catalogo.ElementsC"cd")
seVeçt cd(i ^lementC"titulo") ;
foreach 0/ar titulo In títulos)
console.Wn"teL-ineCt1tulg.Value).j _ _ _ .... _ ' _ .
Este código merece uma pequena explicação. O método ElementsQ retorna todos os
nodos filho de um certo elemento, caso seja usado sem parâmetros. Caso possua um
parâmetro, retoma nodos filho que possuam um certo nome (parâmetro de entrada).
Assim, catalogo.Elements("cd") irá representar todos os elementos cd. Mas, por sua
vez, cada elemento pode ter diversos componentes: texto (por exemplo, "Screaming
Fields of Sonic Love")\s (por exemplo, id="0001"), ou outros elementos
descendentes. Assim, dados todos os elemento cd que foram seleccionados, select
cd.Element("titulo") irá retomar todos os elementos titulo. Se, no código, tivéssemos
mandado imprimir directamente esses elementos escrevendo:
rfòrea,çh 0/âp :titulo in títulos) " " " ™ " " ' • , . " • ' •
; . Coh^d^e.W.c-itáLin.eCtltulpl; _ _ * _.
o resultado teria sido:

<t-i'tulò^-screaming
' '
~FiéTds~of Sonic CõÃ/e</titulo>
- ' ' '

Isto porque cada elemento titulo é constituído, não só pelas tags correspondentes, mas
também pelo texto e, eventualmente, atributos e nodos descendentes. Assim, para obter
apenas o texto, é necessário usar o atributo vai ue do elemento:
+ O/ar- titúl o in';"ti'túl.ós')'
e WnireLii

3 Estritamente falando, a primeira linha não é um elemento mas sim urna directiva XML, correspondendo ao
prólogo do ficheiro.
© FCA - Editora de Informática 435
C#3.5

O resultado será:
Scfeairn ríg Fl èlàs of Som"c Lòve
Uh Huh Her
•The Mj. r ror consplr.acy
Na verdade, uma forma mais eficaz de escrever este exemplo teria sido:
i vá r títulos '= ""
, from titulo In catalogo.DescendantsC"t1tulo")
'• select titulo.value;

foreach (var titulo In títulos)


i _Cpnsple.writeLlne(titulo); ._ _.

Embora titulo não seja filho directo de catalogo, é um descendente deste. O método
oescendentsQ permite seleccionar um conjunto de elementos com um certo nome em
qualquer parte de um ficheiro XML. Assim, é possível pesquisar directamente qualquer
conjunto de nodos presente num ficheiro deste tipo, sem grande esforço.

Vejamos, agora, um exemplo ligeiramente mais complexo. Imaginemos que queremos


encontrar todos os CDs lançados após 1998, imprimindo o seu título, autor e ano. Esta
expressão de consulta seria aparentemente simples, não fosse um pequeno pormenor:
ficheiros XML não especificam tipos de dados - é tudo texto4. Para conseguir realizar
esta pesquisa, é necessário o seguinte código:
var" tituVos._posl998 =
from_cd in catalogo.Descendants("cd")
P ~where~~ (rrit)cd.Èlémént("ano") > 1998' """
| _prderby (1ntócd,.Element(llanoM) _
select riêw '" "~ "~"~
Titulo = cd.Element( 1I t1tulo").Value J
Artista = cd.Element("artista").Value,
Ano = cd.Element("ano").value

Console.Wr1tel_1ne("Ano \ Artista \ Titulo");


Console.WriteLine(" =======================================-==== ") ;
foreach (var cd in titulos_pos!998)
console.Wr1teL_1neC"{OJ- \ {11 \t_i2}", cd.Ano_, çd.Artista., çd.TituJg);
Como se pode verificar, foi necessário realizar uma conversão explícita do campo ano
para int, para que as comparações funcionem correctamente. O resultado da execução
deste código é:
-Àhõ ~~"SrtTst:ã Titulei ~ ~~;
7000 Thlevery Corporation The Mlrror Consplracy
;2004 .. P3 Harvey. Uh H_uh Her

4 Tipagem de dados apenas é conseguida usando XSD.


436 © FCA - Editora de Informática
INTRODUÇÃO À LINQ

Este pormenor das conversões explícitas é bastante importante porque, em muitos casos,
sem conversões explícitas, não é possível escrever as expressões de consulta. Por
exemplo, imaginemos que os elementos cd possuíam ainda dois campos: nvendldos,
representado o número de CDs já vendidos, e preço, representando o preço unitário de
cada CD. Para ser possível encontrar os CDs cujo volume de vendas excedesse, por
exemplo, 1.000.000€, as conversões explícitas seriam essenciais:
ivãr grãndesvéndas = "' " "" " (
! from cd in catalogo.DescendantsÇ"cd")__ _ . _ _ :
where " "" " ~ " -" ' ~ ~ "~ "1
(1nt)c_d^lemejitr;nYe^
"selèct cd; '_ _ _.. _. . _. i
O último aspecto que iremos ver é a facilidade com que é possível criar árvores e
ficheiros XML, usando a classe XE! ement e LINQ.

Para criar uma pequena árvore XML em memória contendo um conjunto de CDs, basta
usar as classes XEl ement e XAtt ri bute. Por exemplo:
XEl ement catai ògcf= ~ ;
new XEl ementC catalogo", ;
; new XElementC"cd" , j
i new XAttribute("id", "0001"), !
new XElement("titulo", "screaming Fields of Sonic Love") ,
new XElementC"artista", "Sonic Youth") , !
new XElementCano", "1995")), ;
; new XElementC"cd" , !
new XAttributeC"id", "0002"),
new XElement("titulo", "Uh Huh Her") , ;
new XElementC"artista", "P3 Harvey") ,
i new XElementC"ano", "2004")), :
new XEl ementC cd", i
new XAttnbuteC"id", "0003"), ;
new XElementC"titulo n , "The Mirror conspi racy") , ;
new XElementC"artista", "Thievery Corporation") ,
new XElementC n ano n , "2000")));
•catai ogp . Save("cat_alogp_cds2 . _xml") ;._ _ ......... ... ..... _ ... ... ...... .i
cria em memória o ficheiro XML que temos estado a usar e guarda-o para disco. Tudo
isto de forma extremamente simples.

Para perceber todo o potencial desta funcionalidade, suponhamos que temos uma
colecção de objectos do tipo CD. CD é uma classe definida da seguinte forma:
icflass' C D -.. — . . ;

Í public int Id { get; set; } i


: public string Titulo { get; set; } :
i public string Artista { get; set; }
public int Ano { get; set; } •
} _ _ _ __________ _____ .... _ ....... _ _.....___________. „„_._ ................ ...... .„_. .•
Estes objectos estão armazenados numa variável catai ogoCDs, do tipo List<CD>. Se
quiséssemos criar um ficheiro de catálogo, apenas teríamos de fazer:
© FCA - Editora de Informática
C#3.5

"XÉ~I emeht" catai ogò


new XElementC catalogo",
; from cd í n cataiogoCDs
select
| new XElementC"cd", j
i new XAttributeC"id", cd.Id), j
; new XElementC"titulo", cd.Titulo),
s new XElementC"artista", cd.Artista),
i new XElement("ano", cd.Ano))); ;

:çatalogp._SayeCIlçatalo30^cds3..xml.").i.. . . . . ;

Este código merece ser examinado com cuidado. Na mesrna expressão de pesquisa,
estamos a combinar, directamente, elementos criados no momento (por exemplo, o
elemento catai ogo) com objectos gerados a partir de uma expressão de consulta de uma
lista de objectos (List<CD> catai ogocos), gerando e gravando um ficheiro XML como
resultado. Este nível de versatilidade e flexibilidade na manipulação de fontes de dados}
quer sejam objectos, bases de dados ou XML é um dos fortes da LINQ.

A listagem seguinte apresenta o código completo, para melhor compreensão.

/*
* Programa que ilustra a geração e manipulação directa de XML.
*/
using System;
using System.Linq;
using System.Xml.Ling;
usi ng System.Collecti ons.Generi c;
class CD
public string id { get; set; }
public string Titulo { get; set; }
public string Artista { get; set; }
public int Ano { get; set; }

class Exemplocapll_2
static void Main(string[] args)
// Cria uma lista de cos correspondendo ao catálogo
List<CD> cataiogoCDs = new List<CD>();
// Adiciona alguns CDS à lista de objectos
cataiogoCDs.AddCnew CDQ {
Id = "0001", Ano ^ 1995,
Titulo = "Screaming Fields of Sonic Love",
Artista = "Sonic Youth" });
cataiogoCDs.AddCnew CD() {
Id = "0002", Ano - 2004,
Titulo = "uh Huh Her",
Artista = "PJ Harvey" });
catalogoCDs.AddCnew CD() {
Id = "0003", Ano ^ 2000,

438 © FCA - Editora de Informática


INTRODUÇÃO À LINQ

T i t u l o = "The Mirror Conspiracy",


Artista = "Thievery Corporation }) ;

// Gera dinamicamente um ficheiro XML com a informação


XElement catalogo =
new XElementC"catalogo",
from cd i n cataiogoCDs
select
new XElement("cd",
new XAttri bute("i d", cd.ld),
new XElement("titulo", cd.Titulo),
new XElement("artista", cd.Artista),
new XElementC'ano", cd.Ano)));

// Guarda o ficheiro XML em disco


catalogo.Save("catalogo_cds3.xml");

Listagem 11.3-Programa que gera dinamicamente ficheiros XML usando LINQ


(ExemploCapll_2.cs)

" A arquitectura LINQ permite manipular directamenteficheirosXML.


ARETER
" Dado que não existe tipagem de dados em XML directo, as arvores XML
são representadas em termos das classes XE! ement e XAttri bute.
LINQ para XML
~ XElement representa um nodo numa árvore XML, possuindo também
métodos para carregar e gravar a árvore em ficheiro. XAttri bute representa
um atributo de um elemento (nodo) XML.
~ O método ElementsO permite obter dos elementos de um determinado
nodo e, também, os elementos que possuam um certo nome.
" O método DescendentsQ permite procurar, na árvore XML, todos os
descendentes de um certo nodo ou os descendentes com um certo nome.
~ Para obter o valor (texto) de um certo nodo, utiliza-se a propriedade value
de XElement.
" É possível combinar expressões de consulta com a utilização de XEl ement,
para gerar e actualizar, dinamicamente, árvores XML.

© FCA - Editora de Informática 439


I r^ir1
s

Nos últimos onze capítulos, foi coberto imenso terreno. Em particular, na primeira parte
do livro, a linguagem C# foi amplamente explorada. Na segunda parte, foi dada uma
visão geral dos APIs fundamentais da plataforma .NET.

Para terminar, iremos situar o leitor nos API que foram explorados e no que mais ainda
existe para explorar. Embora o objectivo deste livro não seja centrar-se na plataforma
.NET, mas sim na linguagem C#, achamos que é importante o leitor ficar com uma ideia
clara do que existe disponível. A figura seguinte apresenta uma visão de alto nível dos
API existentes. Esta figura é semelhante à apresentada no primeiro capítulo, mas
focando-se nas interfaces de programação.

Acesso à rede

(ASP.NET, Web
Services, Web Forms)

Acesso e manipulação de dados


(ADO.NETeXML)

Base Class Library (BCL)

Common Language Runtime (CLR)

Figura 12.1 — Visão de alto nível dos API da plataforma .NET

12. l INTERFACES DE PRCXSRAMAÇÃO


Examinemos, então, os principais pontos associados a cada um dos blocos presentes na
figura.

© FCA - Editora de Informática 441


C#3.5

12.1. l COMMONLANGUAGERaN77ME(CLPb
Relativamente ao Common Langiiage Runtime, ainda existe muito para explorar. Entre
outras coisas:

O modelo de assemblies, quais os diferentes tipos de assemblies existentes, de


que forma é feita a sua instalação e também a sua configuração;

• De que forma é feito o carregamento dinâmico de código;

Como é que, detalhadamente, funciona o mecanismo de introspecção e como é


gerado e invocado código dinamicamente;

• Interoperabilidade com outras linguagens;

Como é que é feita integração com objectos COM/ActiveX;

Mecanismos de segurança associados à plataforma .NET e sua utilização.

12.1.2 BAS£CLASSlJBRARr(BCL)
Neste livro, foi coberta uma boa parte da Base Class Libraiy. No entanto, o leitor poderá
ainda encontrar muitas classes úteis. Classes que permitem, desde gerar números
aleatórios, a processar texto, até manipular horas e datas. Existe, ainda, um outro conjunto
de classes, conjuntamente chamadas de Globalization, que permitem criar programas
adaptados a executarem em diferentes partes do mundo.

Um tópico bastante importante é a criação e manipulação de objectos distribuídos. Para


isso, é utilizado o API Remoting. No entanto, devido a este ser tão fundamental na
operação da plataforma, é considerado como sendo um API base (espaço de nomes
system) e não um API a par das restantes interfaces de programação de acesso à rede.
Este tópico não foi abordado.

12.1.3 ACESSO E MANIPULAÇÃO DE DADOS

Urn tópico muito importante é o acesso a bases de dados e a manipulação de informação


nestas. Isso é conseguido utilizando o API ADO.NET. Para além disso, a plataforma
.NET dispõem de um extenso conjunto de classes que permitem manipular informação
em formato XML. É de referir que a linguagem LINQ, é hoje em dia fulcral na
arquitectura de manipulação de dados da plataforma .NET. Embora a tenhamos abordado
brevemente, recomenda-se vivamente um estudo mais aprofundado da mesma.

442 © FCA - Editora de Informática


EXPLORAÇÕES FUTURAS

12.1.4 ACESSOÀREDE
Neste livro, foi dada urna panorâmica geral da forma como se pode aceder a serviços na
Internet. No entanto, existe ainda muito para explorar. Em particular, os web sei-vices
merecem um tratamento mais profundo do que foi dado neste livro. Para além disso,
existe ainda a questão da criação de páginas web dinâmicas, utilizando o API ASP.NET,
assim como a manipulação dessas páginas como formulários, os chamados Web F o rins.

12.1.5 PROGRAMAS GRÁFICOS


Hoje em dia, a generalidade das aplicações que correm num computador pessoal são
gráficas. O utilizador interage com um conjunto de janelas e diálogos, utilizando um rato,
realizando as operações necessárias para completar as suas tarefas.

Na plataforma .NET, existem dois API que permitem ao programa, criar programas
gráficos: Windows Forms e GDI+. O Windows Fonns permite criar programas baseados
em janelas, diálogos e controlos. O GDI+ permite desenhar em janelas e manipular
objectos gráficos (por exemplo, imagens). Na prática, trabalhar com janelas é
extremamente fácil e, caso o programador utilize o Visua.lStudio.NET, o trabalho fica
ainda mais facilitado, uma vez que, nesse caso, tudo é feito de forma visual.

12.2 CONCLUSÃO
Para terminar, e num acto de revolta contra livros que "simplesmente terminam" num
capítulo como qualquer outro, gostaríamos de agradecer ao leitor, por nos ter
acompanhado ao longo desta viagem. Esperamos que tenha sido interessante.

© FCA - Editora de Informática 443


Na documentação presente no MSDN, existem algumas recomendações sobre a forma
como se deve dar nomes a variáveis e tipos de dados e como utilizar maiúsculas/
/minúsculas no seu nome.

Regra geral, deve-se utilizar a notação da linguagem Pascal ao dar nome a tipos de dados,
métodos e espaços de nomes. A primeira letra de uma palavra deve ser em maiúscula,
sendo as restantes em minúsculas. Se a variável, ou o tipo de dados, é composta por
várias palavras, cada uma delas deve começar por maiúscula. As palavras não devem ser
separadas por sublinhado (_). Por exemplo, os seguintes nomes de classes e variáveis são
válidos: Empregado, NomeDaPessoa, VelocidadeMaxima.

No caso das interfaces, aplica-se a notação Pascal, só que o nome da interface deverá
começar sempre pela letra /maiúscula. Por exemplo: iLe-itorCD e icollectlon.

Os nomes de variáveis de instância e de constantes devem também utilizar a notação


Pascal. Uma excepção possível a esta regra é quando existe uma propriedade pública
com o mesmo nome do que uma variável de instância privada, que a representa. Nesse
caso, uma solução possível é utilizar a notação Pascal para a propriedade pública e
começar o nome da variável de instância com a primeira letra com minúscula. Por
exemplo, uma classe com uma propriedade pública de nome DataEntrada e uma variável
de instância de nome dataEntrada. No entanto, regra geral, é melhor atribuir um outro
nome à variável de instância, utilizando a notação Pascal.

1 Esta foi a única convenção que violámos ao longo do livro. Ao longo do livro, utilizámos nomes em
maiúsculas separados por sublinhado, para constantes. Por exemplo, SALARIO_MAXIMO e
RESOLUCAO_MAXIMA. Dado que as enumerações funcionam como constantes, este formato foi
também aplicado nesse caso. É opinião dos autores, que esta forma de codificar permite manter uma
melhor perspectiva sobre elementos modificáveis e não modificáveis e quando são feitas atribuições de
variáveis a valores por omissão. No entanto, fica o alerta para o facto de esta nomenclatura não ser seguida
na plataforma .NET. No entanto, convenções de codificação não são especificações de linguagens. O
programador pode (e deve) utilizar o seu discernimento para decidir quando deve ou não aplicá-las.
© FCA - Editora de Informática 445
C#3.5

A variante da notação em que a primeira letra da primeira palavra começada por minúscula
e as seguintes obedecem à notação Pascal chama-se notação Camel, Por exemplo: nome,
nomeDaPessoa, veloddadeMaxIma. Esta notação deve ser utilizada em nomes de
parâmetros de métodos. Por exemplo:
jpQbTic'cTãss""Pessoa ~" 77 Nome "de ^cna"sseT""ridtãçãb"~Pãscal "~~
j prlvate strlng Nome; // Variável de Instância: notação Camel
publlc Pessoa(stn'ng nome) // Parâmetro de entrada: notação Camel
Nome = nome;

// novoNome é um parâmetro de entrada: notação Camel


publlc vold Mod1f1caNome(str1ng nomeNome)
Nome = novoNome;

Outra situação em que se pode utilizar notação Camel é ao dar nome a variáveis
automáticas. No entanto, também é admissível utilizar a notação Pascal para esses casos.
Neste livro, optamos pelo primeiro caso.

Finalmente, um ponto importante para os programadores de Visual C++ e de Visual


Basic é que a utilização da notação húngara é desaconselhada. Em Visual Basic e em
Visual C-f-f-, quando se dá um nome a uma variável, indica-se o seu tipo, através de um
pequeno conjunto de letras. Por exemplo, strMorada, para uma string que representa uma
morada, ou l pResul t para um long pointer, para um resultado. Isto não deve ser feito em
C#. Os nomes das variáveis devem representar apenas o seu conteúdo e não o seu tipo.
Como o C# é uma linguagem strongly typed, é tarefa do compilador, gerar erros, caso
exista incompatibilidade entre tipos, não do programador verificá-las, através de nomes
de variáveis.

446 © FCA - Editora de Informática


ÍNDICE REMISSIVO

# Application • 387, 388 .


AppHcationBxception • 132
Array • Ver Tabelas
#define-24Q,24l ArrayList - 278
#elif-241 AS • 107
#else • 241 ASCH-266,314
#endif-24l ASCIIEncoding-314
#endregion.< 243 Asrax-381,365
terror • 242 : ASP.NET-3,443
m- 240,241 Assembly • 64,66,118,171,327,385
#line • 242 Atributos • 143
#region • 243 Attribute -171, 172
#undef-24l AttributeTargets • 172
#warning • 242 AttributeUsage • 172
Definição • 171
elementos a que se aplicam • 169,170
obtenção em tempo de execução • 174
programação declarativa • 167 :
.NET-1 AutoResetEvent • 364
arquitectura • 4
classes-base-249
sistema de eventos • 162 B

Base - 50, 92
Base Class Library • Ver BCL
BCL • 3,442
/checked • 138 BinaryFormaíter • 318, 319
/doe • 245 BinaryReader • 308, 309, 315
/domain-384 BinaryWriter-308,309,315
/out • 66 BítArray-278,282
/password • 384 Bool-16,60
/r-66 Boolean • 60
/t-66 Boxing/unboxing • 60
Amsafe - 221 Break-26,31
/username • 384 BufferedStream-309
Button • 142
Byte • 12, 60
A
Abort • 335
Abstract'98,-100
AcceptTcpClient • 392
ADO.NET-3,442 Caracteristicas • l
AllowMul tiple-172

© FCA - Editora de Informática 447


C#3.5

CH- • l, 2 ,5, 27, 39, 41, 45, 75, 87, 105,111, foreach • 187
131, 135, 213, 221, 224, 225, 233, 240, hashtable • ver hashtable
241, 249, 276, 446 icollection • 276, 277
Cadeias de caracteres • 9, 16, 17, Ver também ienumerable- 187, 189
stríng iteradores • 187
formatação • 266 list • ver Jist
formatações definidas pelo programador - 268 queue • ver queue
operações • 18, 21 SortedDictionary • ver SortedDictionary
String • ver string SortedList • ver SortedList
StringBuílder • Ver StringBuilder stack • ver Stack
Capturas • ver expressões regulares yield • 196
Caracteres • 16 Collections • 190
sequência de escape • 17 Comentários • 10
Case • 26 documentação XML • 244
Cast • Ver Conversões explícitas fim de linha- 11
Catch • 123 múltiplas linhas • 10
Char-16,60 Common Language Runtime • Ver CLR
Checked • 137 Common Language Specifícaíion • Ver CLS
Class • 47 Common Type System • Ver CTS
base • 50 CompareTo • 292
Classes • 47 Componentes
abstractas • 97 atributos • ver Atributos
construtores • ver construtores definição • 141
conversões • ver conversões entre tipos eventos • ver Eventos
destrutores • ver destrutores métodos • 141
inicialização automática de campos • 74 Propriedades • ver propriedades
inicialização directa de campos • 81 Composição • 48
membros estáticos • 68 Console • 9
membros estáticos readonly • 80 leitura • 260
modificadores de acesso • 64 Const • 19, 67
níveis de acesso • 63 Constantes • 19, 67, 68
seladas • 96 Construtores - 47, 72
utilização genérica de mis • 76 estáticos • 78
Click • 143 por omissão • 73
Clone • 258 utilização de vários • 75
Glose - 235 Context • 387
CLR • 2, 442 Continue-31
boxíng/unboxing • 60 Controlo de fluxo • 24-32
chamada de construtores • 79 break-26, 31
destrutores • 233, 234 case • 26
métodos virtuais • 94 continue • 31
sistema de tipos - 57 default • 26
tratamento de excepções • 130 do-while • 27
CLS-4, 171 for • 28
CLSCompliant- 171 foreach • 29
CodeBehind • 385 goto-27, 31
Código hash • 253 if-else • 24
Coleccções quebra de ciclos • 30
LinkedList • ver LinkedList switch • 26
Colecções while • 27
ArrayList • ver ArrayList Conversões
BitArray • ver BitArray de e para cadeias de caracteres - 261
dictionary • ver Dictionary definidas pelo utilizador • 214
enumeradores • 187, 198 entre classes • 59

448 © FCA - Editora de Informática


ÍNDICE REMISSrVO

entre tipos • 106 Escritores • 365


explícitas • 15, 59, 215 Espaços de nomes • 8, 116
implícitas • 14, 214 aliases -118
numéricas • 14, 15 Estruturas • 109
Convert • 261 Event • 162
Crivo de Eratósíenes • 2S3 EventArgs • 162
Csc.exe • 7, 245, 385 EventHandler • 167
CTS-60, 249, 261, 315 Eventos • 142
delegate- 151
delegates • ver delegates
D event • 162
na plataforma .NET • 162
um exemplo • 165
DataContext • 427
Excepções • 139
Deadlock - 345
bloco try-catch • 123
Decimal • 13, 60
catch • 123
Default - 26, 208
catch global • 125
Delegate- 147, 151
checked • 137
Delegates • 150
de aritmética • 137
multicast • 154
estrutura genérica • 129
Delphi • 5, 45
fmally • 124
Deserialize-319 hierarquia • 135
Destrutores lançamento • 132
close - 235
propagação • 134
díspose • 235
try • 123
finalize • 235
unchecked • 137
idisposable • 238
Exception • 132
sintaxe • 233
Explicit • 215
using • 238
Expressões • 20
Dictionary • 278,285
de consulta • 180, 402
DictionaryEntry • 288
lambda- 158, 186
Directory • 301
Expressões regulares
Directorylnfo • 301
agrupamento de expressões • 272
DISCO • 386 capturas • 273
Dispose • 235 match • 272
DLL • 65, 100, 169
regex • ver regex
DNS • 375 símbolos • 274
Do-27 uso de parêntesis rectos • 271, 275
Documentação em XML • 244
Extern - 100
Double • 13, 60
Dynamic Link Library • Ver DLL

Else • 24 False - 16
EnableSession • 389 Ficheiros • Ver também Streams
Encapsulamento • 47 de texto-311
Enum- 112 gestão do sistema de ficheiros - 301
Enumerações • 112 hierarquia de classes • 301
Enumeradores • 187, 190, 198 leitura e escrita • 307
Equals-251 File • 301
Eratóstenes • cer crivo de Erastóstenes FileAccess • 310
Error-261 FileInfo-301
FileMode-310
© FCA - Editora de Informática 449
C#3.5

FileNotFoundException • 125 ICollection • 276, 277


FileStream - 122, 308, 309, 310 iComparable • 292
FileSystemlnfo - 301 IComparer • 292
Finalize • 235 IDisposable • 238
Finally • 124 IEnurnerable-187, 189
Fixed • 236, 239 lEnumerator- 190, 192
Float- 13,60 If-24
Flush • 392 IFormattable-251,268
For • 28 IIS • 380, 385
Foreach • 29, 276 IIdasm.exe • 344
FormatException • 270 Implicít - 214, 216
formato científico • 14 . In-261
FTP - 371 Inferência automática de tipos -181
From • verlinq expressões de consulta, 184
expressões lambda, 186
tabelas, 183
variáveis, 181
Inherited • 172
Iniciadores de objectos • 75
Garbage collector • 2, 59, 60, 223, 226, 232, 234,
Instância • 48
237
Int • 12, 60
GC • 237
GDI+ • 443 Intlô • 60
Int32 • 60
Genéricos • 201-9
Int64 • 60
default - 208
definição • 203 Interface • 102
Interfaces • 101
métodos • 205
Get • 145 herança • 105
GetCustomAttributes • 175 Internai • 64, 65, 100
Internet • 369
GetHashCode-253,287
abertura de ligação para URI • 369, 371
GetHostByAddress • 376
configuração de proxies - 374
GetHostByNaine • 376
download a partir de URI • 369
GetObjectData • 322
resolução de endereços • 375
Globalization • 442
GlobalProxySelectíon • 375 TCP/EP- ver TCP/IP
upload para URI-371
Goto-27, 31
Group • ver linq web services • ver web services
WebClient • 369
WebRequest • 373
WebResponse • 373
H Internet Information Server • ver HS
Interrupt • 335
Hash • 253 Into • ver linq
Hashtable - 254, 278, 285 InvalidOperationExceptíon • 192
cálculo de valores de hash • 253, 254 lOException • 123
Heap-57, 60,109,223,225 IPAddress • 375
Herança • 48 IPEndPoint • 397
Hexadecimal • 13 IpHostEntry • 375
Hierarquia de classes • 51 Is • 22, 107
ISeríalizable • 322
iteradores • 187

ICloneable - 257

45O © FCA - Editora de Informática


ÍNDICE REMISSIVO

com número arbitrário de parâmetros • 227


de extensão • 230
externos • 100
Jagged arrays • Ver Tabelas dentro de tabelas gestão de versões • 94
Java • I, 2, 4, 5, 27, 39,45, 86, 131, 220, 276, modificadores de acesso • 100
369, 380 overloadíng • 83
Join • 336, verlinq parcialmente definidos • 115
passagem de parâmetros • 86
passagem por referência • 87
L selados • 96
variáveis de saída • 88
Leitores • 365 visibilidade de variáveis • 83
Length • 264 Microsoft Interrnediate Language • Ver MSIL
Let • ver linq Monitores • 348
LinkedList • 278, 280 enter - 349
Linq • 401, ver express\oes de consulta exit • 349
Agregaao -419 funcionamento • 349
from" 181,402,403 monitor • 349
group-181,402,407 produtor/consumidor com buffer finito • 350,
inser... e actualizaao de elementos • 181,432 362
imo-181,402,409 pulse e PulseAll • 349
join-181,402,411 relação com lock • 349
let-181,402,416 wait • 349
orderby 1.81, 402, 410 MSIL • 3, 344
select-181,402,406 Multicast delegates • ver delegates, multlcast
to SQL classes-426 Mutex • 346
where-181,402,405 Mutual exclusion lock • ver Mutex
List • 278
List<^T> • 27S
Literais N
cadeias de caracteres • 18
carácter • 17 Namespace • Ver Espaços de nomes
lógicos • 16 New-49, 51, 53, 57, 92, 94, 100
numéricos • 13,14 NonSerialized • 321
Lock • 341 Nonsígnaled • 364
Long • 13, 60 NorthWind • 424
Null • 59, 74
Null coalescing operator • 219
M Nullabletypes-218
Nnúmeros primos • 283
Main • 8
Malloc - 225
Managed code • 2 O
Managed types • 223
ManualResetEvent • 364 Object • 58, 235, 249, ver também objectos
MarshalByRefObject • 301 equals • ver equals
Match • Ver Expressões regulares GetHashCode • ver GetHashCode
Matches • 272 MemberwiseCIone • ver MemberwiseCIone
MemberwiseCIone • 257, 258 monitor associado • 349
MemoryStream • 308, 309 ReferenceEquals • ver ReferenceEquals
Methodlnfo • 108 ToString • Ver ToString
Métodos • 8 Object-Oriented Programming • 45
abstractos • 97 Objectos • 48
anónimos • 155

© FCA - Editora de Informática 451


C#3.5

comparação • 251 value • 145


cópia • 254 virtuais • 146
iniciadores • ver iniciadores de objectos \vrite-only • 146
serialização • ver Serialização de objectos Protected • 64, 100
Obsolete • 168 internai • 64, 65, 100
OnCIick-,166 Proxies • 374
Operadores • 20 Proxy • 374
aderência a nulo • 219 Public • 47, 63, 64, 100
associados a ponteiros • 221 Pulse • 349
conversões definidas pelo utilizador • 214 PulseAll - 350
operator-211
precedência • 20
redefinição • 210
restrições na redefinição • 213
Q
sizeof-224
QueryString • 372
Operator-211
Queue • 278, 297
Orderby • ver línq
Ouf89, 261
Overflow • 137
OverflowException • 138 R
Overloading • 83, ver métodos, redefinição
Override • 54, 93, 95, 99, 100 ver também Read • 260
métodos, redefinição ReadLine • 260
Overriding • Ver Métodos, redefinição Readonly • 67
Redefinição de operadores • ver operadores,
redefinição
Ref-87, 110
Reference types • 57, 109
ReferenceEquals • 252
Params • 228
Referências • 57
Path • 301
Reflexão • 108, 174
Polimorfismo • 52, 93
Regex • 272
Ponteiros • 220
ReleaseMutex • 328, 346
aritmética • 223
Remoting - 369, 442
operador de endereço (&) • 221
Resume • 335
operador de indirecção (->)• 222
Return • 82
operador de valor (&)• 221
RuntimeType Identification- 108
operador sizeof • 224
para membros de classes • 225
relação com tabelas • 224
sintaxe • 221
Pré-processarnento • 240
Private-47, 63, 64,100 Sbyte • 12, 60
Produtor/consumidor • 350, 362 Schema Defínitíon Language • verXSD
Programação declarativa • ver atributos Sealed • 96
Propriedades • 141 override • 100
automáticas • 146 SecurityException • 302
accessor methods • 145 Select • ver linq
declaradas em interfaces • 146 Sequência de escape • 1.7
get • 145 SeriaIizable-319
get accessor • 145 Serialização de objectos • 318
indexadas • 147 formato binário • 319
read only • 146 formato XML • 326
set • 145 GetObjectData • 322
set accessor • 145 ISerializable - 322

452 © FCA - Editora de Informática


ÍNDICE REMISSIVO

NonSerialized • 321 hierarquia de classes • 308, 309


restrições à serialização em XML • 326 Memory Stream • 309
Serializable-319 princípio de funcionamento • 307
SerializationBxception • 320 Stream • 309
Serializationlnfo • 322 StreamReader • ver StreamReader
SeriaIize-319 StreamWriter • ver StreamWriter
Server • 387 StringReader • 309
Session • 387, 388 StringWriter-309
Set • 145 StreamWriter • 308, 309, 311, 314
Short • 12, 60 String • 17, 18, 60, 262, 292 Ver também
Signaled • 364 Cadelas de caracteres
Sirnple Object Access Protocol • ver SOAP StringBuilder-265
Sincronização • 338 StringReader • 309
AutoResetEvent • 364 Strings • Ver Cadeias de caracteres
deadlock • 327, 345 StringWriter • 309
lock-323,41 Struct- 110
lock a membros estáticas • 345 SuppressFinalíze • 237
lock encadeado • 345 Suspend • 335
ManualResetEvent • 364 Switch • 26
monitores • ver monitores
mutex • 346
ReaderWriterLock • 365 T
secções críticas -341, 343
thread safe • 341 Tabela de hash • Ver Hashtable
TnreadPool • 365 Tabela dinâmica • 189, 196, 201, 27S
timer • 367 Tabelas
Single • 60 argumentos de Main • 37
Sizeof • 224 associadas a ponteiros • 224
Sleep • 333 associativas - 278
SmallTalk-45 cópia • 37
SOAP • 329, 379 definição no stack • 225
SortedDictionary • 278, 290 dentro de tabelas • 40
SortedList • 278,294 rnultidimensionais • 38
Sqrt- 11 número de dimensões • 39
SqlMetal • 424,425 simples • 33
Stack • 60, 83, 110, 130, 220, 223, 225,278, 298 tamanho • 34, 39
Stackalloc • 225 TCP/IP-391
Standard error-261 clientes TCP • 396
Standard input-261 protocolo UDP • 396, 397
Standard output • 261 servidores TCP-392
Standard Tempíate Library • Ver STL TcpClient-392,396
Static • 8, 69, 79, 100 TcpListener • 392
readonly • 80 TextBox • 142
STL • 276 This-76,83, 148
Stream • 309 Thread safe • 341
StreamingContext • 322 ThreadAbortException • 335
StreamReader • 308, 309, 314 ThreadPool • 365
Streams • 300, 307, ver também ficheiros Threads-331
BinaryReader- ver BinaryReader background • 337, 367
BinaryWriter • ver BinaryWriter foreground • 337
BufferedStream • 309 gestão -333
de texto-311 multithreaded • 331
fecho-126, 312 sincronização • ver sincronização
FileStream • verFileStream singlethreaded-331

© FCA - Editora de Informática 453


C#3.5

thrcad • 337 V
timeslice • 333
tíraer • 367
ThreadStart • 333 Value • 145
Throw-132,134 Value types • 257
Timer • 367 teste de igualdade • 256
Tipos Variáveis • 19
anónimos • 179 declaração • 11
anuláveis • 217 Virtual - 54, 93, 100
classes -Ver Classes Visual Basic- 1,4,5,34,446
conversões • Ver Conversões entre tipos Void • 8, S2
enumerações • Ver Enumerações
estruturas • Ver Estruturas
inferência • ver inferência automática de tipos W
lógico • 15, 16
numéricos • 12, 13 Wait • 349
parcialmente definidos • 114 WaitOne - 346
ToString- 61 Web Forras-385, 443
Transrnission Transport Protocol • ver TCP Web Service Description Language • ver WSDL
True- 16 Web services • 4, 379
Try - 123 @WebMethod-381
Type • 108 pplication • 387
Typeof-107,108, 175 clientes • 383
CodeBehind • 385
configuração e instalação - 384
U criação • 381
dados disponíveis a um web servíce • 387
UDP • 396 Disco - 386
UdpCIient • 397 espaços de nomes • 386
Ulnt • 13, 60 informação de aplicação • 387, 388
UInti6-60 informação de sessão • 387, 388
UInt32 • 60 session • 387
UInt64 • 60 thread safeness • 389
Ulong -13, 60 WebMethod-381
Unboxing • Ver boxing/unboxing WebService-381,387
Unchecked • 137 Web.config • 386
Underflow • 138 WebClient-369
Unicode-16, 17,314 WebException • 384
UnicodeEncoding • 314 WebMethod-381
UTF7Encoding-314 WebRequest • 373
UTF-8-314 WebResponse • 373
UnicodeEncoding • 314 WebService-381
Uniform Resource Identifier • Ver URI Where • ver linq
Unsafe - 220 While • 27
URI • 371, 377, 375, 337, 371 Windows Forms • 3,165, 443
classe Uri • 377 Write-311
manipulação • 377 WriteLine- 9, 250,311
UriBuilder-377 número arbitrário de parâmetros • 12, 229
User•388 WSDL-380, 386
User Datagram Protocol • ver UDP wsdl.exe - 383, 384
Ushort-12,60
Using-8, 116, 118,238
UTF7Encoding-314
UTF-8-314

454 © FCA - Editora de Informática


ÍNDICE REMISSIVO

X XmISeriiiIizer-318,326
XinlElement • 328
XSD • 327, 328
XML-308, 327 xsd.exe - 328
documentação de código • 244
linq • 434
relação com SOAP • 380 y
schema • 328
seríalização de objectos • VerSerializaçSo de
objectos • formato XML yield • 196

© FCA - Editora de Informática 455


PUBLICAÇÕES

|FCA

PROGRAMAÇÃO PARA DISPOSITIVOS MÓVEIS EM WINDOWS MOBILE 6 CURSO COMPLETO


Ricardo Queirós
Programação
para DÍSpOSÍtÍYOS MÓVGÍS
WINBOWI noeiu t
Hoje em dia as tecnologias de informação e comunicação são um lugar-comum em casa, no
escritório, na sala de aulas ou mesmo numa conversa de café. A plataforma Windows Mobile
vem enriquecer o panorama do mundo dos dispositivos móveis ao oferecer aos equipamentos,
um conjunto de serviços base essenciais, que dísponibilizam ao utilizador final, mecanismos
promotores da produtividade, flexibilidade e consistência nos seus processos de trabalho. Neste
contexto, estuda-se, na óptica do programador, a plataforma .NET, a sua Framework de
desenvolvimento móvel (.NET Compact Framework 3.5), o seu ambiente de desenvolvimento
integrado (Visual Studio 200S), a sua linguagem de programação (Visual Basic 9.0) e um
conjunto de ferramentas e extensões que permitem a produção célere e consistente de software
para equipamentos móveis, nomeadamente o SDK para o Windows Mobile 6.0.

ISBN: 978-972-722-557-6
N°Págs.:488 Conteúdos: Evolução dos equipamentos Windows CE e Windows Mobile; • A plataforma
.NET e a Compact Framework 3.5; • Criação de um projecto Windows Mobile; • Programação
orientada a objectos; • O Visual Studio e o desenho de interfaces; • Teste, debugging e gestão
de erros; • Empacotamento, distribuição e instalação; • Persistência de dados com o SQL Server
Compact 3.5; • Web Services; Tópicos avançados; • SDK Windows Mobile 6.0 .

ASP:NET3.5 CURSO COMPLETO


Luís Abreu e João Paulo Carreiro

A plataforma .NET tem sido adoptada pela comunidade de desenvolvimento Web desde o seu
lançamento, em 2002. De forma a fornecer um melhor desempenho, flexibilidade e redução no
trabalho de codificação, a Microsoft lança agora o ASP.NET 3.5. Este livro, com vários
exemplos práticos, apresenta as principais características relacionadas com a construção de
aplicações Web através do ASP.NET 3.5. Inicia com a apresentação da framework que serve de
suporte ao desenvolvimento de páginas (Web forms e ASP.NET server controis simples) e
introduz gradualmente todas as novas funcíonalidades disponibilizadas.
Esta obra tem como objectivo ensinar o programador que se está a iniciar na plataforma
ASP.NET, sendo também uma ferramenta indispensável para o programador conhecedor da
framework ASP.NET que pretende fazer a transição para a nova versão.
ISBN: 978-972-722-615-3
N"l'ágs.:8l6 Conteúdos: Validação de dados e scripts no lado cliente • Configuração de aplicações e
tratamento de erros • Controlos data source e controlos data bound • Serviços, segurança e
perfis de utilizadores • User controis, master pages, themes e skins * Localização de aplicações
• Ciclo de vida de uma página • Handlers e módulos • ASP.NET AJAX.

Encomende no s/fornecedor habitual

ou na n/Distribuidora L1DEL
SAIBA MAIS SOBRE NÓS

EM WWW.FGA.PT

VISUAL BASIC 2OO8 CURSO COMPLETO


Henrique Loureiro

VISUAL BASIC Este livro cobre, de uma forma clara e acessível, as técnicas de programação em Visual Basic
2008 m*wwm associadas à tecnologia .NET. As matérias são apresentadas passo a passo e reforçadas com
uma vasta componente prática, composta por exercícios resolvidos e propostos. Expondo os
temas de forma objectiva e aliando a teoria à prática, é uma obra útil tanto para os que estão a
começar a dar os primeiros passos na programação em Visual Basic, como para os que já
possuem conhecimentos nesta área.
Inclui noções gerais de algoritmia e de usabilidade, destinadas a quem nunca programou. No
final de cada capítulo, o leitor encontra teses de consolidação que lhe permitem avaliar os
conhecimentos adquiridos.
Conteúdos: Introdução à programação • Princípios do modelo de programação orientada a objectos *
ISBN: 973-972-722-294-0 Desenho de interfaces gráficas (Windows Forms e Windows Presentation Foundation) • Gestão e
N" Págs.: 448 manipulação de bases de dados relacionais (SQL Server e Access) com ADO.NET * Desenvolvimento
Completo de um projecto baseado num caso real.

ESTRUTURAS DE DADOS E ALGORITMOS EM C TECNOLOGIAS DE INFORMAÇÃO


António Adrego da Rocha

Esta obra tem como objectivo fornecer uma competência sólida no desenvolvimento de programas de
média e elevada complexidade e um conhecimento profundo sobre estruturas de dados avançadas e
algoritmos complexos, usando a linguagem de programação C e aplicando o paradigma da
programação modular. Assim, utiliza uma metodologia que dá particular ênfase à decomposição
funcional das soluções, através da implementação de tipos abstractos de dados. Para atingir este
objectivo, ela está organizada em quatro grandes temas: O estudo das principais estruturas de dados
dinâmicas; O estudo das principais classes de algoritmos, tendo ern consideração a sua complexidade;
O estudo da implementação dos diferentes tipos de memórias; O estudo do tipo abstracto de dados
grafo (Graph), com especial destaque para a implementação dinâmica baseada em listas ligadas, e dos
seus algoritmos mais importantes.
ISBN: 578-972-722-295-7
N° Págs.: 520 Conteúdos: Recursividade • Estruturas de Dados Dinâmicas • Programação Modular • Memórias
Associativas • Memórias de Acesso Aleatório • Pesquisa, Selecção e Ordenação * Filas e Pilhas •
Grafos.

Contacte-nos por e-mail:

j fca@fcs.pt
INTELIGÊNCIA ARTIFICIAL - Fundamentos e Aplicações 2a Edição Rev, e Aum.
Tecnologias de Informação

Os últimos anos vieram confirmar a centralidade do conceito de agente em Inteligência Artificial,


abondagcm seguida na presente obra. Mas algo mudou entretanto: a percepção de que não existe uma IA
mas várias e que, cada vez mais, as diferentes perspectivas não concorrem entre si mas antes se
complementam. Este foi, desde o início, o ponto de vista inovador desta obra. que o tempo agora
confirma. Nesta 2.n Edição, Revista e Aumentada, foi dada especial atenção às sugestões e aos
comentários críticos de diversos leitores. Em particular, para além da reescrita de diversas partes do livro,
foram adicionados novos temas envolvendo máquinas de vector de suporte (agentes aprendizes) e
modelos e arquitecturas BDI (sociedades de agentes).
Conteúdos: Agentes Reactivos • Agentes de Procura • Agentes Baseados em Conhecimento • Agentes
Aprendizes • Agentes Adaplativos • Conhecimento Imperfeito • Interacção com o ambiente • Sociedades
de Agentes.

ANÁLISE INTELIGENTE DE DADOS Tecnologias de informação


Miguel Rocha / Paulo Cortez /José Maia Neves

A Análise Inteligente de Dados (AID) aborda todo um conjunto de algoritmos computacionais que
permitem processar e analisar dados em bruto, de modo a extrair informação útil. Este novo campo
de investigação define-se na intersecção de áreas como a Estatística, a Inteligência Artificial, a
Aprendizagem Automática, as Bases de Dados e os Sistemas de Informação. Este livro aborda a
AID, com especial destaque para a aprendizagem supervisionada. No início são introduzidos os
conceitos básicos, seguindo-se uma descrição detalhada de diversos tipos de algoritmos
supervisionados, tais como: Árvores de Decisão e Regressão, Regras de Classificação e Regressão,
Modelos Lineares e Redes Neuronais Artificiais. Para além da descrição de cada algoritmo, é
apresentada uma implementação do mesmo em Java, sendo também apresentados diversos
exemplos de aplicação. Por fim, explica-se como se podem avaliar e comparar algoritmos de AID,
descrevendo-se também quais os tópicos avançados nesta área. E um livro interesse para alunos e
profissionais das áreas das TI e Comunicação.
Conteúdo- Introdução e conceitos básicos sobre a Análise Inteligente de Dados (AID) -
Al " Stmo de aprendizagem supervisionada - Aplicações da AID em problemas do mundo real •
AvTaçTe comparação^ Algoritmos de AID - Tópicos avançados: Comunas de Modelos
Sistemas Híbridos, Selecção de Modelos, Meta-Aprend.zagem e Aprendizagem Dinâmica
Implementação em Java de diversos algoritmos apresentados.
cnologias
Informação

C# é a linguagem de programação criada pela Microsoft e especialmente pensada para


O o desenvolvimento de aplicações na plataforma .NET. Aliando todo o poder do C++ com a facilidade
de programação do Visual Basic, o C# é uma linguagem rápida e moderna, desenhada especificamente
para aumentar a produtividade dos programadores.

Este livro é destinado a todos os profissionais, investigadores e estudantes universitários que adoptem a
linguagem C# para o desenvolvimento de aplicações. Esta nova edição trata de forma completa a versão
3.0 da linguagem C# e a versão 3.5 da plataforma .NET.

Os tópicos cobertos incluem:


• Programação orientada a< • Ficheiros, sfrr.mf-e seri.ili/
• Programação baseada em componente- • 10 i mu onenlc:
• Tratamento de erros baseado em » Acesso à Internei
e Genéricos; • UNO.

^ -* 51 O código fonte dos programas incluídos no livro está disponível em :a.pt.

Paulo Marques - Docenle e investigador do Departamento de Engenharia Informática da Universidade


cie Coimbra. É coordenador do Profcssional Mester oí SoHw<irr Engitwering, oferecido conjuntamente pela
Universidade de Coimbra e pela Universidade de Carnegie Mellon, nos Estados Unidos. Durante os últimos
anos liderou uma série de projectos em colaboração com instituições como a Agência Espacial Europeia
e a Microsoft Research. Foi responsável pelo sistema RAIL (Runtinw Assemb/y InstrumontJtion í./7?r<iry)
para instrumentação de código na plataforma .NET.

Hernâni Pedroso - Mestre em Engenharia Informática pela Universidade de Coimbra. Desenvolveu a sua
actividade profissional principalmente na empresa Criticai Software, S. A., em soTtware para sistemas
críticos. Foi um dos principais arquitectos do sistema WMPI (Windows Mvssage Passin» Interface). A sua
investigação teve um enfoque especial nas áreas de sistemas distribuídos e paralelos, tolerância a talhas
e computação de elevado desempenho.

?,CpMr1FÍ8Ud^TéCníC° * lnl°rmática de Gesta°- Encontra-se Ii8ado ao desenvolvimento de projectos


í Aplicações Windows numa grande instituição bancária portuguesa. É o fundador da maior
comun.dade portuguesa de programadores .NET (PontoNetPT), tendo sido reconhecido pela Mc™
Portugal como MVP - Afcg V*/«6fe ftvfevianal - na ãrea de V,,,,., Oel,/0/«, Po , ° ai ca um Wo
pessoa, onde apresenta as novidades mais recentes sobre a t ecno,og,a .NET S S ^ i

ISBN 978-972-722^03-6

Anda mungkin juga menyukai