[Tutorial] Lêr de ficheiros. Análise e edição dos dados C# (OOP)

brunoss

Power Member
Boa noite. Eu tenho visto muitas pessoas com dúvidas do género que vou tentar explicar neste tópico. Com a contribuição de outros participantes do forum vou mantendo este post actualizado.

1º Apresentação de um problema geral no qual vocês têm tido dificuldades:
Leitura de um ficheiro para poder construir um array com informação que é para ser editada pelo utilizador. Depois de terem sido feitas as modificações guardar essas alterações no ficheiro.

Vou separar este problema em 4 fases:
1º - Leitura do ficheiro para construir o objecto
2º - Inserção de um elemento no array
3º - Procurar um elemento no array para o modificar
4º - Escrever modificações no ficheiro

Antes de continuar vou descrever um pouco as estruturas de dados mais abordadas neste forum. Se forem iniciantes ou não estiverem muito preocupados com a eficiência podem saltar para o 1º passo.
Um array é a estrutura de dados mais simples. Pode ser indexada para obter e modificar os seus valores. A inserção deverá ser sequencial senão torna-se lenta.
Um arraylist é um array só que quando fica cheio é feita uma expansão para dar a possibilidade de inserir novos elementos.

Escolha de uma estrutura de dados:
Como estamos a lêr de um ficheiro não sabemos quantos elementos vai ter o nosso array por isso deveriamos escolher um arraylist.
Mas na verdade a estrutura de dados mais eficiente, para inserção remoção e pesquisa é uma hashtable.
Se for preciso ordenar então serão outras que não vou aqui falar (por enquanto)

1º Leitura de um ficheiro

A primeira coisa a saber é como é que a informação está organizada no ficheiro.
Por exemplo:
Se a cada linha corresponder um objecto do nosso array então sabemos que temos que contruir um objecto para cada linha do ficheiro
Se os campos estiverem separados por ';' então sabemos que teremos tantos campos como os ';' que aparecerem na linha (mais um)

Agora só temos que saber qual é o tipo de dados de cada campo para meter numa variável do tipo correspondente.
Vou agora apresentar um código genérico para a criação de uma Stream (para lêr de um ficheiro) lendo linha a linha:

Código:
StreamReader sr = new StreamReader(new FileStream("ficheiro.txt", FileMode.Open));
List<Produto> produtos = new List<Produto>();
string linha;
while ((linha=sr.ReadLine()) != null) {[INDENT]string[] campos = linha.Split(';'); //assumindo que os campos são separados por ;[/INDENT]
}

Neste momento temos cada um dos campos no array de String campos e agora gostávamos de agrupar esses campos para depois inserirmos no arraylist.
Vi muitos utilizadores com este problema a sugerirem colocar a informação num array bidimensional, do que eu discordo completamente.

Uma das maneiras mais práticas e simples é criar uma class com cada um dos campos. Vou supor que o nosso ficheiro tem 3 campos por cada elemento. <nome>;<marca>;<preço>
Então criaria a seguinte class:

Código:
public class Produto {[INDENT]string nome;[/INDENT]
        string marca;
        double preço; //podia ser um float também

        public Produto(string nome, string marca, double preco) { //construtor com criação de um objecto completo
            this.nome = nome;
            this.marca = marca;
            this.preco = preco;
        }
}

2º Inserção de elementos no array
E agora temos uma abstracção daquilo que é cada elemento do nosso Array, sabemos que é um produto com aquelas propriedades que podemos alterar.
Vou então simular a criação de um objecto para inserir no arraylist continuando no while.

Código:
 // Dentro do while...
string n = campos[0];string m = campos[1];
double p = Convert.ToDouble(linha[2]);
Produto c = new Produto(n, m, p);
produtos.Add(c);

Quando o nosso ciclo terminar teremos o array com todos os Produtos que o ficheiro possuía. Agora o nosso problema é procurar elementos para que o utilizador o possa editar

3º Procurar um elemento
Este problema depende essencialmente de duas questões.
A primeira é saber se podem haver elementos repetidos na vossa estrutura de dados.
A segunda é saber por que atributo é que estão a procurar o vosso elemento.
No caso genérico assume-se que há elementos repetidos por isso é o que vou fazer. Vou procurar os meus elementos por Marca.
Como estamos a usar um arraylist que não foi feito por nós temos que perceber qual é maneira de encontrar um elemento numa estrutura de dados.

A maneira convencional de encontrar um elemento na estrutura de dados é percorrer os elementos e comparar um a um (com o método equals) e retornar o 1º elemento.
Então temos que implementar o nosso método equals em em Produto.

Código:
 //Na classe Produto
public override bool Equals(object obj)
{[INDENT]if (obj is Produto) { //verificar se o objecto que recebemos é um Produto[/INDENT]
[INDENT=2]Produto c = (Produto)obj;[/INDENT]
                return c.marca.Equals(this.marca); //se for ver se a marca são iguais
            }
            return false;
}

