Sauvegarder des chaînes longues dans un espace réduit

Jean-Luc Mellet(Alphomega)

Gestion de la sauvegarde des chaînes longues dans des fichiers typés.

- Titre : Sauvegarder des chaînes longues dans un espace réduit

- Auteur : Jean-Luc Mellet(Alphomega)

- Parution : 26 avril 2002

- Mise à jour : 25 décembre 2019

- Licence : Ce document est issu de https://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.

Sommaire

- Avant-propos

- I. Les solutions au problème

- I-A. La mauvaise

- I-B. La bonne (à mon avis)

- II. Créer son propre composant de sauvegarde

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…