Skip to content

5. Classes .NET de uso comum

Apresentamos aqui algumas classes da plataforma .NET frequentemente utilizadas. Antes disso, mostramos como obter informações sobre as centenas de classes disponíveis. Esta ajuda é indispensável para o programador C#, mesmo que experiente. O nível de qualidade de uma ajuda (acesso fácil, organização compreensível, pertinência da informação, etc.) pode determinar o sucesso ou o fracasso de um ambiente de desenvolvimento.

5.1. Procurar ajuda sobre as classes .NET

Apresentamos aqui algumas orientações para encontrar ajuda com o Visual Studio.NET

5.1.1. Ajuda/Índice

  • no [1], selecione a opção Help/Contents do menu.
  • em [2], selecione a opção Visual C# Express Edition
  • em [3], a árvore de ajuda sobre C#
  • em [4], outra opção útil é .NET Framework, que dá acesso a todas as classes do framework .NET.

Vamos dar uma vista de olhos aos títulos dos capítulos da ajuda do C#:

  • [1]: uma visão geral do C#
  • [2]: uma série de exemplos sobre alguns aspetos do C#
  • [3]: um curso de C# — poderia substituir vantajosamente o presente documento…
  • [4]: para aprofundar os detalhes do C#
  • [5]: útil para programadores de C++ ou Java. Permite evitar algumas armadilhas.
  • [6]: quando estiver à procura de exemplos, pode começar por aqui.
  • [7]: o que é preciso saber para criar interfaces gráficas
  • [8]: para utilizar melhor o IDE Visual Studio Express
  • [9]: O SQL Server Express 2005 é um SGBD de qualidade distribuído gratuitamente. Iremos utilizá-lo neste curso.

A ajuda do C# é apenas uma parte do que o programador precisa. A outra parte é a ajuda sobre as centenas de classes do framework .NET que irão facilitar o seu trabalho.

  • [1]: selecionamos a ajuda sobre o framework .NET
  • [2]: a ajuda encontra-se no ramo .NET Framework SDK
  • [3]: o ramo .NET «Framework Class Library» apresenta todas as classes .NET de acordo com o espaço de nomes a que pertencem
  • [4]: o espaço de nomes System que foi mais frequentemente utilizado nos exemplos dos capítulos anteriores
  • [5]: no espaço de nomes System, um exemplo, neste caso a estrutura DateTime
  • [6]: a ajuda sobre a estrutura DateTime

5.1.2. Ajuda/Índice/Pesquisar

A ajuda fornecida pelo MSDN é imensa e pode não se saber onde procurar. Nesse caso, pode-se utilizar o índice da ajuda:

  • no [1], utilize a opção [Help/Index] se a janela de ajuda ainda não estiver aberta; caso contrário, utilize [2] numa janela de ajuda já aberta.
  • em [3], especifique o domínio no qual a pesquisa deve ser efetuada
  • em [4], especifique o que procura, neste caso uma turma
  • em [5], a resposta

Outra forma de procurar ajuda é utilizar a função de pesquisa da ajuda:

  • em [1], utilize a opção [Help/Search] se a janela de ajuda ainda não estiver aberta; caso contrário, utilize [2] numa janela de ajuda já aberta.
  • em [3], especificar o que se pretende pesquisar
  • em [4], filtrar os domínios de pesquisa
  • em [5], a resposta apresentada sob a forma de diferentes temas onde o texto procurado foi encontrado.

5.2. As cadeias de caracteres

5.2.1. A classe System.String

A classe System.String é idêntica ao tipo string simples. Possui inúmeras propriedades e métodos. Aqui estão alguns deles:


public int Length { get; }
nombre de caractères de la chaîne

public bool EndsWith(string value)

rend vrai si la chaîne se termine par value

public bool StartsWith(string value)

rend vrai si la chaîne commence par value
public virtual bool Equals(object obj)

rend vrai si la chaînes est égale à obj - équivalent chaîne==obj

public int IndexOf(string value, int startIndex)

rend la première position dans la chaîne de la
chaîne value - la recherche commence à partir du
caractère n° startIndex

public int IndexOf(char value, int startIndex)

idem mais pour le caractère value

public string Insert(int startIndex, string value)

insère la chaîne value dans chaîne en position
startIndex
public static string Join(string separator, string[] value)

méthode de classe - rend une chaîne de caractères,
résultat de la concaténation des valeurs du tableau
value avec le séparateur separator

public int LastIndexOf(char value, int startIndex, int count)

public int LastIndexOf(string value, int startIndex, int count)

idem indexOf mais rend la dernière position au lieu
de la première

public string Replace(char oldChar, char newChar)

rend une chaîne copie de la chaîne courante où le
caractère oldChar a été remplacé par le caractère
newChar

public string[] Split(char[] separator)

la chaîne est vue comme une suite de champs séparés
par les caractères présents dans le tableau
separator. Le résultat est le tableau de ces champs

public string Substring(int startIndex, int length)

sous-chaîne de la chaîne courante commençant à la
position startIndex et ayant length caractères
public string ToLower()
rend la chaîne courante en minuscules
public string ToUpper()
rend la chaîne courante en majuscules
public string Trim()
rend la chaîne courante débarrassée de ses espaces
de début et fin

É importante notar o seguinte: quando um método devolve uma cadeia de caracteres, esta é uma cadeia diferente daquela à qual o método foi aplicado. Assim, S1.Trim() devolve a cadeia S2, e S1 e S2 são duas cadeias diferentes.

Uma cadeia C pode ser considerada como uma matriz de caracteres. Assim,

  • C[i] é o caractere i de C
  • C.Length é o número de caracteres de C

Consideremos o seguinte exemplo:


using System;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            string uneChaine = "l'oiseau vole au-dessus des nuages";
            affiche("uneChaine=" + uneChaine);
            affiche("uneChaine.Length=" + uneChaine.Length);
            affiche("chaine[10]=" + uneChaine[10]);
            affiche("uneChaine.IndexOf(\"vole\")=" + uneChaine.IndexOf("vole"));
            affiche("uneChaine.IndexOf(\"x\")=" + uneChaine.IndexOf("x"));
            affiche("uneChaine.LastIndexOf('a')=" + uneChaine.LastIndexOf('a'));
            affiche("uneChaine.LastIndexOf('x')=" + uneChaine.LastIndexOf('x'));
            affiche("uneChaine.Substring(4,7)=" + uneChaine.Substring(4, 7));
            affiche("uneChaine.ToUpper()=" + uneChaine.ToUpper());
            affiche("uneChaine.ToLower()=" + uneChaine.ToLower());
            affiche("uneChaine.Replace('a','A')=" + uneChaine.Replace('a', 'A'));
            string[] champs = uneChaine.Split(null);
            for (int i = 0; i < champs.Length; i++) {
                affiche("champs[" + i + "]=[" + champs[i] + "]");
            }//for
            affiche("Join(\":\",champs)=" + System.String.Join(":", champs));
            affiche("(\"  abc  \").Trim()=[" + "  abc  ".Trim() + "]");
        }//Principal

        public static void affiche(string msg) {
            // exibe mensagem
            Console.WriteLine(msg);
        }//exibe
    }//classe
}//namespace

A execução produz os seguintes resultados:

uneChaine=l'oiseau vole au-dessus des nuages
uneChaine.Length=34
chaine[10]=o
uneChaine.IndexOf("vole")=9
uneChaine.IndexOf("x")=-1
uneChaine.LastIndexOf('a')=30
uneChaine.LastIndexOf('x')=-1
uneChaine.Substring(4,7)=seau vo
uneChaine.ToUpper()=L'OISEAU VOLE AU-DESSUS DES NUAGES
uneChaine.ToLower()=l'oiseau vole au-dessus des nuages
uneChaine.Replace('a','A')=l'oiseAu vole Au-dessus des nuAges
champs[0]=[l'oiseau]
champs[1]=[vole]
champs[2]=[au-dessus]
champs[3]=[des]
champs[4]=[nuages]
Join(":",champs)=l'oiseau:vole:au-dessus:des:nuages
("  abc  ").Trim()=[abc]

Consideremos um novo exemplo:


using System;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // a linha a analisar
            string ligne = "un:deux::trois:";
            // os separadores de campos
            char[] séparateurs = new char[] { ':' };
            // divisão
            string[] champs = ligne.Split(séparateurs);
            for (int i = 0; i < champs.Length; i++) {
                Console.WriteLine("Champs[" + i + "]=" + champs[i]);
            }
            // junção
            Console.WriteLine("join=[" + System.String.Join(":", champs) + "]");
        }
    }
}

e os resultados da execução:

1
2
3
4
5
6
Champs[0]=un
Champs[1]=deux
Champs[2]=
Champs[3]=trois
Champs[4]=
join=[un:deux::trois:]

O método Split da classe String permite colocar os elementos de uma cadeia de caracteres numa matriz. A definição do método Split aqui utilizado é a seguinte:


    public string[] Split(char[] separator);
separator
tabela de caracteres. Estes caracteres representam os caracteres utilizados para separar os campos da cadeia de caracteres. Assim, se a cadeia for «champ1, champ2, champ3"», poderá utilizar-se separator=new char[] {','}. Se o separador for uma sequência de espaços, utilizar-se-á separator=null.
résultat
matriz de cadeias de caracteres em que cada elemento da matriz corresponde a um campo da cadeia.

O método Join é um método estático da classe String:


    public static string Join(string separator, string[] value);
value
matriz de cadeias de caracteres
separator
uma cadeia de caracteres que servirá de separador de campos
résultat
uma cadeia de caracteres formada pela concatenação dos elementos da tabela value, separados pela cadeia separator.

5.2.2. A classe System.Text.StringBuilder

Anteriormente, referimos que os métodos da classe String aplicados a uma cadeia de caracteres S1 devolviam outra cadeia S2. A classe System.Text.StringBuilder permite manipular S1 sem ter de criar uma cadeia de caracteres S2. Isto melhora o desempenho, evitando a multiplicação de cadeias de caracteres com uma vida útil muito limitada.

