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. Essa ajuda é indispensável até mesmo para desenvolvedores C# experientes. O nível de qualidade de uma documentação (fácil acesso, organização compreensível, relevância das informações, etc.) pode determinar o sucesso ou o fracasso de um ambiente de desenvolvimento.

5.1. Como 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 no menu.
  • no [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 olhada nos títulos dos capítulos da ajuda do C#:

  • [1]: uma visão geral do C#
  • [2]: uma série de exemplos sobre alguns aspectos do C#
  • [3]: um curso de C# — poderia substituir com vantagem o presente documento…
  • [4]: para aprofundar os detalhes do C#
  • [5]: útil para desenvolvedores de C++ ou Java. Ajuda a evitar algumas armadilhas.
  • [6]: quando estiver procurando exemplos, pode começar por aqui.
  • [7]: o que você precisa saber para criar interfaces gráficas
  • [8]: para usar melhor o IDE Visual Studio Express
  • [9]: O SQL Server Express 2005 é um SGBD de alta qualidade distribuído gratuitamente. Vamos utilizá-lo neste curso.

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

  • [1]: selecione a documentação sobre o framework .NET
  • [2]: a ajuda está localizada no ramo .NET Framework SDK
  • [3]: o ramo .NET Framework Class Library apresenta todas as classes .NET de acordo com o namespace ao qual pertencem
  • [4]: o espaço de nomes System, que foi o mais 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 ser difícil saber onde procurar. Nesse caso, pode-se usar o índice da ajuda:

  • no [1], use a opção [Help/Index] se a janela de ajuda ainda não estiver aberta; caso contrário, use [2] em uma janela de ajuda já aberta.
  • em [3], especifique o domínio no qual a pesquisa deve ser realizada
  • em [4], especifique o que você está procurando, neste caso, uma classe
  • em [5], a resposta

Outra maneira de procurar ajuda é usar a função de pesquisa da ajuda:

  • em [1]; use a opção [Help/Search] se a janela de ajuda ainda não estiver aberta; caso contrário, use [2] em uma janela de ajuda já aberta.
  • em [3], especifique o que está sendo pesquisado
  • em [4], filtrar as áreas de pesquisa
  • em [5], a resposta na forma de diferentes tópicos onde o texto procurado foi encontrado.

5.2. As sequências de caracteres

5.2.1. A classe System.String

A classe System.String é idêntica ao tipo string simples. Ela possui diversas 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 observar o seguinte: quando um método retorna uma sequência de caracteres, essa sequência é diferente daquela à qual o método foi aplicado. Assim, S1.Trim() retorna a string S2, e S1 e S2 são duas strings diferentes.

Uma sequência 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 ser analisada
            string ligne = "un:deux::trois:";
            // 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 inserir os elementos de uma sequência de caracteres em um array. A definição do método Split utilizado aqui é a seguinte:


    public string[] Split(char[] separator);
separator
tabela de caracteres. Esses caracteres representam os caracteres usados para separar os campos da sequência de caracteres. Assim, se a sequência for “champ1, champ2, champ3"”, poderá-se usar 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 é 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 sequência de caracteres que servirá como separador de campos
résultat
uma sequência de caracteres formada pela concatenação dos elementos da matriz value, separados pela sequência separator.

5.2.2. A classe System.Text.StringBuilder

Anteriormente, mencionamos que os métodos da classe String aplicados a uma sequência de caracteres S1 retornavam outra sequência de caracteres S2. A classe System.Text.StringBuilder permite manipular S1 sem precisar criar uma sequência de caracteres S2. Isso melhora o desempenho, evitando a multiplicação de sequências de caracteres com vida útil muito limitada.

A classe admite diversos 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 sequência subjacente. Por padrão, capacité é igual a 16. O terceiro construtor acima permite especificar a capacidade dos blocos. O número de blocos de caracteres capacité necessários para armazenar uma sequência S é ajustado automaticamente pela classe StringBuilder. Existem construtores para definir o número máximo de caracteres em um objeto StringBuilder. Por padrão, essa capacidade máxima é 2 147 483 647.

Aqui está um exemplo que ilustra esse 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 realocação de um novo bloco.
  • linha 10: str.Append(String S) permite concatenar a string S, do tipo String, à string str, do tipo StringBuilder.
  • linha 14: criação de um objeto StringBuilder com 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

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

  • linhas 4-5: aumento da capacidade em 16 caracteres
  • linhas 8-9: aumento da capacidade em 32 caracteres, embora 16 fossem suficientes.

Aqui estão 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.

Aqui está 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 seus resultados:

XYZTstabCD

5.3. As matrizes

As tabelas derivam da classe Array:

A classe Array possui diversos métodos para ordenar um array, procurar um elemento em um array, redimensionar um array, etc. Apresentamos algumas propriedades e métodos dessa classe. Quase todos são sobrecarregados, c.a.d, de modo que existem em diferentes variantes. Todo array herda deles.

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 a seguir ilustra o uso 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 digitados 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);
            // classificaçã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);
        }

        // Inserçã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 inserçã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 inserção se a string estiver vazia
                if (réponse.Equals(""))
                    break;
                // verificação da entrada
                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 novo elemento
                    éléments[i - 1] = élément;
                }
            }//enquanto
        }

        // método genérico para exibir 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);
            }
        }

        // busca 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) {
                    // procurando o elemento na matriz
                    if (type == TypeRecherche.dichotomique)
                        // busca dicotômica
                        i = Array.BinarySearch(éléments, élément);
                    else
                        // busca 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 uma matriz éléments digitados pelo teclado. Como não é possível dimensionar a matriz antecipadamente (não se sabe seu tamanho final), é necessário redimensioná-la a cada novo elemento (linha 57). Um algoritmo mais eficiente teria sido alocar espaço para a tabela em grupos de N elementos. No entanto, uma tabela não foi projetada para ser redimensionada. Esse caso é melhor tratado com uma lista (ArrayList, List<T>).
  • linhas 75-113: o método Recherche permite pesquisar no array éléments um elemento digitado no teclado. O modo de pesquisa varia dependendo se o array está ordenado ou não. Para um array não ordenado, realiza-se uma pesquisa linear com o método IndexOf da linha 106. Para um array ordenado, realiza-se uma pesquisa dicotômica com o método BinarySearch da linha 103.
  • linha 18: a tabela éléments é ordenada. Aqui, utiliza-se uma variante de Sort que possui apenas um parâmetro: a tabela a ser ordenada. A relação de ordem utilizada para comparar os elementos da tabela é, então, a implícita desses elementos. Aqui, os elementos são numéricos. É utilizada a ordem natural dos números.

