Avant-propos▲
Ces derniers temps, une question est souvent revenue sur le forum Delphi à propos de la sauvegarde dans un fichier de records. 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 records à 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 appuie 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
.
{String est un des types de données nécessitant une finalisation, et comme tel,}
{ils ne peuvent pas être stockés dans un type File.}
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 redé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 ?
I. Les solutions au problème▲
Comme il est dit dans l'aide, on a plusieurs possibilités. J'en ai détecté une bonne et une moins bonne.
I-A. La mauvaise▲
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
;
// ou bien
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és à 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 ?
I-B. 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.
// Fic est un TFileStream créé plus haut dans le code
procedure
TForm1.SauveRec(Rec: TMonRec);
begin
// Sauvegarde du membre Nom : voir plus bas
Fic.Write
(Rec.Age, sizeof(Rec.Age));
end
;
procedure
TForm1.LitRec(var
Rec: TMonRec);
begin
//Lecture du membre Nom : voir plus bas
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 un 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. Ça 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.
// Fic est un TFileStream créé plus haut dans le code
procedure
TForm1.SauveRec(Rec: TMonRec);
begin
Fic.Write
(Rec.Nom[1
], length(Rec.Nom);
//...
end
;
Là, tout va bien ! Nous disons à Delphi: « Écris 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ête-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 :
// Fic est un TFileStream créé plus haut dans le code
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); // allocation suffisante de place
FicRead(Rec.Nom[1
], I);
Fic.Read
(Rec.Age, sizeof(Rec.Age));
end
;
Simple non ?
II. 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
{ Déclarations privées }
protected
{ Déclarations protégées }
public
procedure
SauveChaine(Chaine: string
);
function
LitChaine: string
;
published
{ Déclarations publiées }
end
;
implementation
{ TEnhFileStream }
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 sûr, rien ne vous empêche de rajouter votre procédure selon les types utilisés. L'ajout 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.
À bientôt pour d'autres articles…