A classe admite vários construtores:

StringBuilder()
constructeur par défaut
StringBuilder(String value)

construction et initialisation avec value
StringBuilder(String value, int capacité)

construction et initialisation avec value avec une taille de
bloc de capacité caractères.

Um objeto StringBuilder trabalha com blocos de capacité caracteres para armazenar a cadeia subjacente. Por predefinição, capacité tem o valor 16. O terceiro construtor acima permite especificar a capacidade dos blocos. O número de blocos de caracteres capacité necessários para armazenar uma cadeia S é ajustado automaticamente pela classe StringBuilder. Existem construtores para definir o número máximo de caracteres num objeto StringBuilder. Por predefinição, esta capacidade máxima é 2 147 483 647.

Eis um exemplo que ilustra este conceito de capacité:


using System.Text;
using System;
namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // str
            StringBuilder str = new StringBuilder("test");
            Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
            for (int i = 0; i < 10; i++) {
                str.Append("test");
                Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
            }
            // str2
            StringBuilder str2 = new StringBuilder("test",10);
            Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
            for (int i = 0; i < 10; i++) {
                str2.Append("test");
                Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
            }
        }
    }
}
  • linha 7: criação de um objeto StringBuilder com um tamanho de bloco de 16 caracteres
  • linha 8: str.Length é o número atual de caracteres da cadeia str. str.Capacity é o número de caracteres que a cadeia str atual pode armazenar antes da reatribuição de um novo bloco.
  • linha 10: str.Append(String S) permite concatenar a cadeia S, do tipo String, à cadeia str, do tipo StringBuilder.
  • linha 14: criação de um objeto StringBuilder com uma capacidade de bloco de 10 caracteres

Resultado da execução:

taille=4, capacité=16
taille=8, capacité=16
taille=12, capacité=16
taille=16, capacité=16
taille=20, capacité=32
taille=24, capacité=32
taille=28, capacité=32
taille=32, capacité=32
taille=36, capacité=64
taille=40, capacité=64
taille=44, capacité=64
taille=4, capacité=10
taille=8, capacité=10
taille=12, capacité=20
taille=16, capacité=20
taille=20, capacité=20
taille=24, capacité=40
taille=28, capacité=40
taille=32, capacité=40
taille=36, capacité=40
taille=40, capacité=40
taille=44, capacité=80

Estes resultados mostram que a classe segue um algoritmo próprio para alocar novos blocos quando a sua capacidade é insuficiente:

  • linhas 4-5: aumento da capacidade em 16 caracteres
  • linhas 8-9: aumento da capacidade em 32 caracteres, quando 16 teriam sido suficientes.

Eis alguns dos métodos da classe:


public StringBuilder Append(string value)

ajoute la chaîne value à l'objet StringBuilder. Rend
l'objet StringBuilder. Cette méthode est surchargée
 pour admettre différents types pour value : byte,
int, float, double, decimal, ... 

public StringBuilder Insert(int index,
string value)

insère value à la position index. Cette méthode est
surchargée comme la précédente pour accepter
différents types pour value.

public StringBuilder Remove(int index, int length)

supprime length caractères à partir de la position
index.

public StringBuilder Replace(string oldValue,
string newValue)

remplace dans StringBuilder, la chaîne oldValue par
la chaîne newValue. Il existe une version surchargée
(char oldChar, char newChar).
public String ToString()

convertit l'objet StringBuilder en un objet de type
String.

Eis um exemplo:


using System.Text;
using System;
namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // str3
            StringBuilder str3 = new StringBuilder("test");
            Console.WriteLine(str3.Append("abCD").Insert(2, "xyZT").Remove(0, 2).Replace("xy", "XY"));
        }
    }
}

e os seus resultados:

XYZTstabCD

5.3. As tabelas

As tabelas derivam da classe Array:

A classe Array possui vários métodos para ordenar um array, procurar um elemento num array, redimensionar um array, etc. Apresentamos algumas propriedades e métodos desta classe. Quase todos estão sobrecarregados, c.a.d, pelo que existem em diferentes variantes. Todos os arrays herdam desta classe.

Propriedades

public int Length {get;}
nombre total d'éléments du tableau, quelque soit son nombre de dimensions
public int Rank {get;}
nombre total de dimensions du tableau

Métodos

public static int BinarySearch<T>(T[] tableau,
 value)
rend la position de [value] dans tableau.
public static int BinarySearch<T>(T[] tableau,
nt index, int length, T value)
idem mais cherche dans tableau à partir de la
position [index] et sur [length] éléments
public static void Clear(Array tableau, int index,
int length)
met les [length] éléments de tableau commençant au
 n° [index] à 0 si numériques, false si booléens, null si références
public static void Copy(Array source,
Array destination, int length)
copie [length] éléments de source dans destination
public int GetLength(int i)
nombre d'éléments de la dimension n° i du tableau
public int GetLowerBound(int i)
indice du 1er élément de la dimension n° i
public int GetUpperBound(int i)
indice du dernier élément de la dimension n° i
public static int IndexOf<T>(T[] tableau,
T valeur)
rend la position de valeur dans tableau ou -1 si
valeur n'est pas trouvée.
public static void Resize<T>(ref T[] tableau,
int n)
redimensionne tableau à n éléments. Les éléments
déjà présents sont conservés.
public static void Sort<T>(T[] tableau,
IComparer<T> comparateur)
trie tableau selon un ordre défini par comparateur.
Cette méthode a été présentée au paragraphe Erreur : source de la référence non trouvée.

O programa seguinte ilustra a utilização de alguns métodos da classe Array:


using System;

namespace Chap3 {
    class Program {
        // tipo de pesquisa
        enum TypeRecherche { linéaire, dichotomique };

        // método principal
        static void Main(string[] args) {
            // leitura dos elementos de uma tabela introduzidos pelo teclado
            double[] éléments;
            Saisie(out éléments);
            // exibição da matriz não ordenada
            Affiche("Tableau non trié", éléments);
            // Pesquisa linear na tabela não ordenada
            Recherche(éléments, TypeRecherche.linéaire);
            // ordenação da tabela
            Array.Sort(éléments);
            // Exibição da tabela ordenada
            Affiche("Tableau trié", éléments);
            // Pesquisa dicotómica na tabela ordenada
            Recherche(éléments, TypeRecherche.dichotomique);
        }

        // Introdução dos valores da tabela de elementos
        // elementos: referência à tabela criada pelo método
        static void Saisie(out double[] éléments) {
            bool terminé = false;
            string réponse;
            bool erreur;
            double élément = 0;
            int i = 0;
            // Inicialmente, a tabela não existe
            éléments = null;
            // Ciclo de introdução dos elementos da tabela
            while (!terminé) {
                // pergunta
                Console.Write("Elément (réel) " + i + " du tableau (rien pour terminer) : ");
                // leitura da resposta
                réponse = Console.ReadLine().Trim();
                // fim da introdução se a cadeia estiver vazia
                if (réponse.Equals(""))
                    break;
                // verificação da introdução
                try {
                    élément = Double.Parse(réponse);
                    erreur = false;
                } catch {
                    Console.Error.WriteLine("Saisie incorrecte, recommencez");
                    erreur = true;
                }//try-catch
                // se não houver erro
                if (!erreur) {
                    // mais um elemento na matriz
                    i += 1;
                    // redimensionamento da matriz para acomodar o novo elemento
                    Array.Resize(ref éléments, i);
                    // inserção de um novo elemento
                    éléments[i - 1] = élément;
                }
            }//enquanto
        }

