Skip to content

3. Noções básicas da linguagem C#

3.1. Introduction

Vamos abordar o C#, em primeiro lugar, como uma linguagem de programação clássica. Abordaremos as classes mais tarde. Num programa, encontramos duas coisas:

  • dados
  • as instruções que os manipulam

Normalmente, procuramos separar os dados das instruções:

3.2. Os dados do C#

O C# utiliza os seguintes tipos de dados:

  1. números inteiros
  1. números reais
  2. números decimais
  3. caracteres e cadeias de caracteres
  4. valores booleanos
  5. os objetos

3.2.1. Os tipos de dados predefinidos

Tipo C#
Tipo .NET
Dado representado
Sufixo
dos valores literais
Codificação
Intervalo de valores
char
Caractere(s)
carácter
 
2 bytes
carácter Unicode (UTF-16)
string
String (C)
cadeia de caracteres
  
referência a uma sequência de caracteres Unicode
int
Int32 (S)
número inteiro
 
4 bytes
[-231, 231-1] [–2147483648, 2147483647]
uint
UInt32 (S)
..
U
4 octetos
[0, 232-1] [0, 4294967295]
long
Int64 (S)
..
L
8 octetos
[-263, 263 -1] [–9223372036854775808, 9223372036854775807]
ulong
UInt64 (S)
..
UL
8 octetos
[0, 264 -1] [0, 18446744073709551615]
sbyte
 
..
 
1 byte
[-27 , 27 -1] [-128,+127]
byte
Byte(s)
..
 
1 byte
[0 , 28 -1] [0,255]
short
Int16 (S)
..
 
2 bytes
[-215, 215-1] [-32768, 32767]
ushort
UInt16 (S)
..
 
2 bytes
[0, 216-1] [0,65535]
float
Individual (S)
número real
F
4 bytes
[1.5 10-45, 3.4 10+38] em valor absoluto
double
Duplo (S)
..
D
8 octetos
[-1.7 10+308, 1.7 10+308] em valor absoluto
decimal
Decimal (S)
número decimal
M
16 bytes
[1.0 10-28,7.9 10+28] em valor absoluto com 28 algarismos significativos
bool
Booleano (S)
..
 
1 byte
true, false
object
Objeto (C)
referência do objeto
  
referência de objeto

Acima, colocámos em paralelo os tipos C# e os seus tipos .NET equivalentes, com o comentário (S) se esse tipo for uma estrutura e (C) se for uma classe. Verifica-se que existem dois tipos possíveis para um inteiro de 32 bits: int e Int32. O tipo int é um tipo C#. Int32 é uma estrutura pertencente ao espaço de nomes System. O seu nome completo é, portanto, System.Int32. O tipo int é um alias em C# que designa a estrutura .NET System.Int32. Da mesma forma, o tipo C# string é um alias para o tipo .NET System.String. System.String é uma classe e não uma estrutura. Os dois conceitos são semelhantes, mas apresentam a seguinte diferença fundamental:

  • uma variável do tipo Structure é manipulada através do seu valor
  • uma variável do tipo Classe é manipulada através do seu endereço (referência na linguagem orientada para objetos).

Tanto um structure como um classe são tipos complexos com atributos e métodos. Assim, poderemos escrever:

string nomDuType=3.GetType().FullName;

No exemplo acima, o literal 3 é, por predefinição, do tipo C# int, ou seja, do tipo .NET System.Int32. Esta estrutura possui um método GetType() que devolve um objeto que encapsula as características do tipo de dados System.Int32. Entre estas, a propriedade FullName devolve o nome completo do tipo. Vemos, portanto, que o literal 3 é um objeto mais complexo do que parece à primeira vista.

Eis um programa que ilustra estes diferentes pontos:


using System;

namespace Chap1 {
    class P00 {
        static void Main(string[] args) {
            // exemplo 1
            int ent = 2;
            float fl = 10.5F;
            double d = -4.6;
            string s = "essai";
            uint ui = 5;
            long l = 1000;
            ulong ul = 1001;
            byte octet = 5;
            short sh = -4;
            ushort ush = 10;
            decimal dec = 10.67M;
            bool b = true;
            Console.WriteLine("Type de ent[{1}] : [{0},{2}]", ent.GetType().FullName, ent,sizeof(int));
            Console.WriteLine("Type de fl[{1}]: [{0},{2}]", fl.GetType().FullName, fl, sizeof(float));
            Console.WriteLine("Type de d[{1}] : [{0},{2}]", d.GetType().FullName, d, sizeof(double));
            Console.WriteLine("Type de s[{1}] : [{0}]", s.GetType().FullName, s);
            Console.WriteLine("Type de ui[{1}] : [{0},{2}]", ui.GetType().FullName, ui, sizeof(uint));
            Console.WriteLine("Type de l[{1}] : [{0},{2}]", l.GetType().FullName, l, sizeof(long));
            Console.WriteLine("Type de ul[{1}] : [{0},{2}]", ul.GetType().FullName, ul, sizeof(ulong));
            Console.WriteLine("Type de b[{1}] : [{0},{2}]", octet.GetType().FullName, octet, sizeof(byte));
            Console.WriteLine("Type de sh[{1}] : [{0},{2}]", sh.GetType().FullName, sh, sizeof(short));
            Console.WriteLine("Type de ush[{1}] : [{0},{2}]", ush.GetType().FullName, ush, sizeof(ushort));
            Console.WriteLine("Type de dec[{1}] : [{0},{2}]", dec.GetType().FullName, dec, sizeof(decimal));
            Console.WriteLine("Type de b[{1}] : [{0},{2}]", b.GetType().FullName, b, sizeof(bool));
        }
    }
}
  • linha 7: declaração de um inteiro ent
  • linha 19: o tipo de uma variável v pode ser obtido através de v.GetType().FullName. O tamanho de uma estrutura S pode ser obtido através de sizeof(S). A instrução Console.WriteLine("... {0} ... {1} ...", param0, param1, ...) exibe no ecrã o texto que constitui o seu primeiro parâmetro, substituindo cada notação {i} pelo valor da expressão parami.
  • linha 22: como o tipo string designa uma classe e não uma estrutura, não é possível utilizar o operador sizeof.

Eis o resultado da execução:

Type de ent[2] : [System.Int32,4]
Type de fl[10,5]: [System.Single,4]
Type de d[-4,6] : [System.Double,8]
Type de s[essai] : [System.String]
Type de ui[5] : [System.UInt32,4]
Type de l[1000] : [System.Int64,8]
Type de ul[1001] : [System.UInt64,8]
Type de b[5] : [System.Byte,1]
Type de sh[-4] : [System.Int16,2]
Type de ush[10] : [System.UInt16,2]
Type de dec[10,67] : [System.Decimal,16]
Type de b[True] : [System.Boolean,1]

A exibição apresenta os tipos .NET e não os aliases C#.

3.2.2. Notação dos dados literais

entier int (32 bits)
145, -7, 0xFF (hexadecimal)
entier long (64 bits) - suffixe L
100000L
réel double
134,789, -45E-18 (-45 × 10⁻¹⁸)
réel float (suffixe F)
134.789F, -45E-18F (-45 10-18)
réel decimal (suffixe M)
100000M
caractère char
'A', 'b'
chaîne de caractères string
"hoje" "c:\\capítulo1\\parágrafo3" @"c:\capítulo1\parágrafo3"
booléen bool
true, false
date
new DateTime(1954,10,13) (ano, mês, dia) para 13/10/1954

Note-se as duas cadeias literais: "c:\\chap1\\paragraph3" e @"c:\chap1\paragraph3". Nas cadeias literais, o carácter \ é interpretado. Assim, «\n» representa o símbolo de fim de linha e não a sucessão dos dois caracteres \ e n. Se se quisesse essa sucessão, seria necessário escrever «\\n», em que a sequência \\ é interpretada como um único caractere \. Também se poderia escrever @"\n" para obter o mesmo resultado. A sintaxe @"texto" exige que o texto seja interpretado exatamente como está escrito. Por vezes, chama-se a isto uma cadeia verbatim.

3.2.3. Declaração de dados

3.2.3.1. Função das declarações

Um programa manipula dados caracterizados por um nome e um tipo. Estes dados são armazenados na memória. No momento da compilação do programa, o compilador atribui a cada dado um local na memória caracterizado por um endereço e um tamanho. Faz-o com base nas declarações feitas pelo programador.

