Ne vous prenez pas les pieds dans les strings


Je rédige cet article un peu tardivement eu égard à l'introduction des chaînes Unicode dans Delphi, mais comme je me suis moi même fait avoir, je pense qu'il n'est pas trop tard pour faire le point.

Avant c'était simple


Avant Delphi 2009, et depuis Delphi 2, les choses étaient relativement simples. Un string était un type dynamique qui pouvait être nul - une chaîne vide - ou non nul. Dans ce cas sa valeur était un pointeur vers le premier caractère de la chaîne. Les caractères de l'époque étaient toujours codés sur 8 bits, pas d'ambiguïté possible. Le type string avait aussi quatre autres caractéristiques remarquables :

1) à l'offset -4 on trouvait la longueur de la chaîne. En effet, contrairement aux PChar du C, le string Delphi peut contenir n'importe quel caractère, y compris le caractère #0. La longueur de la chaîne est donc explicitement stockée à l'offset -4 du pointeur (soit avant le premier caractère)

2) à l'offset -8 on trouve un compteur de références. Cela permet à deux variables string contenant la même valeur de pointer sur la même zone mémoire avec un compteur de référence à 2. Cela permet aussi à Delphi de gérer automatiquement la libération de la chaîne quand son compteur de référence tombe à 0. Ce compteur est par ailleurs géré de façon totalement transparente et automatique par Delphi.

3) Les strings sont dites COW (Copy On Write), c'est à dire que dès que l'on va modifier ne serait-ce qu'un caractère d'une chaîne, Delphi va s'assurer que son compteur de référence vaut 1, sinon il crée une copie de la chaîne avant de la modifier; la chaîne original n'est donc pas altérée, son compteur de référence est simplement réduit de 1.

4) un string contient toujours un caractère supplémentaire #0 en fin de chaîne, cela permet de le rendre immédiatement compatible avec un PChar qui utilise ce caractère comme indicateur de fin de chaîne.

Avec l'Unicode ça se complique


Dire que l'unicode est compliqué serait un raccourci un peu rapide. En fait tout ce que je viens de dire pour les strings d'avant Delphi 2009 est vrai pour les strings Unicode, la seule grosse différence c'est que la taille d'un caractère est passée sur 16bits. De fait, les strings à partir de Delphi 2009 possèdent deux nouvelles informations:

1) à l'offset -10 on trouve la taille d'un caractère sur 16bits, soit 1 ou 2.

2) à l'offset -12 on trouve la page de code associée aux chaînes Ansi sur 16bits également, pour une chaîne Unicode sa valeur est 1200.

Ok, donc depuis 2009 une chaîne peut contenir des caractères sur 8 ou 16bits, et être associée à une page de code...et alors ? en quoi cela est-il plus compliqué ? Quand vous manipuler du "texte" dans Delphi, cela n'a presque pas d'importance, Delphi se charge de tout. Mais dès que vous communiquez avec l'extérieur, que ce soit un fichier, un protocole réseau ou même une simple DLL, vous devez savoir quel format doit avoir votre chaîne de caractères: Unicode, Ansi ou UTF8 pour les trois cas les plus courants.

Le type string correspond donc à une chaîne Unicode dont les caractères sont sur 16bits, tout comme les PChar de l'API Windows telle que déclarée dans Delphi. En effet, en passant à l'Unicode, toute l'unité Winapi.Windows a basculé des fonctions "A" de l'API Windows aux fonctions "W". Ansi MessageBox est un alias de MessageBoxW alors qu'avant c'était un alias de MessageBoxA.

Et si je dois utiliser une API qui réclame un texte sur 8bits, comment faire ? C'est en fait très simple, il suffit d'affecter la valeur de la chaîne Unicode à une chaîne Ansi:

var
  us: string;
  sa: AnsiString;
begin
  us := 'Hello World';
  sa := us; // conversion automatique de Unicode à Ansi - avec avertissement
end;

Le seul petit hic, c'est que la ligne vous générera un avertissement: W1058 Transtypage de chaîne implicite avec perte de données potentielles de 'string' en 'AnsiString'.

