Traduction

Ceci est la traduction la plus fidèle possible de l'article de Jon Skeet, Strings in C# and .NETStrings in C# and .NET.

II. Qu'est-ce qu'un string ?

Un string est essentiellement une séquence de caractères. Chaque caractère est un caractère Unicode dans la plage U+0000 à U+FFFF (nous aborderons ce sujet plus tard). Le type string (j'utiliserai la forme raccourcie de C# plutôt que System.String à chaque fois) possède les caractéristiques décrites aux paragraphes suivants.

C'est un type référence

C'est une idée fausse très répandue que le string est un type valeur. C'est parce que son immuabilité (voir le point suivant) fonctionne un peu comme un type valeur. Il agit en fait comme un type référence normal. Vous pouvez voir mes articles sur le passage de paramètre et la mémoire pour plus de détails sur les différences entre les types valeur et référence.

Il est immuable

Vous ne pouvez jamais réellement changer le contenu d'un string, du moins avec du code sécurisé qui n'utilise pas la réflexion. Pour cette raison, vous finissez souvent par modifier la valeur d'une variable de type string. Par exemple, le code s = s.Replace ("foo", "bar"); ne change pas le contenu du string auquel s faisait référence à l'origine - cela affecte la valeur de s à une nouvelle chaîne de caractères qui est une copie de l'ancien string, mais avec "foo" remplacé par "bar".

Il peut contenir des valeurs NULL

Les programmeurs C sont habitués à ce que les strings soient des séquences de caractères se terminant par '\0', le caractère nul ou null. (Je vais utiliser "null" car c'est ce que le tableau de code Unicode indique en détail, ne le confondez avec le mot-clé null en C# - char est un type valeur et ne peut donc pas être une référence null !). En .NET, les chaînes peuvent contenir des caractères null sans aucun problème en ce qui concerne les méthodes de string. Cependant, d'autres classes (par exemple un grand nombre de classes Windows Forms) peuvent très bien penser que le string se termine au premier caractère null - si votre chaîne apparaît curieusement tronquée, cela pourrait être le problème.

Il surcharge l'opérateur ==

Lorsque l'opérateur == est utilisé pour comparer deux chaînes de caractères, la méthode Equals est appelée, ce qui vérifie l'égalité des contenus des chaînes de caractères plutôt que les références en elles-mêmes. Par exemple, "hello".Substring(0, 4)=="hell" est vrai, même si les références des deux côtés de l'opérateur sont différentes (elles se réfèrent à deux objets strings différents, qui contiennent tous les deux la même séquence de caractères). Notez que la surcharge d'opérateur ne fonctionne ici que si les deux côtés de l'opérateur sont des strings lors de la compilation - les opérateurs ne sont pas appliqués de manière polymorphe. Si chaque côté de l'opérateur est de type objet, dans la mesure où le compilateur est concerné, l'opérateur de base == sera appliqué et la simple égalité de référence sera testée.

III. Pool interne

Le framework .NET a le concept du "pool interne". Il s'agit juste d'un ensemble de strings, mais il s'assure qu'à chaque fois que vous faites référence à la même chaîne littérale, vous obtenez une référence à la même chaîne. Ceci est probablement dépendant du langage, mais c'est certainement vrai en C# et VB.NET, et je serais très surpris de voir un langage pour lequel ce ne serait pas le cas, l'IL rend cela très facile à faire (probablement plus facile que de ne pas réussir à interner des littéraux). Les littéraux sont automatiquement internés, mais on peut également les interner manuellement via la méthode Intern, et vérifier si oui ou non il y a déjà une chaîne internée avec la même séquence de caractères dans le pool en utilisant la méthode IsInterned. Cette méthode renvoie une chaîne de caractères plutôt qu'un boolean - si une chaîne de caractères équivalente est dans le pool, une référence à cette chaîne est renvoyée. Sinon, la valeur null est renvoyée. De même, la méthode Intern renvoie une référence à un string interné - soit le string que vous avez passé s'il était déjà dans le pool, ou un nouveau string interné créé, ou un string équivalent qui était déjà dans le pool.

IV. Littéraux

Les littéraux correspondent à la façon dont vous codez vos strings dans vos programmes écrits en C#. Il existe deux types de littéraux en C#  : les littéraux de chaîne normaux et les littéraux de chaîne verbatim. Les littéraux de chaîne normaux sont semblables à ceux que l'on retrouve dans d'autres langages tels que Java et C - ils commencent et se terminent par " et divers caractères (en particulier, " lui-même, \, le retour chariot (CR) et le saut de ligne (LF)) ont besoin d'être "échappés" afin d'être représentés dans la chaîne de caractères. Les littéraux de chaîne verbatim permettent à peu près tout en leur sein et se terminent au premier " qui n'est pas doublé. Même les retours chariot et les sauts de ligne peuvent apparaître dans le littéral ! Pour obtenir un " au sein de la même chaîne, vous aurez besoin d'écrire "". Les littéraux de chaîne verbatim se distinguent par la présence d'un @ devant les guillemets d'ouverture. Voici quelques exemples des deux types de littéraux :

Littéral de chaîne normal Littéral de chaîne verbatim Résultat
"Hello" @"Hello" Hello
"Backslash: \\" @"Backslash: \" Backslash: \
"Quote: \"" @"Quote: """ Quote: "
"CRLF:\r\nPost CRLF" @"CRLF:
Post CRLF"
CRLF:
Post CRLF

Notez que la différence n'a de sens que pour le compilateur. Une fois que le string est dans le code compilé, il n'y a pas de différence entre les littéraux de chaîne normaux et les littéraux de chaîne verbatim.

L'ensemble des séquences d'échappement est :

  • \' - simple quote, requis pour les caractères littéraux ;
  • \" - double-quote, requis pour les strings littéraux ;
  • \\ - antislash ;
  • \0 - caractère 0 Unicode ;
  • \a - alerte (caractère 7) ;
  • \b - backspace (caractère 8) ;
  • \f - saut de page (caractère 12) ;
  • \n - nouvelle ligne (caractère 10) ;
  • \r - chariot retour (caractère 13) ;
  • \t - tabulation horizontale (caractère 9) ;
  • \v - quote verticale (caractère 11) ;
  • \uxxxx - séquence d'échappement Unicode pour les caractères ayant une valeur hexa xxxx ;
  • \xn[n][n][n] - séquence d'échappement Unicode pour les caractères ayant une valeur hexa nnnn (la longueur de la variable est une version de \uxxxx) ;
  • \Uxxxxxxxx - séquence d'échappement Unicode pour les caractères ayant une valeur hexa xxxxxxxx (pour la génération de substituts).

Parmi ceux-ci, \a, \f, \v, \x et \U sont rarement utilisés, d'après ma propre expérience.

V. Les strings et le débogueur

De nombreuses personnes rencontrent des problèmes en inspectant des strings dans le débogueur, avec VS.NET 2002 et VS.NET 2003. Ironiquement, les problèmes sont souvent générés par le débogueur qui essaye d'être utile et soit affiche le string comme un littéral de chaîne normale échappé d'un anti-slash, soit l'affiche en tant que littéral de chaîne verbatim précédé d'un @. Cela conduit à de nombreuses questions demandant comment le @ peut être enlevé, en dépit du fait que ce n'est pas vraiment là sa première place - c'est seulement la façon dont le débogueur le montre. En outre, certaines versions de VS.NET arrêteront d'afficher le contenu de la chaîne de caractères au premier caractère null, évalueront sa propriété Length de manière incorrecte et calculeront la valeur elles-mêmes au lieu de faire appel au code managé. Encore une fois, ces versions considèrent que le string s'arrête au premier caractère null.

Compte tenu de la confusion que cela a causée, je crois qu'il est préférable d'examiner les strings d'une façon différente lorsqu'on est en mode debug, du moins si vous avez l'impression que quelque chose de bizarre se produit. Je vous suggère d'utiliser une méthode comme celle indiquée ci-dessous, qui va afficher le contenu du string dans la console d'une manière sûre. Selon le type d'application que vous développez, vous voudrez peut-être écrire cette information dans un fichier de log, dans le debug ou trace de listener, ou l'afficher dans une message box.

 
Sélectionnez

static readonly string[] LowNames = 
{
    "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", 
    "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI",
    "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB",
    "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US"
};
public static void DisplayString (string text)
{
    Console.WriteLine ("String length: {0}", text.Length);
    foreach (char c in text)
    {
        if (c < 32)
        {
            Console.WriteLine ("<{0}> U+{1:x4}", LowNames[c], (int)c);
        }
        else if (c > 127)
        {
            Console.WriteLine ("(Possibly non-printable) U+{0:x4}", (int)c);
        }
        else
        {
            Console.WriteLine ("{0} U+{1:x4}", c, (int)c);
        }
    }
}

VI. Usage de la mémoire

Dans l'implémentation actuelle, les strings prennent 20+(n/2)*4 bytes (arrondi à la valeur n/2 près), où n est le nombre de caractères du string. Le type string est inhabituel en ceci que la taille de l'objet varie elle-même. Les seules autres classes qui permettent cela (pour autant que je sache) sont les tableaux. Essentiellement, un string est un tableau de caractères en mémoire, plus la longueur du tableau et la longueur du string (en caractères). La longueur du tableau n'est pas toujours la même que celle des caractères, vu que les strings peuvent être "suralloués" au sein de la bibliothèque mscorlib.dll, afin de les construire plus facilement. (StringBuilder peut le faire, par exemple.) Alors que les strings sont immuables, pour le monde extérieur, le code au sein de mscorlib peut changer les contenus, de sorte que StringBuilder crée un string avec un tableau de caractères interne plus grand que le contenu actuel l'exige, puis ajoute des caractères à ce string jusqu'à ce que la taille du tableau soit atteinte, à ce stade il crée un nouveau string avec un grand tableau. La propriété Length de string contient également un flag dans son premier bit pour dire s'il contient ou pas des caractères non ASCII. Cela permet une optimisation supplémentaire dans certains cas.

Bien que les strings ne se terminent pas par null dans la mesure où l'API est concernée, le tableau de caractères se termine par null, ce qui lui permet d'être passé directement à des fonctions non managées sans qu'une copie ne soit impliquée, en supposant que l'inter-op indique que le string peut être marshallé en tant qu'Unicode.

VII. Encodage

Si vous ne savez pas ce qu'est l'encodage des caractères Unicode, vous pouvez lire mon article dédié à ce sujet.

Comme indiqué au début de l'article, les strings sont toujours encodés en Unicode. L'idée "d'une chaîne Big-5" ou d'une "chaîne encodée en UTF-8" est une erreur (dans la mesure où .NET est concerné) et indique généralement un manque de compréhension, soit de l'encodage, soit de la manière dont .NET gère les strings. Il est très important de comprendre cela - le traitement d'un string comme s'il représentait un texte valide dans un encodage non Unicode est presque toujours une erreur.

Maintenant, le jeu de caractères Unicode (un des défauts de l'Unicode est que ce terme est utilisé pour différentes choses, incluant un jeu de caractères codés et un système de codage de caractères) contient plus de 65 536 caractères. Cela signifie qu'un seul char (System.Char) ne peut pas couvrir tous les caractères. Cela conduit à l'utilisation de substituts où les caractères ci-dessus U+FFFF sont représentés dans les strings par deux caractères. Essentiellement, un string utilise le système d'encodage de caractères UTF-16. La plupart des développeurs n'ont pas vraiment besoin d'en savoir plus à ce sujet, mais cela vaut au moins le coup d'en être conscient.

VIII. Les bizarreries de la culture et de l'internationalisation

Quelques-unes des bizarreries de l'Unicode conduisent à des bizarreries dans la gestion des strings et des caractères. Bon nombre des méthodes de string sont dépendantes de la culture - en d'autres termes, ce qu'ils font dépend de la culture du thread courant. Par exemple, vous vous attendez à ce que "i".toUpper() renvoie quoi ? La plupart des gens diraient "I", mais en turc, la bonne réponse serait "İ" (Unicode U+130, "Lettre latine I majuscule avec un point dessus"). Pour effectuer un changement de casse indépendamment de la culture, vous pouvez utiliser CultureInfo.InvariantCulture et le passer à la surcharge de String.ToUpper qui prend un CultureInfo.

Il y a d'autres anomalies quand il s'agit de comparer, trier et de trouver l'index d'une sous-chaîne. Certaines d'entre elles sont spécifiques à la culture et d'autres pas. Par exemple, dans toutes les cultures (d'après ce que j'ai pu constater) "lassen" et "la\u00dfen" (un "scharfes S" ou eszett étant le caractère Unicode échappé ici) sont considérés comme étant égaux lorsque CompareTo ou Compare sont utilisés, mais pas lorsque c'est Equals. IndexOf traitera le eszett de la même manière qu'un "ss", sauf si vous utilisez un CompareInfo.IndexOf et que vous spécifiez CompareOptions.Ordinal comme option à utiliser.

Certains autres caractères Unicode semblent être invisibles pour le IndexOf normal. Quelqu'un a demandé sur le newsgroup C# pourquoi une méthode recherche/remplace partirait dans une boucle infinie. Il utilisait Replace de façon répétée pour remplacer tous les doubles espaces par un seul espace, et vérifiait si c'était terminé ou non en utilisant IndexOf, de sorte que les espaces multiples étaient réduits à un espace. Malheureusement, cela échouait à cause d'un "étrange" caractère dans le string original entre deux espaces. IndexOf trouvait les doubles espaces, ignorant le caractère supplémentaire, mais pas Replace. Je ne sais pas quel était le vrai caractère présent dans le string, mais le problème peut facilement être reproduit en utilisant U+200C qui est un caractère antiliant sans chasse (quoi que cela puisse vouloir dire : exactement !). Mettez un de ces caractères au milieu du texte que vous recherchez et IndexOf va l'ignorer, mais pas Replace. Une fois encore, pour que les deux méthodes se comportent de la même façon, vous pouvez utiliser CompareInfo.IndexOf et lui passer un CompareOptions.Ordinal. Je suppose qu'il y a beaucoup de codes qui échoueraient face à des données "bizarres" comme celles-ci (je n'aurai pas la prétention de dire que tout mon code est à l'abri de cela).

Microsoft a quelques recommandations concernant la gestion des strings - elles datent de 2005, mais sont toujours valables.

IX. Conclusion

Pour un type aussi fondamental, les strings (et les données textuelles en général) sont plus complexes que ce à quoi on pourrait s'attendre. Il est important de comprendre les bases évoquées ici, même si quelques-uns des points les plus fins de comparaison dans des contextes multiculturels vous échappent pour le moment. En particulier, être en mesure de diagnostiquer les erreurs de code où les données sont perdues, en vérifiant les vraies valeurs des strings, est essentiel.

X. Remerciements

Je tiens à remercier Jon Skeet pour son aimable autorisation de traduire cet article, ainsi que tomlev et jacques_jean pour sa relecture attentive et ses corrections.