Além disso, estas permitem ao compilador detetar erros de programação. Assim, a operação

x=x*2;

será declarada como errada se x for, por exemplo, uma cadeia de caracteres.

3.2.3.2. Declaração de constantes

A sintaxe para declarar uma constante é a seguinte:

    const type nom=valeur;          //define constante nome=valor

Por exemplo:

const float myPI=3.141592F;    

Por que declarar constantes?

  1. A leitura do programa será mais fácil se se atribuir à constante um nome significativo:
    const  float taux_tva=0.186F;
  1. A modificação do programa será mais fácil se a «constante» vier a mudar. Assim, no caso anterior, se a taxa de IVA passar para 33%, a única alteração a fazer será modificar a instrução que define o seu valor:
    const float taux_tva=0.33F;

Se tivéssemos utilizado explicitamente 0,186 no programa, seria necessário alterar inúmeras instruções.

3.2.3.3. Declaração de variáveis

Uma variável é identificada por um nome e está associada a um tipo de dados. O C# distingue entre maiúsculas e minúsculas. Assim, as variáveis FIN e fin são diferentes.

As variáveis podem ser inicializadas no momento da sua declaração. A sintaxe para declarar uma ou mais variáveis é:

 Identificateur_de_type variable1[=valeur1],variable2=[valeur2],...;

onde Identificateur_de_type é um tipo predefinido ou um tipo definido pelo programador. Opcionalmente, uma variável pode ser inicializada ao mesmo tempo que é declarada.

Também é possível não especificar o tipo exato de uma variável, utilizando a palavra-chave var em vez de Identificateur_de_type:

 var variable1=valeur1,variable2=valeur2,...;

A palavra-chave var não significa que as variáveis não tenham um tipo específico. A variável variablei tem o tipo dos dados valeuri que lhe são atribuídos. A inicialização é aqui obrigatória para que o compilador possa deduzir o tipo da variável.

Eis um exemplo:


using System;

namespace Chap1 {
    class P00 {
        static void Main(string[] args) {
            int i=2;
            Console.WriteLine("Type de int i=2 : {0},{1}",i.GetType().Name,i.GetType().FullName);
            var j = 3;
            Console.WriteLine("Type de var j=3 : {0},{1}", j.GetType().Name, j.GetType().FullName);
            var aujourdhui = DateTime.Now;
            Console.WriteLine("Type de var aujourdhui : {0},{1}", aujourdhui.GetType().Name, aujourdhui.GetType().FullName);
        }
    }
}
  • linha 6: um dado explicitamente tipado
  • linha 7: (dado).GetType().Name é o nome abreviado de (dado), (dado).GetType().FullName é o nome completo de (dado)
  • linha 8: um dado tipado implicitamente. Como 3 é do tipo int, j será do tipo int.
  • linha 10: um dado tipado implicitamente. Como DateTime.Now é do tipo DateTime, aujourdhui será do tipo DateTime.

Na execução, obtém-se o seguinte resultado:

1
2
3
Type de int i=2 : Int32,System.Int32
Type de var j=3 : Int32,System.Int32
Type de var aujourdhui : DateTime,System.DateTime

Uma variável cujo tipo é definido implicitamente pela palavra-chave var não pode, posteriormente, mudar de tipo. Assim, não seria possível escrever, após a linha 10 do código, a linha:


            var aujourdhui = "aujourd'hui";

Veremos mais adiante que é possível declarar um tipo «em tempo real» numa expressão. Trata-se, nesse caso, de um tipo anónimo, ou seja, um tipo ao qual o utilizador não atribuiu um nome. É o compilador que atribuirá um nome a esse novo tipo. Se um valor de tipo anónimo tiver de ser atribuído a uma variável, a única forma de declarar essa variável é utilizar a palavra-chave var.

3.2.4. Conversões entre números e cadeias de caracteres

nombre -> chaîne
nombre.ToString()
chaine -> int
int.Parse(cadeia) ou System.Int32.Parse
chaîne -> long
long.Parse(cadeia) ou System.Int64.Parse
chaîne -> double
double.Parse (cadeia) ou System.Double.Parse (cadeia)
chaîne -> float
float.Parse (cadeia) ou System.Float.Parse (cadeia)

A conversão de uma cadeia de caracteres num número pode falhar se a cadeia não representar um número válido. Nesse caso, é gerado um erro fatal denominado «exceção». Este erro pode ser tratado pela seguinte cláusula try/catch:


try{
    appel de la fonction susceptible de générer l'exception
} catch (Exception e){
    traiter l'exception e
}
instruction suivante

Se a função não gerar uma exceção, passa-se então para a instrução seguinte; caso contrário, passa-se para o corpo da cláusula catch e, em seguida, para a instrução seguinte. Voltaremos mais tarde à gestão de exceções. Eis um programa que apresenta algumas técnicas de conversão entre números e cadeias de caracteres. Neste exemplo, a função affiche exibe no ecrã o valor do seu parâmetro. Assim, affiche(S) exibe o valor de S no ecrã, sendo que S é do tipo string.


using System;