        // método genérico para apresentar os elementos de uma matriz
        static void Affiche<T>(string texte, T[] éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

        // pesquisa de um elemento na matriz
        // elementos: matriz de números reais
        // TypeRecherche: dicotômico ou linear
        static void Recherche(double[] éléments, TypeRecherche type) {
            // Pesquisa
            bool terminé = false;
            string réponse = null;
            double élément = 0;
            bool erreur = false;
            int i = 0;
            while (!terminé) {
                // pergunta
                Console.WriteLine("Elément cherché (rien pour arrêter) : ");
                // leitura e verificação da resposta
                réponse = Console.ReadLine().Trim();
                // terminado?
                if (réponse.Equals(""))
                    break;
                // verificação
                try {
                    élément = Double.Parse(réponse);
                    erreur = false;
                } catch {
                    Console.WriteLine("Erreur, recommencez...");
                    erreur = true;
                }//try-catch
                // se não houver erro
                if (!erreur) {
                    // procura-se o elemento na matriz
                    if (type == TypeRecherche.dichotomique)
                        // pesquisa dicotómica
                        i = Array.BinarySearch(éléments, élément);
                    else
                        // pesquisa linear
                        i = Array.IndexOf(éléments, élément);
                    // Exibição da resposta
                    if (i >= 0)
                        Console.WriteLine("Trouvé en position " + i);
                    else
                        Console.WriteLine("Pas dans le tableau");
                }//if
            }//while
        }
    }
}
  • linhas 27-62: o método Saisie insere os elementos de um tabuleiro éléments introduzidos através do teclado. Como não é possível dimensionar o tabuleiro antecipadamente (não se conhece o seu tamanho final), é necessário redimensioná-lo a cada novo elemento (linha 57). Um algoritmo mais eficiente teria sido alocar espaço para a tabela por grupos de N elementos. No entanto, uma tabela não foi concebida para ser redimensionada. Este caso é melhor tratado com uma lista (ArrayList, List<T>).
  • linhas 75-113: o método Recherche permite procurar no tabuleiro éléments um elemento digitado no teclado. O modo de pesquisa varia consoante o tabuleiro esteja ordenado ou não. Para um tabuleiro não ordenado, realiza-se uma pesquisa linear com o método IndexOf da linha 106. Para um tabuleiro ordenado, realiza-se uma pesquisa dicotómica com o método BinarySearch da linha 103.
  • linha 18: ordena-se a tabela éléments. Utiliza-se aqui uma variante de Sort que tem apenas um parâmetro: a tabela a ordenar. A relação de ordem utilizada para comparar os elementos da tabela é, então, a implícita desses elementos. Neste caso, os elementos são numéricos. É utilizada a ordem natural dos números.

Os resultados apresentados no ecrã são os seguintes:

Elément (réel) 0 du tableau (rien pour terminer) : 3,6
Elément (réel) 1 du tableau (rien pour terminer) : 7,4
Elément (réel) 2 du tableau (rien pour terminer) : -1,5
Elément (réel) 3 du tableau (rien pour terminer) : -7
Elément (réel) 4 du tableau (rien pour terminer) :
Tableau non trié----------------------------------
3,6
7,4
-1,5
-7
Elément cherché (rien pour arrêter) :
7,4
Trouvé en position 1
Elément cherché (rien pour arrêter) :
0
Pas dans le tableau
Elément cherché (rien pour arrêter) :

Tableau trié--------------------------------------
-7
-1,5
3,6
7,4
Elément cherché (rien pour arrêter) :
7,4
Trouvé en position 3
Elément cherché (rien pour arrêter) :
0
Pas dans le tableau
Elément cherché (rien pour arrêter) :

5.4. As coleções genéricas

Para além da tabela, existem várias classes para armazenar coleções de elementos. Existem versões genéricas no espaço de nomes System.Collections.Generic e versões não genéricas em System.Collections. Apresentaremos duas coleções genéricas frequentemente utilizadas: a lista e o dicionário.

A lista das coleções genéricas é a seguinte:

Image

5.4.1. A classe genérica List<T>

A classe System.Collections.Generic.List<T> permite implementar coleções de objetos do tipo T cujo tamanho varia durante a execução do programa. Um objeto do tipo List<T> é manipulado quase como um tabuleiro. Assim, o elemento i de uma lista l é denotado por l[i].

Existe também um tipo de lista não genérica: ArrayList, capaz de armazenar referências a quaisquer objetos. ArrayList é funcionalmente equivalente a List<Object>.. Um objeto ArrayList tem o seguinte aspeto:

Na imagem acima, os elementos 0, 1 e i da lista apontam para objetos de tipos diferentes. É necessário que um objeto seja criado primeiro antes de adicionar a sua referência à lista ArrayList. Embora um ArrayList armazene referências a objetos, é possível armazenar números nele. Isto é feito através de um mecanismo denominado Boxing: o número é encapsulado num objeto O do tipo Object e é a referência O que é armazenada na lista. Trata-se de um mecanismo transparente para o programador. Assim, pode escrever-se:

ArrayList liste=new ArrayList();
liste.Add(4);

Isto produzirá o seguinte resultado:

No exemplo acima, o número 4 foi encapsulado num objeto O e a referência O é armazenada na lista. Para o recuperar, pode-se escrever:


            int i = (int)liste[0];

A operação Object -> int é designada por Unboxing. Se uma lista for inteiramente composta por tipos int, declará-la como List<int> melhora o desempenho. Com efeito, os números do tipo int são então armazenados na própria lista e não em tipos Object externos à lista. As operações de boxing/unboxing deixam de ocorrer.

No caso de um objeto List<T> ou T ser uma classe, a lista armazena, mais uma vez, as referências dos objetos do tipo T:

Eis algumas das propriedades e métodos das listas genéricas:

Propriedades

public int Count {get;}
nombre d'éléments de la liste
public int Capacity {get;}
nombre d'éléments que la liste peut contenir avant d'être redimensionnée. Ce
redimensionnement se fait automatiquement. Cette notion de capacité de liste
est analogue à celle de capacité décrite pour la classe StringBuilder paragraphe Erreur : source de la référence non trouvée.

Métodos

public void Add(T item)
ajoute item à la liste
public int BinarySearch<T>(T item)
rend la position de item dans la liste s'il s'y
trouve sinon un nombre <0
public int BinarySearch<T>(T item,
IComparer<T> comparateur)
idem mais le 2ième paramètre permet de comparer deux
éléments de la liste. L'interface IComparer<T> a été
présentée au paragraphe Erreur : source de la référence non trouvée.
public void Clear()
supprime tous les éléments de la liste
public bool Contains(T item)
rend True si item est dans la liste, False sinon
public void CopyTo(T[] tableau)
copie les éléments de la liste dans tableau.
public int IndexOf(T item)
rend la position de item dans tableau ou -1 si
valeur n'est pas trouvée.
public void Insert(T item, int index)
insère item à la position index de la liste
public bool Remove(T item)
supprime item de la liste. Rend True si l'opération
réussit, False sinon.
public void RemoveAt(int index)
supprime l'élément n° index de la liste
public void Sort(IComparer<T> comparateur)
trie la liste selon un ordre défini par comparateur.
 Cette méthode a été présentée paragraphe Erreur : source de la référence non trouvée.
public void Sort()
trie la liste selon l'ordre défini par le type des
éléments de la liste
public T[] ToArray()
rend les éléments de la liste sous forme de tableau

Retomemos o exemplo abordado anteriormente com um objeto do tipo Array e processemo-lo agora com um objeto do tipo List<T>.. Como a lista é um objeto semelhante a uma matriz, o código sofre poucas alterações. Apresentamos apenas as alterações mais significativas:


using System;
using System.Collections.Generic;

namespace Chap3 {
    class Program {
        // tipo de pesquisa
        enum TypeRecherche { linéaire, dichotomique };

        // método principal
        static void Main(string[] args) {
            // leitura dos elementos de uma lista introduzidos pelo teclado
            List<double> éléments;
            Saisie(out éléments);
            // número de elementos
            Console.WriteLine("La liste a {0} éléments et une capacité de {1} éléments", éléments.Count, éléments.Capacity);
            // exibição da lista não ordenada
            Affiche("Liste non triée", éléments);
            // Pesquisa linear na lista não ordenada
            Recherche(éléments, TypeRecherche.linéaire);
            // ordenação da lista
            éléments.Sort();
            // exibição da lista ordenada
            Affiche("Liste triée", éléments);
            // Pesquisa dicotómica na lista ordenada
            Recherche(éléments, TypeRecherche.dichotomique);
        }

        // Introdução dos valores da lista de elementos
        // elementos: referência à lista criada pelo método
        static void Saisie(out List<double> éléments) {
...
            // Inicialmente, a lista está vazia
            éléments = new List<double>();
            // Ciclo de introdução dos elementos da lista
            while (!terminé) {
...
                // se não houver erros
                if (!erreur) {
                    // mais um elemento na lista
                    éléments.Add(élément);
                }
            }//while
        }

        // método genérico para apresentar os elementos de um objeto enumerável
        static void Affiche<T>(string texte, IEnumerable<T> éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

        // procura de um elemento na lista
        // elementos: lista de números reais
        // TypeRecherche: dicotômico ou linear
        static void Recherche(List<double> éléments, TypeRecherche type) {
...
            while (!terminé) {
...
                // se não houver erro
                if (!erreur) {
                    // procura-se o elemento na lista
                    if (type == TypeRecherche.dichotomique)
                        // pesquisa dicotómica
                        i = éléments.BinarySearch(élément);
                    else
                        // pesquisa linear
                        i = éléments.IndexOf(élément);
                    // Exibição da resposta
...
                }//if
            }//while
        }
    }
}
  • linhas 46-51: o método genérico Affiche<T> aceita dois parâmetros:
  • o primeiro parâmetro é um texto a escrever
  • o segundo parâmetro é um objeto que implementa a interface genérica IEnumerable<T>:
1
2
3
4
public interface IEnumerable<T>{
    IEnumerator GetEnumerator();
    IEnumerator<T> GetEnumerator();
}

A estrutura foreach( T elemento in elementos), na linha 48, é válida para qualquer objeto *éléments que implemente a interface *IEnumerable. As tabelas (Array) e as listas (List<T>) implementam a interface IEnumerable<T>. Assim, o método Affiche é adequado tanto para apresentar tabelas como listas.

Os resultados da execução do programa são os mesmos que no exemplo que utiliza a classe Array.

5.4.2. A classe Dictionary<TKey,TValue>

A classe System.Collections.Generic.Dictionary<TKey,TValue> permite implementar um dicionário. Pode-se considerar um dicionário como uma tabela com duas colunas:

chave
valor
chave1
valor1
chave2
valor2
..
...

Na classe Dictionary<TKey,TValue>, as chaves são do tipo Tkey e os valores do tipo TValue. As chaves são únicas, c.a.d, pelo que não podem existir duas chaves idênticas. Um dicionário deste tipo poderia ter o seguinte aspeto se os tipos TKey e TValue designassem classes:

O valor associado à chave C de um dicionário D é obtido através da notação D[C]. Este valor é de leitura e escrita. Assim, pode-se escrever:

1
2
3
4
5
TValue v=...;
TKey c=...;
Dictionary<TKey,TValue> D=new Dictionary<TKey,TValue>();
D[c]=v;
v=D[c];

Se a chave c não existir no dicionário D, a notação D[c] lança uma exceção.

Os principais métodos e propriedades da classe ***Dictionary&lt;TKey,TValue&gt;*** são os seguintes:

Construtores

public Dictionary<TKey,TValue>()
constructeur sans paramètres - construit un dictionnaire vide.
Il existe plusieurs autres constructeurs.

Propriedades

public int Count {get;}
nombre d'entrées (clé, valeur) dans le dictionnaire
public Dictionary<TKey,TValue>.KeyCollection Keys {get;}
collection des clés du dictionnaire.
public Dictionary<TKey,TValue>.ValueCollection Values {get;}
collection des valeurs du dictionnaire.

Métodos

public void Add(TKey key, TValue value)
ajoute le couple (key, value) au dictionnaire
public void Clear()
supprime tous les couples du dictionnaire
public bool ContainsKey (TKey key)
rend True si key est une clé du dictionnaire,
False sinon
public bool ContainsValue (TValue value)
rend True si value est une valeur du dictionnaire,
False sinon
public void CopyTo(T[] tableau)
copie les éléments de la liste dans tableau.
public bool Remove(TKey key)
supprime du dictionnaire le couple de clé key.
Rend True si l'opération réussit, False sinon.
public bool TryGetValue(TKey key,
out TValue value)
rend dans value, la valeur associée à la clé key si
cette dernière existe, sinon rend la valeur par
défaut du type TValue (0 pour les nombres, false
pour les booléens, null pour les références d'objet)

Consideremos o seguinte programa de exemplo:


using System;
using System.Collections.Generic;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // criação de um dicionário <string,int>
            string[] liste = { "jean:20", "paul:18", "mélanie:10", "violette:15" };
            string[] champs = null;
            char[] séparateurs = new char[] { ':' };
            Dictionary<string,int> dico = new Dictionary<string,int>();
            for (int i = 0; i <liste.Length; i++) {
                champs = liste[i].Split(séparateurs);
                dico[champs[0]]= int.Parse(champs[1]);
            }//for
            // número de elementos no dicionário
            Console.WriteLine("Le dictionnaire a " + dico.Count + " éléments");
            // lista de chaves
            Affiche("[Liste des clés]",dico.Keys);
            // lista de valores
            Affiche("[Liste des valeurs]", dico.Values);
            // lista de chaves e valores
            Console.WriteLine("[Liste des clés & valeurs]");
            foreach (string clé in dico.Keys) {
                Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
            }
            // elimina-se a chave «paul»
            Console.WriteLine("[Suppression d'une clé]");
            dico.Remove("paul");
            // lista de chaves e valores
            Console.WriteLine("[Liste des clés & valeurs]");
            foreach (string clé in dico.Keys) {
                Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
            }
            // pesquisa no dicionário
            String nomCherché = null;
            Console.Write("Nom recherché (rien pour arrêter) : ");
            nomCherché = Console.ReadLine().Trim();
            int value;
            while (!nomCherché.Equals("")) {
                dico.TryGetValue(nomCherché, out value);
                if (value!=0) {
                    Console.WriteLine(nomCherché + "," + value);
                } else {
                    Console.WriteLine("Nom " + nomCherché + " inconnu");
                }
                // próxima pesquisa
                Console.Out.Write("Nom recherché (rien pour arrêter) : ");
                nomCherché = Console.ReadLine().Trim();
            }//while
        }