NOTA: O método equals serve para comparar dois elementos se por acaso quiséssemos comparar dois elementos pelos campos todos então teríamos que ter os campos todos no nosso método equals e a nossa procura por um dos campos de Produto já não funcionaria. Portanto esta solução fica assim para explicar apenas uma maneira simples de fazer o desejado.

Agora podemos procurar um elemento com o método IndexOf
Código:
 Produto procurar = new Produto(null, "Marca pretendida", 0); //mais um problema que temos é ter de criar um objecto produto para procurá-lo por Marca
int indice = produtos.IndexOf(procurar);
//Agora podemos fazer o que quisermos com o produto neste índice
if(indice >= 0) //o index of devolve um valor negativo (-1) caso não encontre elemento 
produtos[indice].Preco = 100.14;

Então mas isto levanta outro problema como é que procuraríamos o próximo elemento da mesma marca?
existe para isso outro IndexOf que procura um elemento a partir de determinado índice
Código:
produtos.IndexOf(procurar, indice+1); //se fosse apenas indice devolvia o mesmo objecto

E assim fica resolvida a parte de procurar um elemento e alterar os seus valores.

4º Escrever no ficheiro as modificações

O único problema aqui é termos que escrever o objecto no mesmo formato que o lemos no ficheiro. Então eu sugiro que façam o método ToString na vossa classe e depois basta percorrer o array e escrever cada elemento.

Código:
//Na classe Produto
public override string ToString()
{[INDENT]StringBuilder builder = new StringBuilder(); //o string builder serve para juntar várias strings/valores[/INDENT]
        builder.Append(nome); builder.Append(';');
        builder.Append(marca); builder.Append(';');
        builder.Append(preco);
        return builder.ToString();
}

//escrever os elementos no ficheiro
StreamWriter sw = new StreamWriter(new FileStream("ficheiro.txt", FileMode.Open));
foreach(Produto produto in produtos)[INDENT]sw.WriteLine(produtoto.ToString());[/INDENT]
sw.Flush(); //não se esquecam de fazer flush para a infromação ser escrita no ficheiro
sw.Close();
sr.Close(); //não se esquecam de fechar as streams

 
Última edição:
Desculpem a demora mas só hoje tive algum tempo para continuar o tutorial. Ficaram do post anterior explicar algumas coisas tais como o funcionamento e o uso de uma hashtable para a resolução deste exercício ou outro exercício genérico.

Vou separar a explicação em várias partes:
1º Funcionamento da HashTable (dictionary)
1.1 Funções de hash
1.2 Cenários de Utilização
1.3 Inconvenientes

1º Funcionamento da HashTable
Uma hashTable não é mais que um array de par (chaves,valor) que permite procuras eficientes se os elementos respeitarem alguns critérios que explicarei a seguir.
Para inserirmos um elemento numa hashtable temos que indicar qual é a chave desse elemento. E obtemos esse mesmo elemento pela chave.
Por exemplo:

Código:
Dictionary<string, Info> table = new Dictionary<string, Info>();
            Info nfo = new Info();
            table.Add(nfo.nome,nfo);
Info nfo1 = table[nfo.nome]; //devolve o nfo

1.1 Funções de hash
As HashTable só funcionam bem se tiverem boas funções de hash para as chaves.
A utilidade de uma função de hash é dispersar da maneira mais eficiente possível os elementos. Sendo que dois elementos iguais têm que ter o mesmo código de hash.
Mas dois elementos com o mesmo código de hash não têm que ser iguais. As strings implementam já uma boa função de hash.
Daí sempre que quisermos procurar um valor por uma class que nós criarmos temos sempre que definir o hashcode em concordância com o equals.

1.2 Cenários de Utilização
As tabelas de hash, a partir do momento que tenham uma boa função de hash, é a melhor estrutura para adicionar, remover e procurar elementos.
Devem ser utilizadas sempre se não houver nenhum inconveniente, ou se acharmos que podemos resolver o mesmo problema por exemplo tendo um array ordenado sem muitas percas de eficiência.

1.3 Inconvenientes
Não é possível ter duas chaves iguais numa tabela de hash, por isso se aquilo que por estamos a procura poder ocorrer mais que uma vez temos que arranjar nós uma estrutura para resolver este problema. E assim a procura já não pode ser feita por um único campo
Muitas vezes a chave é redundante com algum dos campos do valor que estamos à procura mas não é propriamente uma grande preocupação.
Dependentemente das implementações as hashTable podem ocupar mais espaço mas a diferença não será significativa. Deve-se dar uma valor no construtor para o número de elementos próximo daqueles que estamos à espera (por excesso) para não haverem expansões porque podem pesar, mas isto é algo que já acontecia com o arraylist
 
Última edição:
Back
Topo