namespace Chap1 {
    class P01 {
        static void Main(string[] args) {

            // dados
            const int i = 10;
            const long l = 100000;
            const float f = 45.78F;
            double d = -14.98;

            // número --> cadeia
            affiche(i.ToString());
            affiche(l.ToString());
            affiche(f.ToString());
            affiche(d.ToString());

            //booleano --> cadeia
            const bool b = false;
            affiche(b.ToString());

            // cadeia de caracteres --> inteiro
            int i1;
            i1 = int.Parse("10");
            affiche(i1.ToString());
            try {
                i1 = int.Parse("10.67");
                affiche(i1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // cadeia de caracteres --> inteiro longo
            long l1;
            l1 = long.Parse("100");
            affiche(l1.ToString());
            try {
                l1 = long.Parse("10.675");
                affiche(l1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // cadeia de caracteres --> double
            double d1;
            d1 = double.Parse("100,87");
            affiche(d1.ToString());
            try {
                d1 = double.Parse("abcd");
                affiche(d1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

            // cadeia de caracteres --> float
            float f1;
            f1 = float.Parse("100,87");
            affiche(f1.ToString());
            try {
                d1 = float.Parse("abcd");
                affiche(f1.ToString());
            } catch (Exception e) {
                affiche("Erreur : " + e.Message);
            }

        }// fim main

        public static void affiche(string S) {
            Console.Out.WriteLine("S={0}",S);
        }
    }// fim da classe
}

Nas linhas 30-32, trata-se a eventual exceção que possa ocorrer. e.Message é a mensagem de erro associada à exceção e.

Os resultados obtidos são os seguintes:

S=10
S=100000
S=45,78
S=-14,98
S=False
S=10
S=Erreur : Input string was not in a correct format.
S=100
S=Erreur : Input string was not in a correct format.
S=100,87
S=Erreur : Input string was not in a correct format.
S=100,87
S=Erreur : Input string was not in a correct format.

Note-se que os números reais na forma de cadeia de caracteres devem utilizar a vírgula e não o ponto decimal. Assim, escrever-se-á

double d1=10.7; 

mas

double d2=int.Parse("10,7");

3.2.5. As tabelas de dados

Uma tabela em C# é um objeto que permite agrupar, sob um mesmo identificador, dados do mesmo tipo. A sua declaração é a seguinte:

Type[] tableau[]=new Type[n]

n é o número de dados que a matriz pode conter. A sintaxe Matriz[i] designa o dado n.º i, em que i pertence ao intervalo [0,n-1]. Qualquer referência ao dado Tableau[i] em que i não pertença ao intervalo [0,n-1] provocará uma exceção. Uma matriz pode ser inicializada ao mesmo tempo que é declarada:

    int[] entiers=new int[] {0,10,20,30};

ou, mais simplesmente:

    int[] entiers={0,10,20,30};

Os tabuletos têm uma propriedade Length, que corresponde ao número de elementos do tabuleto.

Um tabuleiro bidimensional pode ser declarado da seguinte forma:

Type[,] tabela = new Type[n,m];

onde n é o número de linhas e m o número de colunas. A sintaxe Tableau[i,j] designa o elemento j da linha i da matriz. A matriz bidimensional também pode ser inicializada ao mesmo tempo que é declarada:

    double[,] réels=new double[,] { {0.5, 1.7}, {8.4, -6}};

ou, mais simplesmente:

    double[,] réels={ {0.5, 1.7}, {8.4, -6}};

O número de elementos em cada uma das dimensões pode ser obtido através do método GetLength(i), em que i=0 representa a dimensão correspondente ao primeiro índice, i=1 a dimensão correspondente ao segundo índice, …

O número total de dimensões é obtido através da propriedade Rank, e o número total de elementos através da propriedade Length.

Uma matriz de matrizes é declarada da seguinte forma:

Type[][] array = new Type[n][];

A declaração acima cria uma matriz com n linhas. Cada elemento matriz[i] é uma referência a uma matriz unidimensional. Estas referências matriz[i] não são inicializadas na declaração acima. O seu valor é a referência nula.

O exemplo abaixo ilustra a criação de um tabuleiro de tabuleiros:


            // um tabuleiro de tabuleiros
            string[][] noms = new string[3][];
            for (int i = 0; i < noms.Length; i++) {
                noms[i] = new string[i + 1];
            }//for
            // inicialização
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    noms[i][j] = "nom" + i + j;
                }//for j
            }//for i
  • linha 2: um tabuleiro noms com 3 elementos do tipo string[][]. Cada elemento é um ponteiro de tabuleiro (uma referência de objeto) cujos elementos são do tipo string[].
  • linhas 3-5: os 3 elementos da matriz noms são inicializados. Cada um «aponta» agora para uma matriz de elementos do tipo string[]. noms[i][j] é o elemento j da matriz do tipo string [] referenciada por noms[i].
  • linha 9: inicialização do elemento «nomes[i][j]» no interior de um ciclo duplo. Aqui, «noms[i]» é um tabuleiro com i+1 elementos. Como «noms[i]» é um tabuleiro, «noms[i].Length» corresponde ao seu número de elementos.

Eis um exemplo que reúne os três tipos de matrizes que acabámos de apresentar:


using System;

namespace Chap1 {
    // matrizes

    using System;

    // classe de teste
    public class P02 {
        public static void Main() {
            // um tabuleiro unidimensional inicializado
            int[] entiers = new int[] { 0, 10, 20, 30 };
            for (int i = 0; i < entiers.Length; i++) {
                Console.Out.WriteLine("entiers[{0}]={1}", i, entiers[i]);
            }//para

            // um tabuleiro de duas dimensões inicializado
            double[,] réels = new double[,] { { 0.5, 1.7 }, { 8.4, -6 } };
            for (int i = 0; i < réels.GetLength(0); i++) {
                for (int j = 0; j < réels.GetLength(1); j++) {
                    Console.Out.WriteLine("réels[{0},{1}]={2}", i, j, réels[i, j]);
                }//for j
            }//for i

            // um tabuleiro de tabuleiros
            string[][] noms = new string[3][];
            for (int i = 0; i < noms.Length; i++) {
                noms[i] = new string[i + 1];
            }//para
            // inicialização
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    noms[i][j] = "nom" + i + j;
                }//para j
            }//para i
            // exibição
            for (int i = 0; i < noms.Length; i++) {
                for (int j = 0; j < noms[i].Length; j++) {
                    Console.Out.WriteLine("noms[{0}][{1}]={2}", i, j, noms[i][j]);
                }//para j
            }//para i
        }//Principal
    }//classe
}//namespace

Ao executar o código, obtemos os seguintes resultados:

entiers[0]=0
entiers[1]=10
entiers[2]=20
entiers[3]=30
réels[0,0]=0,5
réels[0,1]=1,7
réels[1,0]=8,4
réels[1,1]=-6
noms[0][0]=nom00
noms[1][0]=nom10
noms[1][1]=nom11
noms[2][0]=nom20
noms[2][1]=nom21
noms[2][2]=nom22

3.3. As instruções básicas do C#

Distinguem-se

1 as instruções elementares executadas pelo computador.

2 as instruções de controlo do andamento do programa.

As instruções elementares tornam-se evidentes quando se analisa a estrutura de um microcomputador e dos seus periféricos.

  1. leitura de informações provenientes do teclado

  2. processamento de informações

  3. Gravação de informações no ecrã

3.3.1. Gravação no ecrã

Existem várias instruções para escrever no ecrã:

Console.Out.WriteLine(expression)
Console.WriteLine(expression)
Console.Error.WriteLine (expression)

onde expression é qualquer tipo de dados que possa ser convertido numa cadeia de caracteres para ser apresentado no ecrã. Todos os objetos em C# ou .NET têm um método ToString() que é utilizado para efetuar essa conversão.

A classe System.Console dá acesso às operações de escrita no ecrã (Write, WriteLine). A classe Console tem duas propriedades, Out e Error, que são fluxos de escrita do tipo TextWriter:

  • Console.WriteLine() é equivalente a Console.Out.WriteLine() e escreve no fluxo Out normalmente associado ao ecrã.
  • Console.Error.WriteLine() escreve no fluxo Error, que normalmente também está associado ao ecrã.

Os fluxos Out e Error podem ser redirecionados para ficheiros de texto durante a execução do programa, como veremos em breve.

3.3.2. Leitura de dados introduzidos pelo teclado

O fluxo de dados proveniente do teclado é designado pelo objeto Console.In do tipo TextReader. Este tipo de objeto permite ler uma linha de texto com o método ReadLine:

    string ligne=Console.In.ReadLine();

A classe Console disponibiliza um método ReadLine associado, por predefinição, ao fluxo In. Assim, pode escrever-se:

    string ligne=Console.ReadLine();

A linha digitada no teclado é armazenada na variável ligne e pode, em seguida, ser utilizada pelo programa. O fluxo In pode ser redirecionado para um ficheiro, tal como os fluxos Out e Error.

3.3.3. Exemplo de entradas e saídas

Eis um pequeno programa que ilustra as operações de entrada e saída do teclado/ecrã:


using System;

namespace Chap1 {
    // classe de teste
    public class P03 {
        public static void Main() {

            // gravação no fluxo Out
            object obj = new object();
            Console.Out.WriteLine(obj);

            // gravação no fluxo Error
            int i = 10;
            Console.Error.WriteLine("i=" + i);

            // leitura de uma linha introduzida pelo teclado
            Console.Write("Tapez une ligne : ");
            string ligne = Console.ReadLine();
            Console.WriteLine("ligne={0}", ligne);
        }//fim da função main
    }//fim da classe
}
  • linha 9: obj é uma referência de objeto
  • linha 10: obj é exibido no ecrã. Por predefinição, é chamado o método obj.ToString().
  • linha 14: também se pode escrever:

            Console.Error.WriteLine("i={0}",i);

O primeiro parâmetro «i={0}» é o formato de exibição; os restantes parâmetros são as expressões a exibir. Os elementos {n} são parâmetros «posicionais». Durante a execução, o parâmetro {n} é substituído pelo valor da expressão n.º n.

O resultado da execução é o seguinte:

1
2
3
4
System.Object
i=10
Tapez une ligne : je suis là
ligne=je suis là
  • linha 1: a exibição produzida pela linha 10 do código. O método obj.ToString() exibiu o nome do tipo da variável obj: System.Object. O tipo object é um alias em C# do tipo .NET System.Object.

3.3.4. Redirecionamento de E/S

Existem, em DOS e UNIX, três dispositivos padrão denominados:

  1. dispositivo de entrada padrão — designa, por predefinição, o teclado e tem o n.º 0
  2. periférico de saída padrão — designa, por predefinição, o ecrã e tem o n.º 1
  3. dispositivo de erro padrão — designa, por predefinição, o ecrã e tem o n.º 2

Em C#, o fluxo de escrita Console.Out escreve no dispositivo 1, o fluxo de escrita Console.Error escreve no dispositivo 2 e o fluxo de leitura Console.In lê os dados provenientes do dispositivo 0.

Ao iniciar um programa no DOS ou no Unix, é possível definir quais serão os dispositivos 0, 1 e 2 para o programa em execução. Consideremos a seguinte linha de comando:

pg arg1 arg2 .. argn

Através dos argumentos argi do programa pg, é possível redirecionar os dispositivos de E/S padrão para ficheiros:

0<in.txt
o fluxo de entrada padrão n.º 0 é redirecionado para o ficheiro in.txt. No programa, o fluxo Console.In irá, portanto, obter os seus dados do ficheiro in.txt.
1>out.txt
redireciona a saída n.º 1 para o ficheiro out.txt. Isto significa que, no programa, o fluxo Console.Out gravará os seus dados no ficheiro out.txt
1>>out.txt
o mesmo, mas os dados gravados são adicionados ao conteúdo atual do ficheiro out.txt.
2>error.txt
redireciona a saída n.º 2 para o ficheiro error.txt. Isto faz com que, no programa, o fluxo Console.Error grave os seus dados no ficheiro error.txt
2>>error.txt
O mesmo se aplica, mas os dados gravados são adicionados ao conteúdo atual do ficheiro error.txt.
1>out.txt 2>error.txt
Os dispositivos 1 e 2 são ambos redirecionados para ficheiros

Note-se que, para redirecionar os fluxos de E/S do programa pg para ficheiros, o programa pg não precisa de ser alterado. É o sistema operativo que determina a natureza dos dispositivos 0, 1 e 2. Consideremos o seguinte programa:


using System;

namespace Chap1 {

    // redirecionamentos
    public class P04 {
        public static void Main(string[] args) {
            // leitura do fluxo In
            string data = Console.In.ReadLine();
            // gravação do fluxo de saída
            Console.Out.WriteLine("écriture dans flux Out : " + data);
            // gravação do fluxo de erros
            Console.Error.WriteLine("écriture dans flux Error : " + data);
        }//Principal
    }//classe
}

Vamos gerar o executável a partir deste código-fonte:

  • em [1]: o executável é criado clicando com o botão direito do rato no projeto / Build
  • em [2]: numa janela do DOS, o executável 04.exe foi criado na pasta bin/Release do projeto.

Executemos os seguintes comandos na janela do DOS [2]:

1
2
3
4
5
6
7
8
...\04\bin\Release>echo test >in.txt
...\04\bin\Release>more in.txt
test
...\04\bin\Release>04 0<in.txt 1>out.txt 2>err.txt
...\04\bin\Release>more out.txt
écriture dans flux Out : test
...\04\bin\Release>more err.txt
écriture dans flux Error : test
  • linha 1: insere-se a cadeia test no ficheiro in.txt
  • linhas 2-3: exibimos o conteúdo do ficheiro in.txt para verificação
  • linha 4: execução do programa 04.exe. O fluxo In é redirecionado para o ficheiro in.txt, o fluxo Out para o ficheiro out.txt, o fluxo Error para o ficheiro err.txt. A execução não provoca qualquer exibição.
  • linhas 5-6: conteúdo do ficheiro out.txt. Este conteúdo mostra-nos que:
  • o ficheiro in.txt foi lido
  • a saída para o ecrã foi redirecionada para o ficheiro out.txt
  • linhas 7-8: verificação semelhante para o ficheiro err.txt

Vê-se claramente que os fluxos Out e In não escrevem nos mesmos dispositivos, uma vez que foi possível redirecioná-los separadamente.

3.3.5. Atribuição do valor de uma expressão a uma variável

Vamos analisar aqui a operação variable=expression;

A expressão pode ser do tipo: aritmética, relacional, booleana ou de caracteres

3.3.5.1. Interpretação da operação de atribuição

A operação variable=expression;

é, ela própria, uma expressão cuja avaliação decorre da seguinte forma:

  • a parte direita da atribuição é avaliada: o resultado é um valor V.
  • o valor V é atribuído à variável
  • o valor V é também o valor da atribuição, agora considerada como uma expressão.

É assim que a operação

    V1=V2=expression

é válida. Devido à prioridade, é o operador = mais à direita que será avaliado. Temos, portanto,

    V1=(V2=expression)

A expressão V2=expressão é avaliada e tem como valor V. A avaliação desta expressão provocou a atribuição de V a V2. O operador = seguinte é então avaliado na forma:

    V1=V

O valor desta expressão continua a ser V. A sua avaliação provoca a atribuição de V a V1.

Assim, a operação V1=V2=expressão

é uma expressão cuja avaliação

  • provoca a atribuição do valor de expression às variáveis V1 e V2
  • retorna como resultado o valor expression.

É possível generalizar para uma expressão do tipo:

V1=V2=....=Vn=expression

3.3.5.2. Expressão aritmética

Os operadores das expressões aritméticas são os seguintes:

  • adição

  • subtração

* multiplicação

/ divisão: o resultado é o quociente exato se pelo menos um dos operandos for real. Se ambos os operandos forem inteiros, o resultado é o quociente inteiro. Assim, 5/2 → 2 e 5,0/2 → 2,5.

% divisão: o resultado é o resto, independentemente da natureza dos operandos, sendo o quociente um número inteiro. Trata-se, portanto, da operação módulo.

Existem várias funções matemáticas. Aqui estão algumas delas:

double Sqrt(double x)
raiz quadrada
double Cos(double x)
Cosseno
double Sin(double x)
Senus
double Tan(double x)
Tangente
double Pow(double x,double y)
x elevado a y (x>0)
double Exp(double x)
Exponencial
double Log(double x)
Logaritmo natural
double Abs(double x)
valor absoluto

etc...

Todas estas funções estão definidas numa classe C# chamada Math. Ao utilizá-las, é necessário antepor-lhes o nome da classe onde estão definidas. Assim, escrever-se-á:

double x, y=4;
x=Math.Sqrt(y);

A definição completa da classe Math é a seguinte:

Image

Image

Image

Image

Image

3.3.5.3. Prioridades na avaliação de expressões aritméticas

A prioridade dos operadores na avaliação de uma expressão aritmética é a seguinte (da mais elevada à mais baixa):

[fonctions], [ ( )],[ *, /, %], [+, -]

Os operadores de um mesmo bloco [ ] têm a mesma prioridade.

3.3.5.4. Expressões relacionais

Os operadores são os seguintes:

       <, <=, ==, !=, >, >=

prioridades dos operadores

>, >=, <, <=
==, !=

O resultado de uma expressão relacional é o valor booleano false se a expressão for falsa; caso contrário, é true.

      bool fin;
      int x=...;
      fin=x>4;

Comparação de dois caracteres

Sejam dois caracteres C1 e C2. É possível compará-los com os operadores

    <, <=, ==, !=, >, >=

São então os seus códigos Unicode, que são números, que são comparados. De acordo com a ordem Unicode, temos as seguintes relações:

espace < .. < '0' < '1' < .. < '9' < .. < 'A' < 'B' < .. < 'Z' < .. < 'a' < 'b' < .. <'z'

Comparação de duas cadeias de caracteres

São comparadas caractere a caractere. A primeira desigualdade encontrada entre dois caracteres implica uma desigualdade no mesmo sentido nas cadeias de caracteres.

Exemplos:

Suponhamos que se queiram comparar as cadeias «Gato» e «Cão»

Esta última desigualdade permite concluir que «Gato» < «Cão».

Suponhamos que se queiram comparar as cadeias «Gato» e «Gatinho». Há igualdade durante todo o tempo até que a cadeia «Gato» se esgote. Neste caso, a cadeia esgotada é declarada como a mais «pequena». Temos, portanto, a relação

    "Chat" < "Chaton".

Funções de comparação de duas cadeias de caracteres

É possível utilizar os operadores relacionais == e != para verificar se duas cadeias de caracteres são iguais ou não, ou ainda o método Equals da classe System.String. Para as relações < <= > >=, deve utilizar-se o método CompareTo da classe System.String:


using System;

namespace Chap1 {
    class P05 {
        static void Main(string[] args) {
            string chaine1="chat", chaine2="chien";
            int n = chaine1.CompareTo(chaine2);
            bool egal = chaine1.Equals(chaine2);
            Console.WriteLine("i={0}, egal={1}", n, egal);
            Console.WriteLine("chien==chaine1:{0},chien!=chaine2:{1}", "chien"==chaine1,"chien" != chaine2);
        }
    }
}

Na linha 7, a variável i terá o valor:

0 se as duas cadeias forem iguais

1 se a cadeia n.º 1 for maior que a cadeia n.º 2

-1 se a cadeia n.º 1 for menor que a cadeia n.º 2

Na linha 8, a variável egal terá o valor true se as duas cadeias forem iguais; caso contrário, terá o valor false. Na linha 10, utilizam-se os operadores == e != para verificar se duas cadeias são iguais ou não.

Resultados da execução:

i=-1, egal=False
chien==chaine1:False,chien!=chaine2:False

3.3.5.5. Expressões booleanas

Os operadores que podem ser utilizados são AND (&&) OR (||) NOT (!). O resultado de uma expressão booleana é um valor booleano.

Prioridades dos operadores:

  1. !
  2. &&
  3. ||

            double x = 3.5;
            bool valide = x > 2 && x < 4;

Os operadores relacionais têm prioridade sobre os operadores && e ||.

3.3.5.6. Operações bit a bit

Os operadores

Sejam i e j dois números inteiros.

i<<n
desloca i de n bits para a esquerda. Os bits de entrada são zeros.
i>>n
desloca i n bits para a direita. Se i for um inteiro com sinal (signed char, int, long), o bit de sinal é preservado.
i & j
realiza a operação lógica ET entre i e j, bit a bit.
i | j
realiza a operação lógica bit a bit entre i e j, como em OU.
~i
completa i com 1
i^j
realiza a operação OU EXCLUSIF entre i e j

Seja o seguinte código:


short i = 100, j = -13;
ushort k = 0xF123;
Console.WriteLine("i=0x{0:x4}, j=0x{1:x4}, k=0x{2:x4}", i,j,k);
Console.WriteLine("i<<4=0x{0:x4}, i>>4=0x{1:x4},k>>4=0x{2:x4},i&j=0x{3:x4},i|j=0x{4:x4},~i=0x{5:x4},j<<2=0x{6:x4},j>>2=0x{7:x4}", i << 4, i >> 4, k >> 4, (short)(i & j), (short)(i | j), (short)(~i), (short)(j << 2), (short)(j >> 2));
  • o formato {0:x4} apresenta o parâmetro n.º 0 no formato hexadecimal (x) com 4 caracteres (4).

Os resultados da execução são os seguintes:

i=0x0064, j=0xfff3, k=0xf123
i<<4=0x0640, i>>4=0x0006,k>>4=0x0f12,i&j=0x0060,i|j=0xfff7,~i=0xff9b,j<<2=0xffcc,j>>2=0xfffc

3.3.5.7. Combinação de operadores

a=a+b pode ser escrito como a+=b

a=a-b pode ser escrito como a-=b

O mesmo se aplica aos operadores /, %, *, <<, >>, &, |, ^. Assim, a=a/2; pode ser escrito como a/=2;

3.3.5.8. Operadores de incremento e decremento

A notação variable++ significa variable=variable+1 ou ainda variable+=1

A notação variable-- significa variable=variable-1 ou ainda variable-=1.

3.3.5.9. O operador ternário?

A expressão

    expr_cond ? expr1:expr2

é avaliada da seguinte forma:

1 a expressão expr_cond é avaliada. Trata-se de uma expressão condicional cujo valor é vrai ou faux

2 Se for verdadeira, o valor da expressão é o de expr1 e expr2 não é avaliada.

3 Se for falsa, ocorre o inverso: o valor da expressão é o de expr2 e expr1 não é avaliado.

A operação i=(j>4 ? j+1:j-1); atribuirá à variável i: j+1 se j>4, j-1 caso contrário. É o mesmo que escrever if(j>4) i=j+1; else i=j-1;, mas é mais conciso.

3.3.5.10. Prioridade geral dos operadores

() []  fonction                  
gd
! ~ ++ --                        
dg
new (type) opérateurs cast       
dg
*  /  %                          
gd
+  -                             
gd
<<  >>                           
gd
< <=  > >= instanceof            
gd
==    !=                         
gd
&                                
gd
^                                
gd
|                                
gd
&&                               
gd
||                               
gd
?   :                            
dg
= += -= etc. .                   
dg

gd indica que, em caso de igualdade de prioridade, é seguida a regra de prioridade da esquerda para a direita. Isto significa que, quando numa expressão existem operadores com a mesma prioridade, é o operador mais à esquerda na expressão que é avaliado em primeiro lugar. dg indica uma prioridade da direita para a esquerda.

3.3.5.11. As conversões de tipo

É possível, numa expressão, alterar momentaneamente a codificação de um valor. A isto chama-se alteração do tipo de um dado ou, em inglês, «type casting». A sintaxe para alterar o tipo de um valor numa expressão é a seguinte:

    (type) valeur

O valor assume então o tipo indicado. Isto implica uma alteração na codificação do valor.


using System;

namespace Chap1 {
    class P06 {
        static void Main(string[] args) {
            int i = 3, j = 4;
            float f1=i/j;
            float f2=(float)i/j;
            Console.WriteLine("f1={0}, f2={1}",f1,f2);
        }
    }
}
  • na linha 7, f1 terá o valor 0,0. A divisão 3/4 é uma divisão inteira, uma vez que ambos os operandos são do tipo int.
  • na linha 8, (float)i é o valor de i transformado em float. Agora, temos uma divisão entre um número real do tipo float e um número inteiro do tipo int. É então efetuada a divisão entre números reais. O valor de j será também convertido para o tipo float, após o que será efetuada a divisão entre os dois reais. f2 terá então o valor 0,75.

Eis os resultados da execução:

f1=0, f2=0,75

Na operação (float)i:

  • i é um valor codificado de forma exata em 2 bytes
  • (float); i é o mesmo valor codificado de forma aproximada como número real em 4 bytes

Verifica-se, portanto, uma transcodificação do valor de i. Esta transcodificação ocorre apenas durante o cálculo, mantendo a variável i sempre o seu tipo int.

3.4. As instruções de controlo do andamento do programa

3.4.1. Paragem

O método Exit, definido na classe Environment, permite interromper a execução de um programa.

syntaxe        void Exit(int status)
action        arrête le processus en cours et rend la valeur status au processus père

Exit provoca o fim do processo em curso e devolve o controlo ao processo chamador. O valor de status pode ser utilizado por este último. Em DOS, esta variável de estado é devolvida na variável de sistema ERRORLEVEL, cujo valor pode ser verificado num ficheiro batch. No Unix, com o interpretador de comandos Shell Bourne, é a variável $? que recupera o valor de status.

    Environment.Exit(0);

interromperá a execução do programa com um valor de estado igual a 0.

3.4.2. Estrutura de escolha simples

 syntaxe :  if (condition) {actions_condition_vraie;} else {actions_condition_fausse;}

Notas:

  • a condição está entre parênteses.
  • cada ação termina com um ponto-e-vírgula.
  • As chaves não terminam com ponto e vírgula.
  • As chaves só são necessárias se houver mais do que uma ação.
  • A cláusula else pode estar ausente.
  • Não existe a cláusula then.

O equivalente algorítmico desta estrutura é a estrutura «se... então... senão»:

Exemplo


    if (x>0)  { nx=nx+1;sx=sx+x;} else dx=dx-x;

É possível aninhar as estruturas de escolha:

if(condition1)
if (condition2)
        {......}
      else         //condição2
         {......}
else         //condição1
     {.......}

Por vezes, surge o seguinte problema:


using System;

namespace Chap1 {
    class P07 {
        static void Main(string[] args) {
            int n = 5;
            if (n > 1)
                if (n > 6)
                    Console.Out.WriteLine(">6");
                else Console.Out.WriteLine("<=6");
        }
    }
}

No exemplo anterior, a referência else da linha 10 a que if se refere? A regra é que um else se refere sempre ao if mais próximo: if(n>6), na linha 8, no exemplo. Consideremos outro exemplo:


            if (n2 > 1) {
                if (n2 > 6) Console.Out.WriteLine(">6");
       } else Console.Out.WriteLine("<=1");    

Aqui, queríamos associar um else ao if(n2>1) e não um else ao if(n2>6). Devido à observação anterior, somos obrigados a colocar chaves no if(n2>1) {...} else ...

3.4.3. Estrutura de casos

A sintaxe é a seguinte:

switch(expression) {
    case v1:     
            actions1;
            break;
    case v2:     
            actions2;
            break;
         . .. .. .. .. ..
    default:     
            actions_sinon;
            break;
}

notas