Os resultados exibidos na tela 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

Além da tabela, existem diversas classes para armazenar coleções de elementos. Há 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 array. 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 a seguinte aparência:

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 sua referência à lista ArrayList. Embora um ArrayList armazene referências a objetos, é possível armazenar números nele. Isso é feito por meio de um mecanismo chamado Boxing: o número é encapsulado em um objeto O do tipo Object, e é a referência O que é armazenada na lista. Esse é um mecanismo transparente para o desenvolvedor. Assim, pode-se escrever:

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

Isso produzirá o seguinte resultado:

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


            int i = (int)liste[0];

A operação Object -> int é chamada de Unboxing. Se uma lista for inteiramente composta por tipos int, declará-la como List<int> melhora o desempenho. De fato, os números do tipo int passam a ser armazenados na própria lista e não em tipos Object externos à lista. As operações de boxing/unboxing não ocorrem mais.

Para um objeto List<T> ou T que seja uma classe, a lista armazena, mais uma vez, as referências dos objetos do tipo T:

Aqui estão 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

Vamos retomar o exemplo abordado anteriormente com um objeto do tipo Array e tratá-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 modificações mais significativas:


using System;
using System.Collections.Generic;

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

        // método principal
        static void Main(string[] args) {
            // leitura dos elementos de uma lista digitados no 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);
            // classificaçã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);
        }

        // inserçã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 inserção dos elementos da lista
            while (!terminé) {
...
                // se não houver erro
                if (!erreur) {
                    // mais um elemento na lista
                    éléments.Add(élément);
                }
            }//while
        }

        // método genérico para exibir 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);
            }
        }

        // busca 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)
                        // busca dicotômica
                        i = éléments.BinarySearch(élément);
                    else
                        // busca 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 ser gravado
  • 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>. Portanto, o método Affiche é adequado tanto para exibir tabelas quanto listas.

