Avant-propos

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:

 
Sélectionnez

[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.

 
Sélectionnez

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 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.

1.1. 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:

 
Sélectionnez

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é à 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,

 
Sélectionnez

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.

 
Sélectionnez

// 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:

 
Sélectionnez
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:

 
Sélectionnez
Fic.Write(Rec.Nom, Length(Rec.Nom);

ne fonctionnera pas. Pourquoi ? Regardons les définitions de TFileStream.Write et TFileStream.Read:

 
Sélectionnez

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.

 
Sélectionnez

// 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: "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:

 
Sélectionnez

// 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 ?

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é:

 
Sélectionnez

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:

 
Sélectionnez

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.

 
Sélectionnez

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

 
Sélectionnez

Fic.SauveMonRec(MonRec);

pour sauver votre record dans un fichier.

A bientôt pour d'autres articles ....