  • o valor da expressão de controlo do switch pode ser um número inteiro, um carácter ou uma cadeia de caracteres
  • A expressão de controlo está entre parênteses.
  • A cláusula default pode estar ausente.
  • Os valores vi são valores possíveis da expressão. Se a expressão tiver o valor vi, as ações associadas à cláusula case vi são executadas.
  • A instrução break faz com que se saia da estrutura «case».
  • Cada bloco de instruções associado a um valor vi deve terminar com uma instrução de ramificação (break, goto, return, ...); caso contrário, o compilador sinaliza um erro.

Exemplo

Em algoritmos

                selon la valeur de choix
                    cas 0
                         fin du module
                    cas 1
                         exécuter module M1
                    cas 2
                        exécuter module M2
                    sinon
                         erreur<--vrai
                findescas

Em C#

int choix = 2; 
bool erreur = false;
switch (choix) {
   case 0: return;
   case 1: M1(); break;
   case 2: M2(); break;
   default: erreur = true; break;
}
}// fim de Main

static void M1() {
    Console.WriteLine("M1");
}

static void M2() {
    Console.WriteLine("M2");
}
}

3.4.4. Estruturas de repetição

3.4.4.1. Número de repetições conhecido

Estrutura «for»

A sintaxe é a seguinte:


    for (i=id;i<=if;i=i+ip){ 
       actions; 
      } 

Notas

  • Os três argumentos do for estão entre parênteses e separados por ponto e vírgula.
  • Cada ação do for termina com um ponto e vírgula.
  • A chave só é necessária se houver mais do que uma ação.
  • A chave não é seguida de ponto-e-vírgula.

O equivalente algorítmico é a estrutura pour:

pour i variant de id à if avec un pas de ip
    actions
finpour

que pode ser traduzida por uma estrutura tantque:

    i  id
    tantque i<=if
        actions
        i i+ip
    fintantque

Estrutura «foreach»

A sintaxe é a seguinte:


foreach (Type variable in collection)
    instructions; 
}

Notas

  • collection é uma coleção de objetos enumerável. A coleção de objetos enumerável que já conhecemos é o tabelo
  • Type é o tipo dos objetos da coleção. No caso de um array, seria o tipo dos elementos do array
  • variable é uma variável local do ciclo que irá assumir sucessivamente, como valor, todos os valores da coleção.

Assim, o código seguinte:


            string[] amis = { "paul", "hélène", "jacques", "sylvie" };
            foreach (string nom in amis) {
                Console.WriteLine(nom);
         }

exibiria:

paul
hélène
jacques
sylvie

3.4.4.2. Número de repetições desconhecido

Existem várias estruturas em C# para este caso.

Estrutura «while»


    while(condition){
          actions;
        } 

O ciclo repete-se enquanto a condição for verdadeira. O ciclo pode nunca vir a ser executado.

Notas:

  • a condição está entre parênteses.
  • cada ação termina com um ponto-e-vírgula.
  • A chave só é necessária se houver mais do que uma ação.
  • A chave não é seguida de ponto-e-vírgula.

A estrutura algorítmica correspondente é a estrutura «enquanto»:

tantque condition
    actions
fintantque

Estrutura «repetir até» (do while)

A sintaxe é a seguinte:


    do{
       instructions;
    }while(condition); 

O ciclo repete-se até que a condição se torne falsa. Neste caso, o ciclo é executado pelo menos uma vez.

Notas

  • A condição está entre parênteses.
  • Cada ação termina com um ponto-e-vírgula.
  • A chave só é necessária se houver mais do que uma ação.
  • A chave não é seguida de ponto-e-vírgula.

A estrutura algorítmica correspondente é a estrutura «repetir … até»:

répéter
    actions
jusqu'à condition

Estrutura «for» (for)

A sintaxe é a seguinte:


for(instructions_départ;condition;instructions_fin_boucle){
    instructions; 
}

O ciclo repete-se enquanto a condição for verdadeira (avaliada antes de cada iteração do ciclo). As instruções Instructions_départ são executadas antes de entrar no ciclo pela primeira vez. As instruções Instructions_fin_boucle são executadas após cada iteração do ciclo.

notas

  • as diferentes instruções em instructions_depart e instructions_fin_boucle são separadas por vírgulas.

A estrutura algorítmica correspondente é a seguinte:

instructions_départ
tantque condition
    actions
    instructions_fin_boucle
fintantque

Exemplos

Os seguintes fragmentos de código calculam, todos eles, a soma dos primeiros 10 números inteiros.

            int i, somme, n=10;
            for (i = 1, somme = 0; i <= n; i = i + 1)
                somme = somme + i;

            for (i = 1, somme = 0; i <= n; somme = somme + i, i = i + 1) ;

            i = 1; somme = 0;
            while (i <= n) { somme += i; i++; }

            i = 1; somme = 0;
            do somme += i++;
            while (i <= n);

3.4.4.3. Instruções de gestão de ciclo

break
sai do ciclo «for», «while», «do...while».
continue
avança para a iteração seguinte nos ciclos «for», «while» e «do...while»

3.5. Gestão de exceções

Muitas funções em C# podem gerar exceções, ou seja, erros. Quando uma função pode gerar uma exceção, o programador deve gerir essa exceção com o objetivo de obter programas mais resistentes a erros: deve-se evitar sempre que uma aplicação «trave» de forma inesperada.

A gestão de uma exceção é feita de acordo com o seguinte esquema:

try{
    code susceptible de générer une exception
} catch (Exception e){
    traiter l'exception e
}
instruction suivante

Se a função não gerar uma exceção, passa-se então para a instrução seguinte; caso contrário, passa-se para o corpo da cláusula catch e, em seguida, para a instrução seguinte. É importante notar os seguintes pontos:

  • e é um objeto do tipo Exception ou derivado. É possível ser mais preciso utilizando tipos como IndexOutOfRangeException, FormatException, SystemException, etc.: existem vários tipos de exceções. Ao escrever catch (Exception e), indicamos que pretendemos tratar todos os tipos de exceções. Se o código da cláusula try for suscetível de gerar vários tipos de exceções, podemos querer ser mais precisos, tratando a exceção com várias cláusulas catch:
try{
    code susceptible de générer les exceptions
} catch ( IndexOutOfRangeException e1){
    traiter l'exception e1
}
} catch ( FormatException e2){
    traiter l'exception e2
}
instruction suivante
  • É possível adicionar uma cláusula «finally» às cláusulas try/catch:
try{
    code susceptible de générer une exception
} catch (Exception e){
    traiter l'exception e
}
finally{
    code exécuté après try ou catch
}
instruction suivante

Quer haja ou não uma exceção, o código da cláusula finally será sempre executado.

  • Na cláusula catch, pode não se querer utilizar o objeto Exception disponível. Em vez de escrever catch (Exception e){..}, escreve-se então catch(Exception){...} ou, mais simplesmente, catch {...}.
  • A classe Exception possui uma propriedade Message que contém uma mensagem detalhando o erro que ocorreu. Assim, se se pretender exibir essa mensagem, escrever-se-á:
catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}",ex.Message);
    ...
}//catch
  • A classe Exception possui um método ToString que devolve uma cadeia de caracteres indicando o tipo da exceção, bem como o valor da propriedade Message. Assim, poderemos escrever:
catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.ToString());
    ...
}//catch

Também se pode escrever:

catch (Exception ex){
    Console.WriteLine("L'erreur suivante s'est produite : {0}",ex);
    ...
}//catch

O compilador atribuirá ao parâmetro {0} o valor ex.ToString().

O exemplo seguinte mostra uma exceção gerada pela utilização de um elemento de tabela inexistente:


using System;

namespace Chap1 {
    class P08 {
        static void Main(string[] args) {
            // declaração e inicialização de um tabuleiro
            int[] tab = { 0, 1, 2, 3 };
            int i;
            // exibição da matriz com um «for»
            for (i = 0; i < tab.Length; i++)
                Console.WriteLine("tab[{0}]={1}", i, tab[i]);
            // exibição de um tabelo com um for each
            foreach (int élmt in tab) {
                Console.WriteLine(élmt);
            }
            // geração de uma exceção
            try {
                tab[100] = 6;
            } catch (Exception e) {
                Console.Error.WriteLine("L'erreur suivante s'est produite : " + e);
                return;
            }//try-catch
            finally {
                Console.WriteLine("finally ...");
            }
        }
    }
}

