Sauvegarder des chaînes longues dans un espace réduit
Date de publication :
26/04/2002
Date de mise a jour :
16/08/2004
Par
Jean-Luc Mellet(Alphomega) (http://alphomega.developpez.com/)
Gestion de la sauvegarde des chaines longues dans des fichiers typés.
Avant-propos
1. Les solutions au problème
1.1. La mauvaise
1.2. La bonne (à mon avis)
2. Créer son propre composant de sauvegarde
Ces derniers temps, une question est souvent revenue sur le forum Delphi à propos de la sauvegarde dans un fichier de record. Quand on utilise un "membre" de type string dans un record et que l'on veut s'en servir pour créer un fichier de record à des fins de sauvegarde, Delphi génère une erreur indiquant:
[Erreur] Unit1.pas(48): Le type 'TMonRec' nécessite une finalisation - non autorisé dans type fichier
Si on appuis sur F1 après sélection de cette erreur, Delphi nous explique:
Extrait de l'aide Delphi de Borland
Certains types sont traités de manière spéciale par le compilateur sur une base interne, c'est-à-dire qu'ils doivent être correctement terminés pour libérer toutes les ressources qu'ils peuvent détenir actuellement. Comme le compilateur ne peut pas déterminer quel type est actuellement stocké dans une section variant d'enregistrement au moment de l'exécution, il est impossible de garantir que ces types de données spéciaux sont correctement terminés.
program Produce;
type
Data = record
name : string;
end;
var
inFile : file of Data;
begin
end.
program Solve;
type
Data = record
name : array [1..25] of Char;
end;
var
inFile : file of Data;
begin
end.
Une solution simple, pour le cas de String, est de re-déclarer le type comme tableau de caractères. Pour les autres cas nécessitant une finalisation, il devient de plus en plus difficile de maintenir une structure de fichier binaire avec des fonctions Pascal standard, telles que 'file of'. Dans ces situations, il est probablement plus simple d'écrire des routines d'E/S de fichier spécialisées.
Fin de l'extrait de l'aide Delphi de Borland
Alors, quelles sont les solutions ?
1. Les solutions au problème
Comme il est dit dans l'aide, on a plusieurs possibilités. J'en ai détecté 1 bonne et 1 moins bonne.
Sans être vraiment mauvaise, la première a un inconvénient majeur. Elle utilise parfois inutilement de l'espace disque. Nous allons voir pourquoi. Cette solution consiste à déclarer le membre de la structure avec une longueur fixe. On a alors le choix de créer un ShortString (String[xx]), ou bien un tableau de char. Par exemple:
TMonRec = record
Nom: array[0..49] of char;
Age: integer;
end;
TMonRec = record
Nom: string[50];
Age: integer;
end;
Dans ce cas, plus de problème ! Delphi est d'accord.
L'ennui, c'est que tout le monde n'a pas un nom de 50 caractères. Imaginons une personne qui s'appelle M. Po. Pour stocker son nom, nous utiliserons 50 octets, donc 48 de trop. Quel gaspillage !!!
Dans le cas d'un nom, la structure string[50] est valable car un nom fait rarement plus de 50 caractères. Mais si l'on voulait enregistrer dans notre record des chaînes de caractères plus longues, nous serions limité à 255 caractères, car une chaîne de longueur définie est un ShortString, et donc avec cette limitation. Alors comment éviter ce gaspillage de place tout en sauvegardant ce qu'on veut dans un fichier ?
1.2. La bonne (à mon avis)
La bonne solution quand on veut sauvegarder des chaînes de longueurs différentes, passe par l'oubli des "file of " pour créer son propre système de sauvegarde. Nous allons voir comment sauvegarder une chaîne de caractères avec juste ce qu'il faut d'espace disque.
Si l'on reprend notre structure exemple,
TMonRec = record
Nom: string;
Age: integer;
end;
nous savons que Delphi ne nous permettra pas de créer un file of TMonRec. Il va donc falloir créer notre propre procédure d'I/O dans le fichier de sauvegarde.
procedure TForm1.SauveRec(Rec: TMonRec);
begin
Fic.Write(Rec.Age, sizeof(Rec.Age));
end;
procedure TForm1.LitRec(var Rec: TMonRec);
begin
Fic.Read(Rec.Age, sizeof(Rec.Age));
end;
Pour le problème de Rec.Nom, le premier réflexe du débutant sera de coder comme ceci:
Fic.Write(Rec.Nom, sizeof(Rec.Nom);
Hélas, le résultat sera désastreux. Pourquoi ? Tout simplement parce pour Delphi, une variable string ne représente qu'une adresse mémoire. Donc, cette façon de procéder va sauvegarder n'importe quoi dans une emplacement de 4 octets (la taille du pointer).
De la même façon:
Fic.Write(Rec.Nom, Length(Rec.Nom);
ne fonctionnera pas. Pourquoi ? Regardons les définitions de TFileStream.Write et TFileStream.Read:
function Write(const Buffer; Count: Longint): Longint; override;
function Read(var Buffer; Count: Longint): Longint; override;
Pour Write, le paramètre Buffer est un const. Nous pourrions donc lui assigner notre Rec.Nom puisque c'est un pointer, donc const lui aussi. Le problème, c'est que dans ce cas, nous allons sauver la valeur du pointer et pas la chaîne. Pour Read, au contraire, Buffer est un var. Mais notre "pointer" Rec.Nom est un pointer, donc const. Ca ne va pas non plus.
La solution est simple: Au lieu d'utiliser le pointer, nous utiliserons le "vrai" premier caractère de la chaîne.
procedure TForm1.SauveRec(Rec: TMonRec);
begin
Fic.Write(Rec.Nom[1], length(Rec.Nom);
end;
Là, tout va bien ! Nous disons à Delphi: "Ecris dans le fichier ce que tu trouves à partir Rec[1] (premier caractère "réel") sur une longueur de length(Rec.Nom), donc, tous les caractères, et arrètes toi là. Ainsi, quelle que soit la longueur de la chaîne, on n'utilise QUE la place nécessaire.
Le problème va se poser à la lecture du fichier. Comme Read attend une longueur de caractère à lire, il faut bien lui fournir cette longueur. La solution est toute simple. Il suffit de sauvegarder aussi la longueur de la chaîne. Ce qui nous donne:
procedure TForm1.SauveRec(Rec: TMonRec);
var
I: integer;
begin
I := length(Rec.Nom);
Fic.Write(I, sizeof(I));
Fic.Write(Rec.Nom[1], I);
end;
procedure TForm1.LitRec(var Rec: TMonRec);
var
I: integer;
begin
Fic.Read(I, sizeof(I));
SetLength(Rec.Nom, I);
FicRead(Rec.Nom[1], I);
Fic.Read(Rec.Age, sizeof(Rec.Age));
end;
Simple non ?
2. Créer son propre composant de sauvegarde
Pour créer un code réutilisable, vous pouvez faire comme moi, et créer un descendant de TFileStream avec des fonctions supplémentaires. Voici le code utilisé:
unit EnhFileStream;
interface
uses
Windows, Messages, SysUtils, Classes;
type
TEnhFileStream = class(TFileStream)
private
protected
public
procedure SauveChaine(Chaine: string);
function LitChaine: string;
published
end;
implementation
function TEnhFileStream.LitChaine: string;
var
I: integer;
begin
Read(I, sizeof(I));
result := '';
SetLength(result, I);
Read(result[1], I);
end;
procedure TEnhFileStream.SauveChaine(Chaine: string);
var
I: integer;
begin
I := length(Chaine);
Write(I, sizeof(I));
Write(Chaine[1], I);
end;
end.
De cette façon, vous n'avez plus à vous soucier de l'écriture. La sauvegarde sera par exemple:
procedure TForm1.SauveRec(Rec: TMonRec);
begin
Fic := TEnhFileStream.Create(FicName, fmCreate or fmOpenWrite);
Fic.SauveChaine(Rec.Nom);
Fic.Write(Rec.Age, sizeof(Rec.Age));
Fic.Free;
end;
Bien sur, ne vous empèche de rajouter votre procédure selon les types utilisés. L'ajoût de SauveMonRec(Rec: TMonRec) vous permettra un appel direct d'I/O dans votre code source. Vous ajouterez une facilité de maintenance supplémentaire.
procedure TEnhFileStream.SauveMonRec(Rec: TMonRec);
var
I: integer;
begin
I := length(Rec.Nom);
Write(I, sizeof(I));
Write(Rec.Nom[1], I);
Write(Rec.Age, sizeof(Rec.Age));
end;
et le tour est joué. Dans votre code source, il vous suffira d'un
Fic.SauveMonRec(MonRec);
pour sauver votre record dans un fichier.
A bientôt pour d'autres articles ....
Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur.
La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.
|