Os resultados da execução do programa são os mesmos do 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 de 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, de modo que não pode haver duas chaves idênticas. Um dicionário desse tipo poderia se parecer com isto se os tipos TKey e TValue designassem classes:

O valor associado à chave C de um dicionário D é obtido pela notação D[C]. Esse valor é de leitura e gravação. 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<TKey,TValue> 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é]);
            }
            // a chave “paul” é excluída
            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 exibir 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: 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: exclusão de uma entrada do dicionário
  • linha 41: busca por uma chave no dicionário. Se ela não existir, o método TryGetValue atribuirá o valor 0 a value, pois value é do tipo numérico. Essa técnica só pode ser utilizada aqui porque sabemos que o valor 0 não está no 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 arquivos de texto

5.5.1. A classe StreamReader

A classe System.IO.StreamReader permite ler o conteúdo de um arquivo de texto. Na verdade, ela é capaz de processar fluxos que não são arquivos. Aqui estão algumas de 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.

Aqui está 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 arquivo 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: exibe o nome do diretório de execução
  • linhas 12, 27: um try/catch para lidar com uma possível exceção.
  • linha 15: a estrutura using flux=new StreamReader(...) é um recurso para evitar ter que fechar explicitamente o fluxo após seu uso. Esse fechamento é feito automaticamente assim que se sai do escopo do using.
  • linha 15: o arquivo lido se chama infos.txt. Como se trata de um nome relativo, ele será procurado no diretório de execução exibido pela linha 8. Se não estiver lá, uma exceção será lançada e tratada pelo try/catch.
  • linhas 16-20: o arquivo é lido linha por linha
  • linha 25: o arquivo é lido de uma só vez

O arquivo 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#:

Veremos que bin/Release é a pasta de execução quando o projeto é executado com 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, inserirmos o nome do arquivo 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 gravar em um arquivo de texto. Assim como a classe StreamReader, ela é capaz de processar fluxos que não são arquivos. Aqui estão algumas de 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 arquivo de texto
            try {
                // criação do arquivo 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 digitada não estiver vazia
                    while (ligne != "") {
                        // gravação da linha no arquivo de texto
                        fluxInfos.WriteLine(ligne);
                        // leitura de uma nova linha digitada no 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 precisarmos fechar explicitamente o fluxo por meio de uma operação Close. Esse fechamento é feito automaticamente ao sair da operação using.
  • Por que um try/catch nas linhas 11 e 27? Na linha 13, poderíamos especificar um nome de arquivo no formato /rep1/rep2/ .../fichier com um caminho /rep1/rep2/... que não existe, tornando assim impossível a criação de fichier. Nesse caso, seria lançada uma exceção. Existem outros casos possíveis de exceção (disco cheio, permissões 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 arquivo infos2.txt foi criado na pasta bin/Release do projeto:

 

5.6. Os arquivos binários

As classes System.IO.BinaryReader e System.IO.BinaryWriter servem para ler e gravar arquivos binários.

Consideremos o seguinte aplicativo:

// sintaxe pg texto bin logs
// lê-se um arquivo de texto (texto) e seu conteúdo é armazenado em um arquivo binário (bin
// o arquivo de texto contém linhas no formato nome : idade, que serão armazenadas em uma estrutura string, int
// (logs) é um arquivo de texto de logs

O arquivo 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 arquivo de texto (texto) e seu conteúdo é armazenado em um arquivo binário (bin)
// o arquivo de texto contém linhas no formato nome : idade, que serão armazenadas em uma estrutura string, int
// (logs) é um arquivo de texto de logs

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 arquivo de texto – gravação de arquivo binário
            try {
                // abertura do arquivo de texto para leitura
                input = new StreamReader(arguments[0]);
                // abertura do arquivo binário para gravação
                output = new BinaryWriter(new FileStream(arguments[1], FileMode.Create, FileAccess.Write));
                // abertura do arquivo de logs para gravação
                logs = new StreamWriter(arguments[2]);
                // análise do arquivo de texto
                while ((ligne = input.ReadLine()) != null) {
                    // mais uma linha
                    numLigne++;
                    // linha vazia?
                    if (ligne.Trim() == "") {
                        // ignorado
                        continue;
                    }
                    // uma linha com nome: idade
                    champs = ligne.Split(séparateurs);
                    // precisamos de 2 campos
                    if (champs.Length != 2) {
                        // registramos 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 == "") {
                        // registramos 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) {
                        // registra-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, os dados são gravados no arquivo binário
                    if (!erreur) {
                        output.Write(nom);
                        output.Write(age);
                    }
                    // próxima linha
                }//enquanto
            }catch(Exception e){
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // fechamento dos arquivos
                if(input!=null) input.Close();
                if(output!=null) output.Close();
                if(logs!=null) logs.Close();
            }
        }
    }
}

Vamos nos concentrar nas operações relacionadas à 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). Aqui, trata-se de um fluxo criado a partir de um arquivo (FileStream), cujos dados são:

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

