Você está na página 1de 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