Na linha 18 acima, será gerada uma exceção porque a matriz tab não possui o elemento n.º 100. A execução do programa produz os seguintes resultados:

tab[0]=0
tab[1]=1
tab[2]=2
tab[3]=3
0
1
2
3
L'erreur suivante s'est produite : System.IndexOutOfRangeException: L'index se trouve en dehors des limites du tableau.
   à Chap1.P08.Main(String[] args) dans C:\data\travail\2007-2008\c# 2008\poly\Chap1\08\Program.cs:linha 7
finally ...
  • linha 9: ocorreu a exceção [System.IndexOutOfRangeException]
  • linha 11: a cláusula finally (linhas 23-25) do código foi executada, apesar de, na linha 21, existir uma instrução return para sair do método. É importante notar que a cláusula finally é sempre executada.

Eis outro exemplo em que se trata a exceção provocada pela atribuição de uma cadeia de caracteres a uma variável do tipo inteiro, quando a cadeia não representa um número inteiro:


using System;

namespace Chap1 {
    class P08 {
        static void Main(string[] args) {

            // exemplo 2
            // Pede-se o nome
            Console.Write("Nom : ");
            // leitura da resposta
            string nom = Console.ReadLine();
            // pergunta-se a idade
            int age = 0;
            bool ageOK = false;
            while (!ageOK) {
                // pergunta
                Console.Write("âge : ");
                // leitura e verificação da resposta
                try {
                    age = int.Parse(Console.ReadLine());
                    ageOK = age>=1;
                } catch {
                }//try-catch
                if (!ageOK) {
                    Console.WriteLine("Age incorrect, recommencez...");
                }
            }//while
            // exibição final
            Console.WriteLine("Vous vous appelez {0} et vous avez {1} an(s)",nom,age);
        }
    }
}
  • linhas 15-27: o ciclo de introdução da idade de uma pessoa
  • linha 20: o texto digitado no teclado é convertido num número inteiro pelo método int.Parse. Este método lança uma exceção se a conversão não for possível. Por isso, a operação foi colocada num bloco try/catch.
  • linhas 22-23: se for lançada uma exceção, passa-se para o `catch`, onde nada é feito. Assim, a variável booleana ageOK, definida em false na linha 14, permanecerá em false.
  • linha 21: se chegarmos a esta linha, significa que a conversão de string para int foi bem-sucedida. Verifica-se, no entanto, se o inteiro obtido é superior ou igual a 1.
  • linhas 24-26: é emitida uma mensagem de erro se a idade estiver incorreta.

Alguns resultados da execução:

1
2
3
Nom : dupont
âge : 23
Vous vous appelez dupont et vous avez 23 an(s)
1
2
3
4
5
6
7
Nom : durand
âge : x
Age incorrect, recommencez...
âge : -4
Age incorrect, recommencez...
âge : 12
Vous vous appelez durand et vous avez 12 an(s)

3.6. Aplicação de exemplo - V1

Propõe-se escrever um programa que permita calcular o imposto de um contribuinte. Consideramos o caso simplificado de um contribuinte que tenha apenas o seu salário para declarar (dados de 2004 relativos aos rendimentos de 2003):

  • calcula-se o número de quotas do trabalhador nbParts = nbEnfants/2 + 1 se não for casado, nbEnfants/2 + 2 se for casado, em que nbEnfants é o número de filhos que tem.
  • se tiver pelo menos três filhos, tem mais meia quota
  • calcula-se o seu rendimento tributável R = 0,72 * S, em que S é o seu salário anual
  • calcula-se o seu coeficiente familiar QF = R/nbParts
  • calcula-se o seu imposto I. Consideremos a seguinte tabela:
4262
0
0
8382
0,0683
291,09
14753
0,1914
1322,92
23 888
0,2826
2668,39
38 868
0,3738
4846,98
47 932
0,4262
6883,66
0
0,4809
9505,54

Cada linha tem 3 campos. Para calcular o imposto I, procura-se a primeira linha em que QF <= campo1. Por exemplo, se QF = 5000, encontrar-se-á a linha

    8382        0.0683        291.09

O imposto I é, então, igual a 0,0683*R - 291,09*nbParts. Se QF for tal que a relação QF <= campo1 nunca for verificada, então são utilizados os coeficientes da última linha. Neste caso:

0 0,4809 9505,54

o que dá o imposto I = 0,4809*R - 9505,54*nbParts.

O programa C# correspondente é o seguinte:


using System;

namespace Chap1 {
    class Impots {
        static void Main(string[] args) {
            // tabelas de dados necessárias para o cálculo do imposto
            decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
            decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
            decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };

            // recuperar o estado civil
            bool OK = false;
            string reponse = null;
            while (!OK) {
                Console.Write("Etes-vous marié(e) (O/N) ? ");
                reponse = Console.ReadLine().Trim().ToLower();
                if (reponse != "o" && reponse != "n")
                    Console.Error.WriteLine("Réponse incorrecte. Recommencez");
                else OK = true;
            }//while
            bool marie = reponse == "o";

            // número de filhos
            OK = false;
            int nbEnfants = 0;
            while (!OK) {
                Console.Write("Nombre d'enfants : ");
                try {
                    nbEnfants = int.Parse(Console.ReadLine());
                    OK = nbEnfants >= 0;
                } catch {
                }// try
                if (!OK) {
                    Console.WriteLine("Réponse incorrecte. Recommencez");
                }
            }// enquanto

            // salário
            OK = false;
            int salaire = 0;
            while (!OK) {
                Console.Write("Salaire annuel : ");
                try {
                    salaire = int.Parse(Console.ReadLine());
                    OK = salaire >= 0;
                } catch {
                }// try
                if (!OK) {
                    Console.WriteLine("Réponse incorrecte. Recommencez");
                }
            }// while

            // cálculo do número de quotas
            decimal nbParts;
            if (marie) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;

            // rendimento tributável
            decimal revenu = 0.72M * salaire;

            // quociente familiar
            decimal QF = revenu / nbParts;

            // procura da faixa de imposto correspondente a QF
            int i;
            int nbTranches = limites.Length;
            limites[nbTranches - 1] = QF;
            i = 0;
            while (QF > limites[i]) i++;
            // o imposto
            int impots = (int)(coeffR[i] * revenu - coeffN[i] * nbParts);

            // exibe-se o resultado
            Console.WriteLine("Impôt à payer : {0} euros", impots);
        }
    }
}
  • linhas 7-9: os valores numéricos são seguidos do sufixo M (Money) para que sejam do tipo decimal.
  • linha 16:
  • Console.ReadLine() transforma a cadeia C1 digitada no teclado
  • C1.Trim() remove os espaços no início e no fim de C1 — devolve a cadeia C2
  • C2.ToLower() devolve a cadeia C3, que é a cadeia C2 convertida para minúsculas.
  • linha 21: o valor booleano marie recebe o valor true ou false da relação reponse=="o"
  • linha 29: a cadeia de caracteres introduzida pelo teclado é convertida para o tipo int. Se a conversão falhar, é lançada uma exceção.
  • linha 30: o valor booleano OK recebe o valor true ou false da relação nbEnfants>=0
  • linhas 55-56: não se pode escrever simplesmente nbEnfants/2. Se nbEnfants fosse igual a 3, teríamos 3/2, uma divisão inteira que daria 1 e não 1,5. Por isso, escreve-se (decimal)nbEnfants para tornar real um dos operandos da divisão e obter, assim, uma divisão entre números reais.

Eis alguns exemplos de execução:

Etes-vous marié(e) (O/N) ? o
Nombre d'enfants : 2
Salaire annuel : 60000
Impôt à payer : 4282 euros
Etes-vous marié(e) (O/N) ? oui
Réponse incorrecte. Recommencez
Etes-vous marié(e) (O/N) ? o
Nombre d'enfants : trois
Réponse incorrecte. Recommencez
Nombre d'enfants : 3
Salaire annuel : 60000 euros
Réponse incorrecte. Recommencez
Salaire annuel : 60000
Impôt à payer : 2959 euros

3.7. Argumentos do programa principal

A função principal Main pode aceitar como parâmetro uma matriz de cadeias de caracteres: String[] (ou string[]). Esta matriz contém os argumentos da linha de comandos utilizada para iniciar a aplicação. Assim, se iniciarmos o programa P com o seguinte comando (DOS):


        P arg0 arg1 … argn