A classe BinaryWriter possui diferentes métodos Write sobrecarregados para gravar os diversos tipos de dados simples

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

Os três argumentos do método Main são fornecidos ao projeto (por meio de suas propriedades) [1], e o arquivo de texto a ser processado é colocado na pasta bin/Release [2]:

Com o seguinte arquivo [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 arquivo binário [personnes1.bin] criado, bem como o arquivo de logs [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 arquivo binário [personnes1.bin] será fornecido pelo programa a seguir. Este também aceita três argumentos:

// sintaxe pg bin texto logs
// lê-se um arquivo binário e seu conteúdo é armazenado em um arquivo de texto (texto)
// o arquivo binário possui uma estrutura string, int
// o arquivo de texto contém linhas no formato nome : idade
// logs é um arquivo de texto de logs

Portanto, realizamos a operação inversa. Lemos um arquivo binário para criar um arquivo de texto. Se o arquivo de texto gerado for idêntico ao arquivo original, saberemos que a conversão texto → binário → texto ocorreu corretamente. O código é o seguinte:


using System;
using System.IO;

// sintaxe pg bin texto logs
// lê-se um arquivo binário bin e seu conteúdo é gravado em um arquivo de texto (texto)
// o arquivo binário tem uma estrutura string, int
// o arquivo de texto contém linhas no formato nome : idade
// logs é um arquivo de texto de logs

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 arquivo binário – gravação de arquivo de texto
            try {
                // abertura do arquivo binário para leitura
                input = new BinaryReader(new FileStream(arguments[0], FileMode.Open, FileAccess.Read));
                // abertura do arquivo de texto para gravação
                output = new StreamWriter(arguments[1]);
                // abertura do arquivo de logs para gravação
                logs = new StreamWriter(arguments[2]);
                // análise do arquivo binário
                fini = false;
                while (!fini) {
                    try {
                        // leitura do nome
                        nom = input.ReadString().Trim();
                        // leitura da idade
                        age = input.ReadInt32();
                        // gravação no arquivo de texto
                        output.WriteLine(nom + ":" + age);
                        // próxima pessoa
                        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 {
                // fechamento dos arquivos
                if (input != null)
                    input.Close();
                if (output != null)
                    output.Close();
                if (logs != null)
                    logs.Close();
            }
        }
    }
}

Vamos nos deter nas operações relacionadas à 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). Aqui, trata-se de um fluxo construído a partir de um arquivo (FileStream), cujos dados são:

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

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

  • linha 60: a operação de fechamento 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 o segundo aplicativo
  • em [2], os argumentos passados para Main
  • em [3], os arquivos gerados pela execução do aplicativo.

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

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

5.7. Expressões regulares

A classe System.Text.RegularExpressions.Regex permite o uso de expressões regulares. Elas permitem verificar o formato de uma sequência de caracteres. Assim, é possível verificar se uma sequência que representa uma data está, de fato, no formato dd/mm/aa. Para isso, utiliza-se um modelo e compara-se a sequência com esse modelo. Portanto, neste exemplo, j, m e a devem ser algarismos. O modelo de um formato de data válido é, portanto, “\d\d/\d\d/\d\d”, em que o símbolo \d representa um algarismo. Os símbolos que podem ser utilizados em um 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 “\(" corresponde a “(”.
^ 
Corresponde ao início da entrada.
$ 
Corresponde ao fim da entrada.
* 
Corresponde ao caractere anterior zero ou mais vezes. Assim, “zo*” corresponde a “z” ou a “zoo”.
+ 
Corresponde ao caractere anterior uma ou mais vezes. Assim, “zo+” corresponde a “zoo”, mas não a “z”.
? 
Corresponde ao caractere anterior zero ou uma vez. Por exemplo, “a?ve?” corresponde a “ve” em “lever”.
.
Corresponde a qualquer caractere único, exceto o caractere de nova linha.
(modèle) 
Procura o modèle e armazena a correspondência. A subcadeia correspondente pode ser extraída da coleção Matches obtida, usando o Item [0]...[n]. Para encontrar correspondências com caracteres entre parênteses ( ), use “\(" 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 caractere. 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 caractere. 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 pelo menos n e, no máximo, m vezes o caractere. 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 na 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 esteja na série especificada. Por exemplo, “[^m-z]” corresponde a qualquer caractere que não esteja entre “m” e “z”.
\b 
Corresponde a um limite que representa uma palavra; em outras palavras, à 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 caractere que representa um algarismo. É equivalente a [0-9].
\D 
Corresponde a um caractere que não representa um algarismo. É equivalente a [^0-9].
\f 
Corresponde a um caractere de quebra de página.
\n 
Corresponde a um caractere de nova linha.
\r 
Corresponde a um caractere de retorno de carro.
\s 
Corresponde a qualquer espaço em branco, incluindo espaço, tabulação, quebra 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 caractere de tabulação.
\v 
Corresponde a um caractere de tabulação vertical.
\w 
Corresponde a qualquer caractere que represente uma palavra e inclua um sublinhado. É equivalente a “[A-Za-z0-9_]”.
\W 
Corresponde a qualquer caractere que não represente uma palavra. É equivalente a “[^A-Za-z0-9_]”.
\num 
Corresponde a num, onde num é um número inteiro positivo. Refere-se às correspondências armazenadas. 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 caractere de tabulação. "\0011" equivale a "\001" e "1". Os valores de escape octais não devem exceder 256. Se isso ocorrer, apenas os dois primeiros dígitos serão considerados na expressão. Permite usar os códigos ASCII em expressões regulares.
\xn
Corresponde a n, onde n é um valor de escape hexadecimal. Os valores de escape hexadecimais devem conter obrigatoriamente dois dígitos. Por exemplo, "\x41" corresponde a "A". "\x041" equivale a "\x04" e "1". Permite usar os códigos ASCII em expressões regulares.

Um elemento em um modelo pode aparecer uma ou mais vezes. Vejamos alguns exemplos relacionados ao símbolo \d, que representa um dígito:

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

Imaginemos agora o modelo capaz de descrever o formato esperado para uma sequência 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 por espaços
\s*\d+\s*
um número inteiro que pode ser com sinal e precedido ou seguido por espaços
\s*[+|-]?\s*\d+\s*
um número real sem sinal que pode ser precedido ou seguido por espaços
\s*\d+(.\d*)?\s*
um número real que pode ser com sinal e precedido ou seguido por espaços
\s*[+|]?\s*\d+(.\d*)?\s*
uma sequência de caracteres contendo a palavra juste
\bjuste\b
  

É possível especificar onde procurar o padrão na sequência:

padrão
significado
^padrão
o padrão inicia a sequência
padrão$
o modelo encerra a sequência
^padrão$
o modelo inicia e encerra a sequência
padrão
o padrão é procurado em toda a sequência, começando pelo início dela.
sequência pesquisada
padrão
uma sequência que termina com um ponto de exclamação
!$
uma sequência que termina com um ponto
\.$
uma sequência que começa com //
^//
uma sequência que contém apenas uma palavra, eventualmente seguida ou precedida por espaços
^\s*\w+\s*$
uma sequência contendo duas palavras, eventualmente seguidas ou precedidas por espaços
^\s*\w+\s*\w+\s*$
uma sequência 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 sequência corresponde a um padrão específico, como também é possível recuperar nessa sequência os elementos correspondentes aos subconjuntos do padrão que foram colocados entre parênteses. Assim, se analisarmos uma sequência 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).

5.7.1. Verificar se uma sequência corresponde a um determinado modelo

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

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

Depois que a expressão regular modelo for criada, é possível compará-la com sequências de caracteres usando 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

Aqui está 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 modelo
            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 em uma string

O método Matches permite recuperar os elementos de uma string 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 representa o número de elementos da coleção. Se résultats for um objeto MatchCollection, resultados[i] é o elemento i dessa coleção e é do tipo Match. A classe Match possui diversas 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 sequência explorada

Vejamos o exemplo a seguir:


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 números
  • linha 10: a sequência na qual se procura esse padrão
  • linha 11: recuperam-se todos os elementos de exemplaire3 que atendem ao padrão modèle2
  • linhas 14-16: exibimos esses elementos

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

Subconjuntos de um modelo podem ser “recuperados”. Assim, não só é possível verificar se uma sequência corresponde a um modelo específico, como também recuperar nessa sequência os elementos correspondentes aos subconjuntos do modelo que foram colocados entre parênteses. Assim, se analisarmos uma sequência de caracteres contendo 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 exibidos 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 está nas linhas 12 a 19:

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

Se résultat for do tipo Match, résultats.Groups é do tipo GroupCollection e résultats.Groups[i] é do tipo Group. A classe Group possui 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 sequência analisada

5.7.4. Um programa de treino

Encontrar a expressão regular que permite verificar se uma sequência corresponde de fato a um determinado padrão pode, às vezes, ser um verdadeiro desafio. O programa a seguir permite que você pratique. Ele solicita um padrão e uma sequência e indica se a sequência 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 usuário os modelos e os exemplares a serem comparados com este
            while (true) {
                // solicita-se o modelo
                Console.Write("Tapez le modèle à tester ou rien pour arrêter :");
                modèle = Console.In.ReadLine();
                // concluído?
                if (modèle.Trim() == "")
                    break;
                // criamos a expressão regular
                try {
                    regex = new Regex(modèle);
                } catch (Exception ex) {
                    Console.WriteLine("Erreur : " + ex.Message);
                    continue;
                }
                // solicita-se ao usuário os exemplares a serem comparados 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);
                    // sucesso?
                    if (résultats.Count == 0) {
                        Console.WriteLine("Je n'ai pas trouvé de correspondances");
                        continue;
                    }//se
                    // são exibidos 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);
                            }
                        }
                    }
                }
            }
        }
    }
}