        // método genérico para apresentar os elementos de um tipo enumerável
        static void Affiche<T>(string texte, IEnumerable<T> éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

    }
}
  • linha 8: um array de string que servirá para inicializar o dicionário <string,int>
  • linha 11: o dicionário <string,int>
  • linhas 12-15: a sua inicialização a partir da matriz de string da linha 8
  • linha 17: número de entradas do dicionário
  • linha 19: as chaves do dicionário
  • linha 21: os valores do dicionário
  • linha 29: eliminação de uma entrada do dicionário
  • linha 41: pesquisa de uma chave no dicionário. Se esta não existir, o método TryGetValue atribuirá o valor 0 a value, uma vez que value é do tipo numérico. Esta técnica só é aplicável aqui porque sabemos que o valor 0 não consta do dicionário.

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

Le dictionnaire a 4 éléments
[Liste des clés]----------------------------------
jean
paul
mélanie
violette
[Liste des valeurs]-------------------------------
20
18
10
15
[Liste des clés & valeurs]
clé=jean valeur=20
clé=paul valeur=18
clé=mélanie valeur=10
clé=violette valeur=15
[Suppression d'une clé]
[Liste des clés & valeurs]
clé=jean valeur=20
clé=mélanie valeur=10
clé=violette valeur=15
Nom recherché (rien pour arrêter) : violette
violette,15
Nom recherché (rien pour arrêter) : x
Nom x inconnu

5.5. Os ficheiros de texto

5.5.1. A classe StreamReader

A classe System.IO.StreamReader permite ler o conteúdo de um ficheiro de texto. Na verdade, é capaz de processar fluxos que não são ficheiros. Aqui estão algumas das suas propriedades e métodos:

Construtores

public StreamReader(string path)
construit un flux de lecture à partir du fichier de chemin path. Le
contenu du fichier peut être encodé de diverses façons. Il existe un
constructeur qui permet de préciser le codage utilisé. Par défaut,
c'est le codage UTF-8 qui est utilisé.

Propriedades

public bool EndOfStream {get;}
True si le flux a été lu entièrement

Métodos

public void Close()
ferme le flux et libère les ressources allouées pour
sa gestion. A faire obligatoirement après
exploitation du flux.
public override int Peek()
rend le caractère suivant du flux sans le consommer.
Un Peek supplémentaire rendrait donc le même
caractère.
public override int Read()
rend le caractère suivant du flux et avance d'un
caractère dans le flux.
public override int Read(char[] buffer,
int index, int count)
lit count caractères dans le flux et les met dans
buffer à partir de la position index. Rend le nombre
de caractères lus - peut être 0.
public override string ReadLine()
rend la ligne suivante du flux ou null si on était à
la fin du flux.
public override string ReadToEnd()
rend la fin du flux ou "" si on était à la fin du
flux.

Eis um exemplo:


using System;
using System.IO;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // diretório de execução
            Console.WriteLine("Répertoire d'exécution : "+Environment.CurrentDirectory);
            string ligne = null;
            StreamReader fluxInfos = null;
            // leitura do conteúdo do ficheiro infos.txt
            try {
                // leitura 1
                Console.WriteLine("Lecture 1----------------");
                using (fluxInfos = new StreamReader("infos.txt")) {
                    ligne = fluxInfos.ReadLine();
                    while (ligne != null) {
                        Console.WriteLine(ligne);
                        ligne = fluxInfos.ReadLine();
                    }
                }
                // leitura 2
                Console.WriteLine("Lecture 2----------------");
                using (fluxInfos = new StreamReader("infos.txt")) {
                    Console.WriteLine(fluxInfos.ReadToEnd());
                }
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
            }
        }
    }
}
  • linha 8: apresenta o nome do diretório de execução
  • linhas 12 e 27: um try/catch para gerir uma eventual exceção.
  • linha 15: a estrutura «using flux=new StreamReader(...)» é uma facilidade para não ter de fechar explicitamente o fluxo após a sua utilização. Este encerramento é feito automaticamente assim que se sai do âmbito do using.
  • linha 15: o ficheiro lido chama-se infos.txt. Como se trata de um nome relativo, será procurado no diretório de execução indicado na linha 8. Se não estiver lá, será lançada uma exceção, que será gerida pelo try/catch.
  • linhas 16-20: o ficheiro é lido linha a linha
  • linha 25: o ficheiro é lido de uma só vez

O ficheiro infos.txt é o seguinte:

12620:0:0
13190:0,05:631
15640:0,1:1290,5

e está localizado na seguinte pasta do projeto C#:

Vamos descobrir que bin/Release é a pasta de execução quando o projeto é executado através de Ctrl-F5.

A execução produz os seguintes resultados:

1
2
3
4
5
6
7
8
9
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Lecture 1----------------
12620:0:0
13190:0,05:631
15640:0,1:1290,5
Lecture 2----------------
12620:0:0
13190:0,05:631
15640:0,1:1290,5

Se, na linha 15, introduzirmos o nome do ficheiro xx.txt, obtemos os seguintes resultados:

1
2
3
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Lecture 1----------------
L'erreur suivante s'est produite : Could not find file 'C:\...\Chap3\07\bin\Release\xx.txt'.

5.5.2. A classe StreamWriter

A classe System.IO.StreamReader permite escrever num ficheiro de texto. Tal como a classe StreamReader, é, na verdade, capaz de processar fluxos que não são ficheiros. Aqui estão algumas das suas propriedades e métodos:

Construtores

public StreamWriter(string path)
construit un flux d'écriture dans le fichier de chemin path. Le
contenu du fichier peut être encodé de diverses façons. Il existe un
constructeur qui permet de préciser le codage utilisé. Par défaut,
c'est le codage UTF-8 qui est utilisé.

Propriedades

public virtual bool AutoFlush
{get;set;}
fixe le mode d'écriture dans le fichier du buffer associé au flux. Si
égal à False, l'écriture dans le flux n'est pas immédiate : il y a
d'abord écriture dans une mémoire tampon puis dans le fichier lorsque la
mémoire tampon est pleine sinon l'écriture dans le fichier est immédiate
(pas de tampon intermédiaire). Par défaut c'est le mode tamponné qui est
utilisé. Le tampon n'est écrit dans le fichier que lorsqu'il est plein ou
bien lorsqu'on le vide explicitement par une opération Flush ou encore
lorsqu'on ferme le flux StreamWriter par une opération Close. Le mode
AutoFlush=False est le plus efficace lorsqu'on travaille avec des
fichiers parce qu'il limite les accès disque. C'est le mode par défaut
pour ce type de flux. Le mode AutoFlush=False ne convient pas à tous les
flux, notamment les flux réseau. Pour ceux-ci, qui souvent prennent place
dans un dialogue entre deux partenaires, ce qui est écrit par l'un des
partenaires doit être immédiatement lu par l'autre. Le flux d'écriture
doit alors être en mode AutoFlush=True.
public virtual string NewLine {get;set;}
les caractères de fin de ligne. Par défaut "\r\n". Pour un système Unix,
il faudrait utiliser "\n".

Métodos

public void Close()
ferme le flux et libère les ressources allouées pour sa
gestion. A faire obligatoirement après exploitation du flux.
public override void Flush()
écrit dans le fichier, le buffer du flux, sans attendre qu'il
soit plein.
public virtual void Write(T value)
écrit value dans le fichier associé au flux. Ici T n'est pas
un type générique mais symbolise le fait que la méthode
Write accepte différents types de paramètres (string, int,
object, ...). La méthode value.ToString est utilisée pour
produire la chaîne écrite dans le fichier.
public virtual void WriteLine(T value)
même chose que Write mais avec la marque de fin de ligne
(NewLine) en plus.

Consideremos o seguinte exemplo:


using System;
using System.IO;

namespace Chap3 {
    class Program2 {
        static void Main(string[] args) {
            // diretório de execução
            Console.WriteLine("Répertoire d'exécution : " + Environment.CurrentDirectory);
            string ligne = null;                        // uma linha de texto
            StreamWriter fluxInfos = null;    // o ficheiro de texto
            try {
                // criação do ficheiro de texto
                using (fluxInfos = new StreamWriter("infos2.txt")) {
                    Console.WriteLine("Mode AutoFlush : {0}", fluxInfos.AutoFlush);
                    // leitura da linha digitada no teclado
                    Console.Write("ligne (rien pour arrêter) : ");
                    ligne = Console.ReadLine().Trim();
                    // loop enquanto a linha introduzida não estiver vazia
                    while (ligne != "") {
                        // gravação da linha no ficheiro de texto
                        fluxInfos.WriteLine(ligne);
                        // leitura de uma nova linha introduzida pelo teclado
                        Console.Write("ligne (rien pour arrêter) : ");
                        ligne = Console.ReadLine().Trim();
                    }//while
                }
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
            }
        }
    }
}
  • linha 13: mais uma vez, utilizamos a sintaxe using(flux) para não termos de fechar explicitamente o fluxo através de uma operação Close. Este encerramento é efetuado automaticamente à saída do using.
  • Porquê um try/catch, nas linhas 11 e 27? Na linha 13, poderíamos indicar um nome de ficheiro no formato /rep1/rep2/ .../fichier com um caminho /rep1/rep2/... que não existe, tornando assim impossível a criação de fichier. Seria então lançada uma exceção. Existem outros casos possíveis de exceção (disco cheio, direitos insuficientes, ...)

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

1
2
3
4
5
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Mode AutoFlush : False
ligne (rien pour arrêter) : 1ère ligne
ligne (rien pour arrêter) : 2ième ligne
ligne (rien pour arrêter) :

O ficheiro infos2.txt foi criado na pasta bin/Release do projeto:

 

5.6. Os ficheiros binários

As classes System.IO.BinaryReader e System.IO.BinaryWriter servem para ler e escrever ficheiros binários.

Consideremos a seguinte aplicação:

// sintaxe pg texto bin logs
// lê-se um ficheiro de texto (texto) e o seu conteúdo é guardado num ficheiro binário (bin
// o ficheiro de texto contém linhas do tipo nome : idade, que serão armazenadas numa estrutura string, int
// (logs) é um ficheiro de texto de registos

O ficheiro de texto tem o seguinte conteúdo:

1
2
3
4
5
6
7
8
9
paul : 10
helene : 15

jacques : 11
sylvain : 12
xx : -1

xx: yy : zz
xx : yy

O programa é o seguinte:


using System;
using System.IO;

// sintaxe pg texto bin logs
// lê-se um ficheiro de texto (texto) e o seu conteúdo é armazenado num ficheiro binário (bin)
// o ficheiro de texto contém linhas do tipo nome : idade, que serão armazenadas numa estrutura string, int
// (logs) é um ficheiro de texto de registos

namespace Chap3 {
    class Program {
        static void Main(string[] arguments) {
            // são necessários 3 argumentos
            if (arguments.Length != 3) {
                Console.WriteLine("syntaxe : pg texte binaire log");
                Environment.Exit(1);
            }//if

            // variáveis
            string ligne=null;
            string nom=null;
            int age=0;
            int numLigne = 0;
            char[] séparateurs = new char[] { ':' };
            string[] champs=null;
            StreamReader input = null;
            BinaryWriter output = null;
            StreamWriter logs = null;
            bool erreur = false;
            // leitura de ficheiro de texto - escrita de ficheiro binário
            try {
                // abertura do ficheiro de texto para leitura
                input = new StreamReader(arguments[0]);
                // abertura do ficheiro binário para escrita
                output = new BinaryWriter(new FileStream(arguments[1], FileMode.Create, FileAccess.Write));
                // abertura do ficheiro de registos para escrita
                logs = new StreamWriter(arguments[2]);
                // análise do ficheiro de texto
                while ((ligne = input.ReadLine()) != null) {
                    // mais uma linha
                    numLigne++;
                    // linha vazia?
                    if (ligne.Trim() == "") {
                        // ignora-se
                        continue;
                    }
                    // uma linha com nome: idade
                    champs = ligne.Split(séparateurs);
                    // são necessários 2 campos
                    if (champs.Length != 2) {
                        // regista-se o erro
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nombre de champs incorrect", numLigne, arguments[0]);
                        // linha seguinte
                        continue;
                    }//if
                    // o primeiro campo não pode estar vazio
                    erreur = false;
                    nom = champs[0].Trim();
                    if (nom == "") {
                        // regista-se o erro
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nom vide", numLigne, arguments[0]);
                        erreur = true;
                    }
                    // o segundo campo deve ser um número inteiro >=0
                    if (!int.TryParse(champs[1],out age) || age<0) {
                        // regista-se o erro
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un âge [{2}] incorrect", numLigne, arguments[0], champs[1].Trim());
                        erreur = true;
                    }//se
                    // se não houver erro, gravam-se os dados no ficheiro binário
                    if (!erreur) {
                        output.Write(nom);
                        output.Write(age);
                    }
                    // linha seguinte
                }//enquanto
            }catch(Exception e){
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // fecho dos ficheiros
                if(input!=null) input.Close();
                if(output!=null) output.Close();
                if(logs!=null) logs.Close();
            }
        }
    }
}

Vamos analisar as operações relacionadas com a classe BinaryWriter:

  • linha 34: o objeto BinaryWriter é aberto pela operação

            output=new BinaryWriter(new FileStream(arguments[1],FileMode.Create,FileAccess.Write));

O argumento do construtor deve ser um fluxo (Stream). Neste caso, trata-se de um fluxo criado a partir de um ficheiro (FileStream), cujos dados são:

  • (continuação)
    • o nome
    • a operação a realizar, neste caso FileMode.Create para criar o ficheiro
    • o tipo de acesso, neste caso FileAccess.Write para acesso em escrita ao ficheiro
  • linhas 70-73: as operações de escrita
             // escrevem-se os dados no ficheiro binário
            output.Write(nom);
            output.Write(age);

A classe BinaryWriter dispõe de vários métodos Write sobrecarregados para gravar os diferentes tipos de dados simples

  • linha 81: a operação de encerramento do fluxo
        output.Close();

Os três argumentos do método Main são fornecidos ao projeto (através das suas propriedades) [1] e o ficheiro de texto a processar é colocado na pasta bin/Release [2]:

Com o seguinte ficheiro [personnes1.txt]:

1
2
3
4
5
6
7
8
9
paul : 10
helene : 15

jacques : 11
sylvain : 12
xx : -1

xx: yy : zz
xx : yy

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

  • no [1], o ficheiro binário [personnes1.bin] criado, bem como o ficheiro de registos [logs.txt]. Este último tem o seguinte conteúdo:
1
2
3
La ligne n° [6] du fichier [personnes1.txt] a un âge [-1] incorrect
La ligne n° [8] du fichier [personnes1.txt] a un nombre de champs incorrect
La ligne n° [9] du fichier [personnes1.txt] a un âge [yy] incorrect

O conteúdo do ficheiro binário [personnes1.bin] será fornecido pelo programa que se segue. Este programa também aceita três argumentos:

// sintaxe pg bin texto logs
// lê-se um ficheiro binário «bin» e o seu conteúdo é guardado num ficheiro de texto («texto»)
// o ficheiro binário tem uma estrutura string, int
// o ficheiro de texto contém linhas com o formato nome : idade
// logs é um ficheiro de texto com registos

Fazemos, portanto, a operação inversa. Lemos um ficheiro binário para criar um ficheiro de texto. Se o ficheiro de texto produzido for idêntico ao ficheiro original, saberemos que a conversão texto --> binário --> texto decorreu com sucesso. O código é o seguinte:


using System;
using System.IO;

// sintaxe pg bin texto logs
// lê-se um ficheiro binário «bin» e o seu conteúdo é guardado num ficheiro de texto («texto»)
// o ficheiro binário tem uma estrutura string, int
// o ficheiro de texto contém linhas com o formato nome : idade
// logs é um ficheiro de texto com registos

namespace Chap3 {
    class Program2 {
        static void Main(string[] arguments) {
            // são necessários 3 argumentos
            if (arguments.Length != 3) {
                Console.WriteLine("syntaxe : pg binaire texte log");
                Environment.Exit(1);
            }//if

            // variáveis
            string nom = null;
            int age = 0;
            int numPersonne = 1;
            BinaryReader input = null;
            StreamWriter output = null;
            StreamWriter logs = null;
            bool fini;
            // leitura de ficheiro binário - escrita de ficheiro de texto
            try {
                // abertura do ficheiro binário para leitura
                input = new BinaryReader(new FileStream(arguments[0], FileMode.Open, FileAccess.Read));
                // abertura do ficheiro de texto para escrita
                output = new StreamWriter(arguments[1]);
                // abertura do ficheiro de registos para escrita
                logs = new StreamWriter(arguments[2]);
                // análise do ficheiro binário
                fini = false;
                while (!fini) {
                    try {
                        // leitura do nome
                        nom = input.ReadString().Trim();
                        // leitura da idade
                        age = input.ReadInt32();
                        // gravação no ficheiro de texto
                        output.WriteLine(nom + ":" + age);
                        // pessoa seguinte
                        numPersonne++;
                    } catch (EndOfStreamException) {
                        fini = true;
                    } catch (Exception e) {
                        logs.WriteLine("L'erreur suivante s'est produite à la lecture de la personne n° {0} : {1}", numPersonne, e.Message);
                    }
                }//while
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // fecho dos ficheiros
                if (input != null)
                    input.Close();
                if (output != null)
                    output.Close();
                if (logs != null)
                    logs.Close();
            }
        }
    }
}