Je me bas contre les avertissements, si vous laissez votre source produire des dizaines d'avertissements "sans importance" vous finissez par ne plus voir ceux qui le sont. Je m'efforce donc d'avoir toujours un source qui compile sans aucun avertissement...j'ai déjà travaillé sur des projets qui produisaient des milliers d'avertissements (oui des milliers !) il devient alors tout simplement impossible de remarquer un nouvel avertissement important qui pourrait mettre en valeur un bug potentiel dans votre code.

On peut cependant très facilement supprimer l'avertissement de conversion en déclarant celle-ci explicitement :

var
  us: string;
  sa: AnsiString;
begin
  us := 'Hello World';
  sa := AnsiString(us); // conversion explicite de Unicode à Ansi - sans avertissement
end;

le code produit est exactement le même, il peut en effet y avoir une perte de données car l'Unicode peut représenter une très large gamme de caractères alors que l'Ansi est limité à 256 d'entre eux. Sur notre exemple, il n'y aura aucune perte, mais si je peux mélanger dans une même chaîne Unicode du français, du grecque et du japonais, c'est tout simplement impossible dans une chaîne Ansi qui possède une seule page de code associée à un seul groupe de pays qui ne peu pas inclure à la fois la France et le Japon.

Il existe cependant un format 8bits qui permet de s'en sortir, c'est l'UTF8, et ça tombe bien Delphi possède un type UTF8string - ce n'est autre qu'une AnsiString dont la page de code est fixée à 65001.

var
  us: string;
  u8: UTF8String;
begin
  us := 'Hello World';
  u8 := us; // conversion automatique de Unicode à UTF8 - avec avertissement
end;

l'avertissement n'est plus le même : W1057 Transtypage de chaîne implicite de 'string' à 'UTF8String', car il n'y a aucune perte potentielle d'information, et là aussi on peut supprimer l'avertissement en utilisant une conversion explicite.

var
  us: string;
  u8: UTF8String;
begin
  us := 'Hello World';
  u8 := UTF8String(us); // conversion explicite de Unicode à UTF8 - sans avertissement
end;

Et c'est en suivant ce même principe que je suis tombé dans un piège ! Prenez l'exemple ci-dessous qui effectue la conversion d'une chaîne ANSI 8Bits en UTF8 :

var
  sa: AnsiString;
  u8: UTF8String;
begin
  sa := 'Hello World';
  u8 := sa; 
end;

On obtient là encore des avertissements et on serait tenté d'écrire

var
  sa: AnsiString;
  u8: UTF8String;
begin
  sa := 'Hello World';
  u8 := UTF8String(sa); 
end;

Ceci supprime en effet tout avertissement, mais le résultat n'est pas celui attendu ! Au lieu de faire une conversion Ansi vers UTF8 ce code force la page de code de "sa" en UTF8, on se retrouve donc avec une variable "u8" contenant une chaîne Ansi et non UTF8 car aucune conversion n'est effectuée ! On peut s'en assurer en affichant la page de code de "u8".

var
  sa: AnsiString;
  u8: UTF8String;
begin
  sa := 'Hello World';
  u8 := UTF8String(sa); 
  WriteLn(StringCodePage(u8)); // 1252 et non 65001 pour UTF8 !
end;

En fait, si on supprime le transtypage explicite, on constate qu'il y a deux avertissements :
W1057 Transtypage de chaîne implicite de 'AnsiString' en 'string'
W1057 Transtypage de chaîne implicite de 'string' en 'UTF8String'
Il y a deux conversions implicites, d'abord en Unicode et ensuite en UTF8; dès lors, la conversion explicite doit être la suivante:

var
  sa: AnsiString;
  u8: UTF8String;
begin
  sa := 'Hello World';
  u8 := UTF8String(string(sa)); 
end;

vous obtiendrez le même résultat en faisant appel à la fonction de conversion UTF8Encode

var
  sa: AnsiString;
  u8: UTF8String;
begin
  sa := 'Hello World';
  u8 := UTF8Encode(sa); 
end;

plus précisément, la fonction s'assure que la chaîne en paramètre n'est pas déjà en page de code 65001 (UTF8) et si tel n'est pas le cas, elle effectue la double conversion Unicode/UTF8.

Conclusion


Voilà un petit article rapide sur les strings, les avertissements et les fausses bonnes idées pour les supprimer qui j'espère vous aura éclairé sur le sujet :)
Date de dernière modification : 06/08/2017