Veja 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á vimos esse 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 nos permite definir o separador com base em um 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 em um arquivo de texto linhas do tipo campo1, campo2, ..., campo n. Os campos são separados por uma vírgula, mas esta pode ser precedida ou seguida por espaços. O método Split da classe string não é adequado nesse caso. O método RegEx oferece a solução. Se ligne for a linha lida, os campos poderão ser obtidos por

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

conforme mostra o exemplo a seguir:


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*");
            // decomposiçã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. Aplicativo de exemplo — V3

Retomamos a aplicação estudada 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 feito 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 cotas
            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 ela é abstrata e precisa ser derivada para ser útil. Essa inicialização era feita 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 faixas 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 acima, os dados necessários para o cálculo do imposto estavam embutidos no código da classe. A nova versão do exemplo os coloca em um arquivo 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

Como a execução deste arquivo pode gerar exceções, criamos uma classe especial para gerenciá-las:


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. Ela servirá para registrar qualquer erro que ocorra durante a processamento do arquivo de texto com os dados.
  • linha 7: uma enumeração que representa códigos de erro:
    • Acces: erro de acesso ao arquivo de texto de dados
    • Ligne: linha sem os três campos esperados
    • Champ1: o campo nº 1 está incorreto
    • Champ2: o campo nº 2 está incorreto
    • Champ3: o campo nº 3 está incorreto