Vamos analisar as operações relacionadas com a classe BinaryReader:

  • linha 30: o objeto BinaryReader é aberto pela operação

            input=new BinaryReader(new FileStream(arguments[0],FileMode.Open,FileAccess.Read));

O argumento do construtor deve ser um fluxo (Stream). Neste caso, trata-se de um fluxo construído a partir de um ficheiro (FileStream), para o qual se fornece:

  • (continuação)
    • o nome
    • a operação a realizar, neste caso FileMode.Open para abrir um ficheiro existente
    • o tipo de acesso, neste caso FileAccess.Read para um acesso de leitura ao ficheiro
  • linhas 40, 42: as operações de leitura
nom=input.ReadString().Trim();
age=input.ReadInt32();

A classe BinaryReader dispõe de vários métodos ReadXX para ler os diferentes tipos de dados simples

  • linha 60: a operação de encerramento do fluxo
        input.Close();

Se executarmos os dois programas em sequência, transformando personnes1.txt em personnes1.bin e, em seguida, personnes1.bin em personnes2.txt2, obtemos os seguintes resultados:

  • em [1], o projeto está configurado para executar a segunda aplicação
  • em [2], os argumentos passados para Main
  • em [3], os ficheiros produzidos pela execução da aplicação.

O conteúdo de [personnes2.txt] é o seguinte:

1
2
3
4
paul:10
helene:15
jacques:11
sylvain:12

5.7. As expressões regulares

A classe System.Text.RegularExpressions.Regex permite a utilização de expressões regulares. Estas permitem verificar o formato de uma cadeia de caracteres. Assim, é possível verificar se uma cadeia que representa uma data está, de facto, no formato dd/mm/aa. Para tal, utiliza-se um modelo e compara-se a cadeia com esse modelo. Assim, neste exemplo, j, m e a devem ser algarismos. O modelo de um formato de data válido é, então, «\d\d/\d\d/\d\d», em que o símbolo \d representa um algarismo. Os símbolos que podem ser utilizados num modelo são os seguintes:

Caractère
Descrição
\ 
Marca o caractere seguinte como caractere especial ou literal. Por exemplo, «n» corresponde ao caractere «n». «\n» corresponde a um caractere de nova linha. A sequência «\\» corresponde a «\», enquanto que «\»( corresponde a «(».
^ 
Corresponde ao início da entrada.
$ 
Corresponde ao fim da introdução.
* 
Corresponde ao carácter anterior zero ou mais vezes. Assim, «zo*» corresponde a «z» ou a «zoo».
+ 
Corresponde ao carácter anterior uma ou mais vezes. Assim, «zo+» corresponde a «zoo», mas não a «z».
? 
Corresponde ao carácter anterior zero ou uma vez. Por exemplo, «a?ve?» corresponde a «ve» em «lever».
.
Corresponde a qualquer carácter único, exceto o carácter de nova linha.
(modèle) 
Procura o modèle e memoriza a correspondência. A subcadeia correspondente pode ser extraída da coleção Matches obtida, utilizando o Item [0]...[n]. Para encontrar correspondências com caracteres entre parênteses ( ), utilize "\(" ou "\)".
x|y
Corresponde a x ou a y. Por exemplo, «z|foot» corresponde a «z» ou a «foot». «(z|f)oo» corresponde a «zoo» ou a «foo».
{n}
n é um número inteiro não negativo. Corresponde exatamente a n vezes o carácter. Por exemplo, «o{2}» não corresponde a «o» em «Bob», mas aos dois primeiros «o» em «fooooot».
{n,} 
n é um número inteiro não negativo. Corresponde a, pelo menos, n vezes o carácter. Por exemplo, «o{2,}» não corresponde ao «o» em «Bob», mas a todos os «o» em «fooooot». «o{1,}» equivale a «o+» e «o{0,}» equivale a «o*».
{n,m} 
m e n são números inteiros não negativos. Corresponde a, no mínimo, n e, no máximo, m vezes o carácter. Por exemplo, «o{1,3}» corresponde aos três primeiros «o» em «foooooot» e «o{0,1}» equivale a «o?».
[xyz] 
Conjunto de caracteres. Corresponde a um dos caracteres indicados. Por exemplo, «[abc]» corresponde a «a» em «plat».
[^xyz] 
Conjunto de caracteres negativo. Corresponde a qualquer caractere não indicado. Por exemplo, «[^abc]» corresponde a «p» em «plat».
[a-z] 
Intervalo de caracteres. Corresponde a qualquer caractere da série especificada. Por exemplo, «[a-z]» corresponde a qualquer letra minúscula entre «a» e «z».
[^m-z] 
Intervalo de caracteres negativo. Corresponde a qualquer caractere que não se encontre na série especificada. Por exemplo, «[^m-z]» corresponde a qualquer caractere que não se encontre entre «m» e «z».
\b 
Corresponde a um limite que representa uma palavra, ou seja, à posição entre uma palavra e um espaço. Por exemplo, «er\b» corresponde a «er» em «lever», mas não a «er» em «verbe».
\B 
Corresponde a um limite que não representa uma palavra. «en*t\B» corresponde a «ent» em «bien entendu».
\d 
Corresponde a um carácter que representa um algarismo. É equivalente a [0-9].
\D 
Corresponde a um carácter que não representa um algarismo. É equivalente a [^0-9].
\f 
Corresponde a um carácter de salto de página.
\n 
Corresponde a um carácter de nova linha.
\r 
Corresponde a um carácter de retorno de carro.
\s 
Corresponde a qualquer espaço em branco, incluindo espaço, tabulação, salto de página, etc. É equivalente a «[ \f\n\r\t\v]».
\S 
Corresponde a qualquer caractere de espaço não em branco. É equivalente a «[^ \f\n\r\t\v]».
\t 
Corresponde a um carácter de tabulação.
\v 
Corresponde a um carácter de tabulação vertical.
\w 
Corresponde a qualquer carácter que represente uma palavra e inclua um sublinhado. É equivalente a «[A-Za-z0-9_]».
\W 
Corresponde a qualquer carácter que não represente uma palavra. É equivalente a «[^A-Za-z0-9_]».
\num 
Corresponde a num, em que num é um número inteiro positivo. Refere-se às correspondências memorizadas. Por exemplo, «(.)\1» corresponde a dois caracteres idênticos consecutivos.
\n
Corresponde a n, em que n é um valor de escape octal. Os valores de escape octais devem ter 1, 2 ou 3 dígitos. Por exemplo, «\11» e «\011» correspondem ambos a um carácter de tabulação. «\0011» equivale a «\001» e «1». Os valores de escape octais não devem exceder 256. Se tal acontecer, apenas os dois primeiros dígitos serão considerados na expressão. Permite utilizar os códigos ASCII em expressões regulares.
\xn
Corresponde a n, em que n é um valor de escape hexadecimal. Os valores de escape hexadecimais têm de incluir obrigatoriamente dois dígitos. Por exemplo, «\x41» corresponde a «A». «\x041» equivale a «\x04» e «1». Permite utilizar os códigos ASCII em expressões regulares.

Um elemento num modelo pode estar presente uma ou mais vezes. Vejamos alguns exemplos relacionados com o símbolo \d, que representa um algarismo:

modelo
significado
\d
um algarismo
\d?
0 ou 1 dígito
\d*
0 ou mais dígitos
\d+
1 ou mais dígitos
\d{2}
2 algarismos
\d{3,}
pelo menos 3 algarismos
\d{5,7}
entre 5 e 7 dígitos

Imaginemos agora o modelo capaz de descrever o formato esperado para uma cadeia de caracteres:

cadeia procurada
modelo
uma data no formato dd/mm/aa
\d{2}/\d{2}/\d{2}
uma hora no formato hh:mm:ss
\d{2}:\d{2}:\d{2}
um número inteiro sem sinal
\d+
uma sequência de espaços, que pode estar vazia
\s*
um número inteiro sem sinal que pode ser precedido ou seguido de espaços
\s*\d+\s*
um número inteiro que pode ser com sinal e precedido ou seguido de espaços
\s*[+|-]?\s*\d+\s*
um número real sem sinal que pode ser precedido ou seguido de espaços
\s*\d+(.\d*)?\s*
um número real que pode ser com sinal e precedido ou seguido de espaços
\s*[+|]?\s*\d+(.\d*)?\s*
uma cadeia de caracteres que contém a palavra juste
\bjuste\b
  

É possível especificar onde se procura o padrão na cadeia:

padrão
significado
^padrão
o padrão inicia a cadeia
padrão$
o modelo termina a cadeia
^padrão$
o modelo inicia e termina a cadeia
padrão
o padrão é procurado em toda a cadeia, começando pelo início da mesma.
cadeia procurada
padrão
uma cadeia que termina com um ponto de exclamação
!$
uma cadeia que termina com um ponto
\.$
uma cadeia que começa com a sequência //
^//
uma cadeia que contém apenas uma palavra, eventualmente seguida ou precedida de espaços
^\s*\w+\s*$
uma cadeia que contenha duas palavras, eventualmente seguidas ou precedidas de espaços
^\s*\w+\s*\w+\s*$
uma cadeia que contenha a palavra secret
\bsecret\b

Os subconjuntos de um padrão podem ser «recuperados». Assim, não só é possível verificar se uma cadeia corresponde a um padrão específico, como também é possível recuperar nessa cadeia os elementos correspondentes aos subconjuntos do padrão que foram colocados entre parênteses. Assim, se analisarmos uma cadeia de caracteres que contenha uma data dd/mm/aa e, além disso, quisermos extrair os elementos dd, mm e aa dessa data, utilizaremos o padrão (\d\d)/(\d\d)/(\d\d).

5.7.1. Verificar se uma cadeia corresponde a um modelo determinado

Um objeto do tipo Regex é construído da seguinte forma:

public Regex(string pattern)
construit un objet "expression régulière" à partir d'un modèle passé
en paramètre (pattern)

Depois de criada a expressão regular modelo, é possível compará-la com cadeias de caracteres utilizando o método IsMatch:

public bool IsMatch(string input)
vrai si la chaîne input correspond au modèle de l'expression
régulière

Eis um exemplo:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // uma expressão regular modelo
            string modèle1 = @"^\s*\d+\s*$";
            Regex regex1 = new Regex(modèle1);
            // comparar um exemplar com o padrão
            string exemplaire1 = "  123  ";
            if (regex1.IsMatch(exemplaire1)) {
                Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire1, modèle1);
            } else {
                Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire1, modèle1);
            }//if
            string exemplaire2 = "  123a  ";
            if (regex1.IsMatch(exemplaire2)) {
                Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire2, modèle1);
            } else {
                Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire2, modèle1);
            }//if
        }

    }
}