e se a função Main estiver declarada da seguinte forma:

public static void Main(string[] args)

teremos args[0]="arg0", args[1]="arg1" … Eis um exemplo:


using System;

namespace Chap1 {
    class P10 {
        static void Main(string[] args) {
            // listam-se os parâmetros recebidos
            Console.WriteLine("Il y a  " + args.Length + " arguments");
            for (int i = 0; i < args.Length; i++) {
                Console.Out.WriteLine("arguments[" + i + "]=" + args[i]);
            }
        }
    }
}

Para passar argumentos ao código executado, deve-se proceder da seguinte forma:

  • em [1]: clique com o botão direito do rato no projeto / Propriedades
  • em [2]: separador [Debug]
  • em [3]: introduzir os argumentos

A execução produz os seguintes resultados:

1
2
3
4
5
Il y a  4 arguments
arguments[0]=a0
arguments[1]=a1
arguments[2]=a2
arguments[3]=a3

Note-se que a assinatura

public static void Main()

é válida se a função Main não esperar parâmetros.

3.8. As enumerações

Uma enumeração é um tipo de dados cujo domínio de valores é um conjunto de constantes inteiras. Consideremos um programa que tem de gerir as notas de um exame. Seria necessário ter cinco: Passable, AssezBien, Bien, TrèsBien, Excellent.

Poderíamos, então, definir uma enumeração para estas cinco constantes:


        enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };

Internamente, estas cinco constantes são codificadas por números inteiros consecutivos, começando por 0 para a primeira constante, 1 para a seguinte, etc... Uma variável pode ser declarada para assumir estes valores na enumeração:


            // uma variável cujos valores provêm da enumeração «Mentions»
Mentions maMention = Mentions.Passable;

É possível comparar uma variável com os diferentes valores possíveis da enumeração:


            if (maMention == Mentions.Passable) {
                Console.WriteLine("Peut mieux faire");
}

É possível obter todos os valores da enumeração:


            // lista de menções sob a forma de cadeias de caracteres
            foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
                Console.WriteLine(m);
}

Da mesma forma que o tipo simples int é equivalente à estrutura System.Int32, o tipo simples enum é equivalente à estrutura System.Enum. Esta estrutura possui um método estático GetValues que permite obter todos os valores de um tipo enumerado que seja passado como parâmetro. Este deve ser um objeto do tipo Type, que é uma classe de informações sobre o tipo de um dado. O tipo de uma variável v é obtido através de v.GetType(). O tipo de um tipo T é obtido através de typeof(T). Assim, neste caso, maMention.GetType() devolve o objeto Type da enumeração Mentions e Enum.GetValues(maMention.GetType()) a lista de valores da enumeração Mentions.

Se escrevermos agora


            //lista das menções sob a forma de números inteiros
            foreach (int m in Enum.GetValues(typeof(Mentions))) {
                Console.WriteLine(m);
}

na linha 2, a variável do ciclo é do tipo inteiro. Obtém-se, então, a lista de valores da enumeração sob a forma de inteiros. O objeto do tipo System.Type, correspondente ao tipo de dados Mentions, é obtido por typeof(Mentions). Poderíamos ter escrito, tal como anteriormente, maMention.GetType().

O programa seguinte ilustra o que acabou de ser explicado:


using System;

namespace Chap1 {
    class P11 {
        enum Mentions { Passable, AssezBien, Bien, TrèsBien, Excellent };
        static void Main(string[] args) {
            // uma variável cujos valores provêm da enumeração «Mentions»
            Mentions maMention = Mentions.Passable;
            // exibição do valor da variável
            Console.WriteLine("mention=" + maMention);
            // teste com um valor da enumeração
            if (maMention == Mentions.Passable) {
                Console.WriteLine("Peut mieux faire");
            }
            // lista de menções sob a forma de cadeias de caracteres
            foreach (Mentions m in Enum.GetValues(maMention.GetType())) {
                Console.WriteLine(m);
            }
            //lista de menções sob a forma de números inteiros
            foreach (int m in Enum.GetValues(typeof(Mentions))) {
                Console.WriteLine(m);
            }
        }
    }
}

Os resultados da execução são os seguintes:

mention=Passable
Peut mieux faire
Passable
AssezBien
Bien
TrèsBien
Excellent
0
1
2
3
4

3.9. Passagem de parâmetros para uma função

Vamos analisar aqui a forma como os parâmetros são passados a uma função. Consideremos a seguinte função estática:


        private static void ChangeInt(int a) {
            a = 30;
            Console.WriteLine("Paramètre formel a=" + a);
}

Na definição da função, linha 1, a é designado como um parâmetro formal. Existe apenas para efeitos da definição da função changeInt. Poderia muito bem ter-se chamado b. Consideremos agora uma utilização desta função:


        public static void Main() {
            int age = 20;
            ChangeInt(age);
            Console.WriteLine("Paramètre effectif age=" + age);
}

Aqui, na instrução da linha 3, ChangeInt(idade), age é o parâmetro efetivo que irá transmitir o seu valor ao parâmetro formal a. O que nos interessa é a forma como um parâmetro formal recupera o valor de um parâmetro efetivo.

3.9.1. Passagem por valor

O exemplo seguinte mostra-nos que os parâmetros de uma função são, por predefinição, passados por valor, ou seja, o valor do parâmetro efetivo é copiado para o parâmetro formal correspondente. Temos duas entidades distintas. Se a função alterar o parâmetro formal, o parâmetro efetivo não sofre qualquer alteração.


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            int age = 20;
            ChangeInt(age);
            Console.WriteLine("Paramètre effectif age=" + age);
        }
        private static void ChangeInt(int a) {
            a = 30;
            Console.WriteLine("Paramètre formel a=" + a);
        }
    }
}

Os resultados obtidos são os seguintes:

Paramètre formel a=30
Paramètre effectif age=20

O valor 20 do parâmetro efetivo age foi copiado para o parâmetro formal a (linha 10). Este foi posteriormente alterado (linha 11). O parâmetro efetivo, por sua vez, permaneceu inalterado. Este modo de passagem é adequado para os parâmetros de entrada de uma função.

3.9.2. Passagem por referência

Numa passagem por referência, o parâmetro efetivo e o parâmetro formal são uma única e mesma entidade. Se a função alterar o parâmetro formal, o parâmetro efetivo também é alterado. Em C#, ambos devem ser precedidos pela palavra-chave ref:

Eis um exemplo:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // exemplo 2
            int age2 = 20;
            ChangeInt2(ref age2);
            Console.WriteLine("Paramètre effectif age2=" + age2);
        }
        private static void ChangeInt2(ref int a2) {
            a2 = 30;
            Console.WriteLine("Paramètre formel a2=" + a2);
        }
    }
}

e os resultados da execução:

Paramètre formel a2=30
Paramètre effectif age2=30

O parâmetro efetivo acompanhou a alteração do parâmetro formal. Este modo de passagem é adequado para os parâmetros de saída de uma função.

3.9.3. Passagem por referência com a palavra-chave «out»

Consideremos o exemplo anterior, no qual a variável age2 não seria inicializada antes da chamada à função changeInt:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // exemplo 2
            int age2;
            ChangeInt2(ref age2);
            Console.WriteLine("Paramètre effectif age2=" + age2);
        }
        private static void ChangeInt2(ref int a2) {
            a2 = 30;
            Console.WriteLine("Paramètre formel a2=" + a2);
        }
    }
}

Ao compilar este programa, surge um erro:

    Use of unassigned local variable 'age2'

É possível contornar este obstáculo atribuindo um valor inicial a age2. Também é possível substituir a palavra-chave ref pela palavra-chave out. Desta forma, indica-se que o parâmetro é apenas um parâmetro de saída e, por isso, não necessita de um valor inicial:


using System;

namespace Chap1 {
    class P12 {
        public static void Main() {
            // exemplo 3
            int age3;
            ChangeInt3(out age3);
            Console.WriteLine("Paramètre effectif age3=" + age3);
        }
        private static void ChangeInt3(out int a3) {
            a3 = 30;
            Console.WriteLine("Paramètre formel a3=" + a3);
        }
    }
}

Os resultados da execução são os seguintes:

Paramètre formel a3=30
Paramètre effectif age3=30