Alguns desses erros podem ocorrer em combinação (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 a matriz 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 arquivo fileName, linha por 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();
                        // ignora-se as linhas vazias
                        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) {
                            // registramos o erro
                            fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
                        } else {
                            // armazenamos a nova faixa de imposto
                            listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
                            // linha seguinte
                            numLigne++;
                        }
                    }
                }
                // a lista listImpot é transferida para a tabela tranchesImpot
                if (code == 0) {
                    // a lista listImpot é transferida para a tabela tranchesImpot
                    tranchesImpot = listTranchesImpot.ToArray();
                }
            } catch (Exception e) {
                // observa-se o erro
                fe= new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", fileName), e) { Code = FileImpotException.CodeErreurs.Acces };
            }
            // erro a ser relatado?
            if (fe != null) throw fe;
        }
    }
}
  • linha 7: a classe FileImpot deriva da classe AbstractImpot, assim como a classe HardwiredImpot fazia na versão 2.
  • linha 9: o construtor da classe FileImpot tem a função de inicializar o campo trancheImpot de sua classe base AbstractImpot. Ele aceita como parâmetro o nome do arquivo de texto que contém os dados.
  • linha 11: o campo tranchesImpot da classe base AbstractImpot é uma matriz que deve ser preenchida com os dados do arquivo filename passado como parâmetro. A leitura de um arquivo de texto é sequencial. Só se sabe o número de linhas depois de ler o arquivo inteiro. Portanto, não é possível dimensionar a matriz tranchesImpot. Os dados serão armazenados temporariamente na lista genérica listTranchesImpot.