e os resultados da execução:

[  123  ] correspond au modèle [^\s*\d+\s*$]
[  123a  ] ne correspond pas au modèle [^\s*\d+\s*$]

5.7.2. Encontrar todas as ocorrências de um padrão numa cadeia

O método Matches permite recuperar os elementos de uma cadeia de caracteres que correspondam a um padrão:

public MatchCollection Matches(string input)
rend la collection des éléments de la chaîne input
correspondant au modèle

A classe MatchCollection possui uma propriedade Count que corresponde ao número de elementos da coleção. Se résultats for um objeto MatchCollection, então [i] é o elemento i dessa coleção e é do tipo Match. A classe Match possui várias propriedades, entre as quais as seguintes:

  • Value: o valor do objeto Match,, ou seja, um elemento que corresponde ao modelo
  • Index: a posição em que o elemento foi encontrado na cadeia explorada

Analisemos o seguinte exemplo:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program2 {
        static void Main(string[] args) {
            // várias ocorrências do modelo no exemplo
            string modèle2 = @"\d+";
            Regex regex2 = new Regex(modèle2);
            string exemplaire3 = "  123  456  789 ";
            MatchCollection résultats = regex2.Matches(exemplaire3);
            Console.WriteLine("Modèle=[{0}],exemplaire=[{1}]", modèle2, exemplaire3);
            Console.WriteLine("Il y a {0} occurrences du modèle dans l'exemplaire ", résultats.Count);
            for (int i = 0; i < résultats.Count; i++) {
                Console.WriteLine("[{0}] trouvé en position {1}", résultats[i].Value, résultats[i].Index);
            }//for
        }
    }
}
  • linha 8: o padrão procurado é uma sequência de algarismos
  • linha 10: a cadeia na qual se procura este padrão
  • linha 11: recuperam-se todos os elementos de exemplaire3 que correspondem ao padrão modèle2
  • linhas 14-16: exibem-se os resultados

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

1
2
3
4
5
Modèle=[\d+],exemplaire=[  123  456  789 ]
Il y a 3 occurrences du modèle dans l'exemplaire
[123] trouvé en position 2
[456] trouvé en position 7
[789] trouvé en position 12

5.7.3. Recuperar partes de um modelo

É possível «recuperar» subconjuntos de um modelo. Assim, não só se pode verificar se uma cadeia de caracteres corresponde a um modelo específico, como também se pode recuperar nessa cadeia os elementos correspondentes aos subconjuntos do modelo que foram colocados entre parênteses. Assim, se analisarmos uma cadeia de caracteres que contenha uma data dd/mm/aa e quisermos, além disso, extrair os elementos dd, mm e aa dessa data, utilizaremos o modelo (\d\d)/(\d\d)/(\d\d).

Vejamos o seguinte exemplo:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program3 {
        static void Main(string[] args) {
            // captura de elementos no modelo
            string modèle3 = @"(\d\d):(\d\d):(\d\d)";
            Regex regex3 = new Regex(modèle3);
            string exemplaire4 = "Il est 18:05:49";
            // verificação do modelo
            Match résultat = regex3.Match(exemplaire4);
            if (résultat.Success) {
                // a instância corresponde ao modelo
                Console.WriteLine("L'exemplaire [{0}] correspond au modèle [{1}]",exemplaire4,modèle3);
                // são apresentados os grupos de parênteses
                for (int i = 0; i < résultat.Groups.Count; i++) {
                    Console.WriteLine("groupes[{0}]=[{1}] trouvé en position {2}",i, résultat.Groups[i].Value,résultat.Groups[i].Index);
                }//for
            } else {
                // a instância não corresponde ao modelo
                Console.WriteLine("L'exemplaire[{0}] ne correspond pas au modèle [{1}]", exemplaire4, modèle3);
            }
        }
    }
}

A execução deste programa produz os seguintes resultados:

1
2
3
4
5
L'exemplaire [Il est 18:05:49] correspond au modèle [(\d\d):(\d\d):(\d\d)]
groupes[0]=[18:05:49] trouvé en position 7
groupes[1]=[18] trouvé en position 7
groupes[2]=[05] trouvé en position 10
groupes[3]=[49] trouvé en position 13

A novidade encontra-se nas linhas 12-19:

  • linha 12: a cadeia exemplaire4 é comparada com o modelo regex3 através do método Match. Este método devolve um objeto Match já apresentado. Utilizamos aqui duas novas propriedades desta classe:
  • Success (linha 13): indica se houve correspondência
  • Groups (linhas 17, 18): coleção em que
    • Groups[0] corresponde à parte da cadeia que corresponde ao modelo
    • Groups[i] (i>=1) corresponde ao grupo de parênteses n.º i

Se résultat for do tipo Match, então résultats.Groups é do tipo GroupCollection e résultats.Groups[i] é do tipo Group. A classe Group tem duas propriedades que utilizamos aqui:

  • Value (linha 18): o valor do objeto Group, que é o elemento correspondente ao conteúdo de um parêntese
  • Index (linha 18): a posição em que o elemento foi encontrado na cadeia explorada

5.7.4. Um programa de treino

Encontrar a expressão regular que permite verificar se uma cadeia corresponde efetivamente a um determinado padrão é, por vezes, um verdadeiro desafio. O programa seguinte permite praticar. Solicita um padrão e uma cadeia e indica se a cadeia corresponde ou não ao padrão.


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program4 {
        static void Main(string[] args) {
            // dados
            string modèle, chaine;
            Regex regex = null;
            MatchCollection résultats;
            // solicita-se ao utilizador os modelos e os exemplares a comparar com este
            while (true) {
                // é solicitado o modelo
                Console.Write("Tapez le modèle à tester ou rien pour arrêter :");
                modèle = Console.In.ReadLine();
                // terminado?
                if (modèle.Trim() == "")
                    break;
                // cria-se a expressão regular
                try {
                    regex = new Regex(modèle);
                } catch (Exception ex) {
                    Console.WriteLine("Erreur : " + ex.Message);
                    continue;
                }
                // solicita-se ao utilizador os exemplares a comparar com o modelo
                while (true) {
                    Console.Write("Tapez la chaîne à comparer au modèle [{0}] ou rien pour arrêter :", modèle);
                    chaine = Console.ReadLine();
                    // concluído?
                    if (chaine.Trim() == "")
                        break;
                    // faz-se a comparação
                    résultats = regex.Matches(chaine);
                    // foi bem-sucedida?
                    if (résultats.Count == 0) {
                        Console.WriteLine("Je n'ai pas trouvé de correspondances");
                        continue;
                    }//se
                    // são apresentados os elementos que correspondem ao modelo
                    for (int i = 0; i < résultats.Count; i++) {
                        Console.WriteLine("J'ai trouvé la correspondance [{0}] en position [{1}]", résultats[i].Value, résultats[i].Index);
                        // dos subelementos
                        if (résultats[i].Groups.Count != 1) {
                            for (int j = 1; j < résultats[i].Groups.Count; j++) {
                                Console.WriteLine("\tsous-élément [{0}] en position [{1}]", résultats[i].Groups[j].Value, résultats[i].Groups[j].Index);
                            }
                        }
                    }
                }
            }
        }
    }
}

Eis um exemplo de execução:

Tapez le modèle à tester ou rien pour arrêter :\d+
Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :123 456 789
J'ai trouvé la correspondance [123] en position [0]
J'ai trouvé la correspondance [456] en position [4]
J'ai trouvé la correspondance [789] en position [8]
Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :(\d{2}):(\d\d)
Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :14:15 abcd 17:18 xyzt
J'ai trouvé la correspondance [14:15] en position [0]
        sous-élément [14] en position [0]
        sous-élément [15] en position [3]
J'ai trouvé la correspondance [17:18] en position [11]
        sous-élément [17] en position [11]
        sous-élément [18] en position [14]
Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :^\s*\d+\s*$
Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :   1456
J'ai trouvé la correspondance [   1456] en position [0]
Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :^\s*(\d+)\s*$
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :1456
J'ai trouvé la correspondance [1456] en position [0]
        sous-élément [1456] en position [0]
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :abcd 1456
Je n'ai pas trouvé de correspondances
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :

5.7.5. O método Split

Já nos deparámos com este método na classe String:


public string[] Split(char[] separator)
la chaîne est vue comme une suite de champs séparés par les
caractères présents dans le tableau separator. Le résultat est
le tableau de ces champs

O método Split da classe Regex permite-nos definir o separador com base num modelo:


public string[] Split(string input)
La chaîne input est décomposée en champs, ceux-ci étant séparés
par un séparateur correspondant au modèle de l'objet Regex
courant.

Suponhamos, por exemplo, que tenhamos num ficheiro de texto linhas com o formato campo1, campo2, …, campo n. Os campos são separados por uma vírgula, mas esta pode ser precedida ou seguida de espaços. O método Split da classe string não é, portanto, adequado. O método RegEx oferece a solução. Se ligne for a linha lida, os campos poderão ser obtidos através de

string[] champs=new Regex(@"s*,\s*").Split(ligne);

, tal como mostra o exemplo seguinte:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program5 {
        static void Main(string[] args) {
            // uma linha
            string ligne = "abc  , def  , ghi";
            // um modelo
            Regex modèle = new Regex(@"\s*,\s*");
            // descomposição da linha em campos
            string[] champs = modèle.Split(ligne);
            // exibição
            for (int i = 0; i < champs.Length; i++) {
                Console.WriteLine("champs[{0}]=[{1}]", i, champs[i]);
            }
        }
    }
}