Lembramos 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 possível erro de processamento do arquivo de texto.
  • linha 16: a expressão regular do separador de campos em uma linha champ1:champ2:champ3 do arquivo de texto. Os campos são separados pelo caractere :, 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 arquivo de texto com um StreamReader
  • linha 21: o ciclo é repetido enquanto houver uma linha para ler e não tiver ocorrido nenhum erro
  • linha 27: a linha lida é dividida em campos por meio da expressão regular da linha 16
  • linhas 29-31: verifica-se se a linha possui, de fato, três campos — registra-se um eventual erro
  • linhas 33-38: conversão das três cadeias de caracteres em três números decimais — registra-se eventuais erros
  • linhas 40-43: se houver erro, é gerada uma exceção do tipo FileImpotException.
  • linhas 44-47: se não houver erro, passa-se para a leitura da próxima linha do arquivo de texto após salvar os dados da linha atual.
  • linhas 52-55: na saída da boca while, os dados da lista genérica listTranchesImpot são copiados para a tabela tranchesImpot da classe base AbstractImpot. Vale lembrar que esse era o objetivo do criador.
  • linhas 56-59: tratamento de uma possível exceção. Esta é encapsulada em um objeto do tipo FileImpotException.
  • linha 61: se a exceção fe da linha 18 tiver sido inicializada, ela é lançada.

O projeto C# completo é o seguinte:

  • em [1]: o projeto completo
  • em [2,3]: as propriedades do arquivo [DataImpot.txt] [2]. A propriedade [Copy to Output Directory] [3] é definida como “always”. Isso faz com que o arquivo [DataImpot.txt] seja copiado para a pasta bin/Release (modo Release) ou bin/Debug (modo Debug) a cada execução. É lá que ele é procurado pelo executável.
  • No [4]: faz-se o mesmo com o arquivo [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. Isso pode gerar uma exceção que é tratada pelo bloco try/catch nas linhas 9, 12 e 18.

Veja alguns exemplos de execução:

Com o arquivo [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 arquivo [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 arquivo [DataImpotInvalide.txt]

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