Resultados da execução:

1
2
3
champs[0]=[abc]
champs[1]=[def]
champs[2]=[ghi]

5.8. Aplicação de exemplo - V3

Retomamos a aplicação analisada nos parágrafos 3.6 (versão 1) e 4.10 (versão 2).

Na última versão analisada, o cálculo do imposto era efetuado na classe abstrata AbstractImpot:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {

        // as faixas de imposto necessárias para o cálculo do imposto
        // provêm de uma fonte externa

        protected TrancheImpot[] tranchesImpot;

        // cálculo do imposto
        public int calculer(bool marié, int nbEnfants, int salaire) {
            // cálculo do número de quotas
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
            // cálculo do rendimento tributável e do quociente familiar
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // cálculo do imposto
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
            // retorno do resultado
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//calcular
    }//classe

}

O método calculer da linha 38 utiliza a tabela tranchesImpot da linha 35, tabela que não é inicializada pela classe AbstractImpot. É por isso que é abstrata e tem de ser derivada para ser útil. Esta inicialização era efetuada pela classe derivada HardwiredImpot:


using System;

namespace Chap2 {
    class HardwiredImpot : AbstractImpot {

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

        public HardwiredImpot() {
                // criação da tabela de escalões de imposto
            tranchesImpot = new TrancheImpot[limites.Length];
                // preenchimento
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
    }// classe
}// espaço de nomes

No exemplo anterior, os dados necessários para o cálculo do imposto estavam «embutidos» no código da classe. A nova versão do exemplo coloca-os num ficheiro de texto:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

Uma vez que a execução deste ficheiro pode gerar exceções, criamos uma classe especial para as gerir:


using System;

namespace Chap3 {
    class FileImpotException : Exception {
        // códigos de erro
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
    
        // código de erro
        public CodeErreurs Code { get; set; }

        // construtores
        public FileImpotException() {
        }
        public FileImpotException(string message)
            : base(message) {
        }
        public FileImpotException(string message, Exception e)
            : base(message,e) {
        }
    }
}
  • linha 4: a classe FileImpotException deriva da classe Exception. Servirá para registar qualquer erro que ocorra durante a processamento do ficheiro de texto com os dados.
  • linha 7: uma enumeração que representa códigos de erro:
    • Acces: erro de acesso ao ficheiro de texto com os dados
    • Ligne: linha que não contém os três campos esperados
    • Champ1: o campo n.º 1 está errado
    • Champ2: o campo n.º 2 está incorreto
    • Champ3: o campo n.º 3 está incorreto

Alguns destes erros podem ocorrer em conjunto (Champ1, Champ2, Champ3). Por isso, a enumeração CodeErreurs foi anotada com o atributo [Flags], o que implica que os diferentes valores da enumeração devem ser potências de 2. Um erro nos campos 1 e 2 resultará, então, no código de erro Champ1 | Champ2.

  • linha 10: a propriedade automática Code armazenará o código do erro.
  • linha 15: um construtor que permite criar um objeto FileImpotException, passando-lhe como parâmetro uma mensagem de erro.
  • linha 18: um construtor que permite criar um objeto FileImpotException, passando-lhe como parâmetros uma mensagem de erro e a exceção que originou o erro.

A classe que inicializa o array tranchesImpot da classe AbstractImpot é agora a seguinte:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace Chap3 {
    class FileImpot : AbstractImpot {

        public FileImpot(string fileName) {
            // dados
            List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
            int numLigne = 1;
            // exceção
            FileImpotException fe = null;
            // leitura do conteúdo do ficheiro fileName, linha a linha
            Regex pattern = new Regex(@"s*:\s*");
            // inicialmente sem erros
            FileImpotException.CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(fileName)) {
                    while (!input.EndOfStream && code == 0) {
                        // linha atual
                        string ligne = input.ReadLine().Trim();
                        // as linhas vazias são ignoradas
                        if (ligne == "") continue;
                        // linha dividida em três campos separados por:
                        string[] champsLigne = pattern.Split(ligne);
                        // existem 3 campos?
                        if (champsLigne.Length != 3) {
                            code = FileImpotException.CodeErreurs.Ligne;
                        }
                        // conversões dos 3 campos
                        decimal limite = 0, coeffR = 0, coeffN = 0;
                        if (code == 0) {
                            if (!Decimal.TryParse(champsLigne[0], out limite)) code = FileImpotException.CodeErreurs.Champ1;
                            if (!Decimal.TryParse(champsLigne[1], out coeffR)) code |= FileImpotException.CodeErreurs.Champ2;
                            if (!Decimal.TryParse(champsLigne[2], out coeffN)) code |= FileImpotException.CodeErreurs.Champ3; ;
                        }
                        // erro?
                        if (code != 0) {
                            // regista-se o erro
                            fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
                        } else {
                            // memoriza-se a nova faixa de imposto
                            listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
                            // linha seguinte
                            numLigne++;
                        }
                    }
                }
                // transfere-se a lista listImpot para a tabela tranchesImpot
                if (code == 0) {
                    // transfere-se a lista listImpot para a tabela tranchesImpot
                    tranchesImpot = listTranchesImpot.ToArray();
                }
            } catch (Exception e) {
                // regista-se o erro
                fe= new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", fileName), e) { Code = FileImpotException.CodeErreurs.Acces };
            }
            // erro a comunicar?
            if (fe != null) throw fe;
        }
    }
}
  • linha 7: a classe FileImpot deriva da classe AbstractImpot, tal como acontecia na versão 2 com a classe HardwiredImpot.
  • linha 9: o construtor da classe FileImpot tem como função inicializar o campo trancheImpot da sua classe base AbstractImpot. Aceita como parâmetro o nome do ficheiro de texto que contém os dados.
  • linha 11: o campo tranchesImpot da classe base AbstractImpot é um tabulero que deve ser preenchido com os dados do ficheiro filename passado como parâmetro. A leitura de um ficheiro de texto é sequencial. Só se sabe o número de linhas depois de ter lido o ficheiro na íntegra. Por isso, não é possível dimensionar a matriz tranchesImpot. Os dados serão armazenados temporariamente na lista genérica listTranchesImpot.

Recorde-se que o tipo TrancheImpot é uma estrutura:


namespace Chap3 {
    // uma faixa de imposto
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}
  • linha 14: fe, do tipo FileImpotException, serve para encapsular um eventual erro de processamento do ficheiro de texto.
  • linha 16: a expressão regular do separador de campos numa linha champ1:champ2:champ3 do ficheiro de texto. Os campos são separados pelo carácter :, precedido e seguido por um número qualquer de espaços.
  • linha 18: o código do erro em caso de erro
  • linha 20: processamento do ficheiro de texto com um StreamReader
  • linha 21: o ciclo repete-se enquanto houver uma linha por ler e não tiver ocorrido nenhum erro
  • linha 27: a linha lida é dividida em campos através da expressão regular da linha 16
  • linhas 29-31: verifica-se se a linha tem efetivamente três campos — regista-se um eventual erro
  • linhas 33-38: conversão das três cadeias de caracteres em três números decimais — registam-se eventuais erros
  • linhas 40-43: se tiver ocorrido um erro, é criada uma exceção do tipo FileImpotException.
  • linhas 44-47: se não tiver havido erro, passa-se à leitura da linha seguinte do ficheiro de texto, após ter guardado os dados da linha atual.
  • linhas 52-55: à saída da boca while, os dados da lista genérica listTranchesImpot são copiados para a tabela tranchesImpot da classe base AbstractImpot. Recorde-se que esse era o objetivo do criador.
  • linhas 56-59: gestão de uma eventual exceção. Esta é encapsulada num objeto do tipo FileImpotException.
  • linha 61: se a exceção fe da linha 18 tiver sido inicializada, então é lançada.

O projeto C# completo é o seguinte:

  • em [1]: o projeto completo
  • em [2,3]: as propriedades do ficheiro [DataImpot.txt] [2]. A propriedade [Copy to Output Directory] [3] é definida como «always». Isto faz com que o ficheiro [DataImpot.txt] seja copiado para a pasta bin/Release (modo Release) ou bin/Debug (modo Debug) em cada execução. É aí que o executável o procura.
  • No [4]: faz-se o mesmo com o ficheiro [DataImpotInvalide.txt].

O conteúdo de [DataImpot.txt] é o seguinte:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

O conteúdo de [DataImpotInvalide.txt] é o seguinte:

a:b:c

O programa de teste [Program.cs] não sofreu alterações: trata-se do programa da versão 2, parágrafo 4.10, com a seguinte diferença:


using System;

namespace Chap3 {
    class Program {
        static void Main() {
...
             // criação de um objeto IImpot
            IImpot impot = null;
            try {
                // criação de um objeto IImpot
                impot = new FileImpot("DataImpot.txt");
            } catch (FileImpotException e) {
                // exibição de erro
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // interrupção do programa
                Environment.Exit(1);
            }

            // loop infinito
            while (true) {
...
            }//while
        }
    }
}
  • linha 8: objeto impot do tipo da interface IImpot
  • linha 11: instanciação do objeto impot com um objeto do tipo FileImpot. Esta operação pode gerar uma exceção que é tratada pelo bloco try/catch das linhas 9, 12 e 18.

Eis alguns exemplos de execução:

Com o ficheiro [DataImpot.txt]

1
2
3
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :

Com um ficheiro [xx] inexistente

L'erreur suivante s'est produite : [Code=Acces,Message=Erreur lors de la lecture du fichier xx, Exception d'origine : Could not find file 'C:\data\2007-2008\c#2008\poly\Chap3\10\bin\Release\xx'.]

Com o ficheiro [DataImpotInvalide.txt]

L'erreur suivante s'est produite : [Code=Champ1, Champ2, Champ3,Message=Ligne n° 1 incorrecte]