Database
 sql >> Base de données >  >> RDS >> Database

Aspects des chaînes dans .NET

Le type de données chaîne est l'un des types de données les plus importants dans tous les langages de programmation. Vous pouvez difficilement écrire un programme utile sans lui. Néanmoins, de nombreux développeurs ne connaissent pas certains aspects de ce type. Par conséquent, considérons ces aspects.

Représentation des chaînes en mémoire

Dans .Net, les chaînes sont localisées selon la règle BSTR (chaîne de base ou chaîne binaire). Cette méthode de représentation des données sous forme de chaîne est utilisée dans COM (le mot "basic" provient du langage de programmation Visual Basic dans lequel il était initialement utilisé). Comme nous le savons, PWSZ (Pointer to Wide-character String, Zero-terminal) est utilisé en C/C++ pour la représentation des chaînes. Avec un tel emplacement en mémoire, une terminaison nulle est située à la fin d'une chaîne. Ce terminateur permet de déterminer la fin de la chaîne. La longueur de la chaîne dans PWSZ n'est limitée que par un volume d'espace libre.

Dans BSTR, la situation est légèrement différente.

Les aspects de base de la représentation de chaîne BSTR en mémoire sont les suivants :

  1. La longueur de la chaîne est limitée par un certain nombre. Dans PWSZ, la longueur de la chaîne est limitée par la disponibilité de mémoire libre.
  2. La chaîne BSTR pointe toujours sur le premier caractère du tampon. PWSZ peut pointer sur n'importe quel caractère du tampon.
  3. Dans BSTR, similaire à PWSZ, le caractère nul est toujours situé à la fin. Dans BSTR, le caractère nul est un caractère valide et peut se trouver n'importe où dans la chaîne.
  4. Parce que le terminateur nul est situé à la fin, BSTR est compatible avec PWSZ, mais pas l'inverse.

Par conséquent, les chaînes dans .NET sont représentées en mémoire conformément à la règle BSTR. Le tampon contient une longueur de chaîne de 4 octets suivie de caractères de deux octets d'une chaîne au format UTF-16, qui, à son tour, est suivie de deux octets nuls (\u0000).

L'utilisation de cette implémentation présente de nombreux avantages :la longueur de la chaîne ne doit pas être recalculée car elle est stockée dans l'en-tête, une chaîne peut contenir des caractères nuls n'importe où. Et la chose la plus importante est que l'adresse d'une chaîne (épinglée) peut être facilement passée sur le code natif où WCHAR* est attendu.

Combien de mémoire un objet chaîne prend-il ?

J'ai rencontré des articles indiquant que la taille de l'objet chaîne est égale à taille=20 + (longueur/2)*4, mais cette formule n'est pas tout à fait correcte.

Pour commencer, une chaîne est un type de lien, donc les quatre premiers octets contiennent SyncBlockIndex et les quatre octets suivants contiennent le pointeur de type.

Taille de la chaîne =4 + 4 + …

Comme je l'ai indiqué ci-dessus, la longueur de la chaîne est stockée dans le tampon. C'est un champ de type int, nous devons donc ajouter 4 octets supplémentaires.

Taille de la chaîne =4 + 4 + 4 + …

Pour passer rapidement une chaîne au code natif (sans copier), le terminateur nul est situé à la fin de chaque chaîne qui prend 2 octets. Par conséquent,

Taille de la chaîne =4 + 4 + 4 + 2 + …

Il ne reste plus qu'à rappeler que chaque caractère d'une chaîne est au codage UTF-16 et prend également 2 octets. Par conséquent :

Taille de la corde =4 + 4 + 4 + 2 + 2 * longueur =14 + 2 * longueur

Encore une chose et nous avons terminé. La mémoire allouée par le gestionnaire de mémoire dans CLR est un multiple de 4 octets (4, 8, 12, 16, 20, 24, …). Ainsi, si la longueur de la chaîne prend 34 octets au total, 36 octets seront alloués. Nous devons arrondir notre valeur au plus grand nombre le plus proche qui est un multiple de quatre. Pour cela, nous avons besoin :

Taille de la chaîne =4 * ((14 + 2 * longueur + 3) / 4) (division entière)

La question des versions :jusqu'à .NET v4, il y avait un m_arrayLength supplémentaire champ de type int dans la classe String qui prenait 4 octets. Ce champ est une longueur réelle du tampon alloué pour une chaîne, y compris le terminateur nul, c'est-à-dire qu'il est de longueur + 1. Dans .NET 4.0, ce champ a été supprimé de la classe. Du coup, un objet de type chaîne occupe 4 octets de moins.

La taille d'une chaîne vide sans le m_arrayLength champ (c'est-à-dire dans .Net 4.0 et supérieur) est égal à =4 + 4 + 4 + 2 =14 octets, et avec ce champ (c'est-à-dire inférieur à .Net 4.0), sa taille est égale à =4 + 4 + 4 + 4 + 2 =18 octets. Si nous arrondissons de 4 octets, la taille sera de 16 et 20 octets, en conséquence.

Aspects de la chaîne

Nous avons donc considéré la représentation des chaînes et la taille qu'elles prennent en mémoire. Parlons maintenant de leurs particularités.

Les aspects de base des chaînes dans .NET sont les suivants :

  1. Les chaînes sont des types de référence.
  2. Les chaînes sont immuables. Une fois créée, une chaîne ne peut pas être modifiée (par des moyens équitables). Chaque appel de la méthode de cette classe renvoie une nouvelle chaîne, tandis que la chaîne précédente devient une proie pour le ramasse-miettes.
  3. Les chaînes redéfinissent la méthode Object.Equals. Par conséquent, la méthode compare les valeurs de caractères dans les chaînes, et non les valeurs de lien.

Examinons chaque point en détail.

Les chaînes sont des types de référence

Les chaînes sont de véritables types de référence. C'est-à-dire qu'ils sont toujours situés en tas. Beaucoup d'entre nous les confondent avec des types de valeur, car ils se comportent de la même manière. Par exemple, ils sont immuables et leur comparaison est effectuée par valeur et non par référence, mais il faut garder à l'esprit qu'il s'agit d'un type référence.

Les chaînes sont immuables

  • Les chaînes sont immuables dans un but. L'immuabilité des chaînes présente un certain nombre d'avantages :
  • Le type de chaîne est thread-safe, car aucun thread ne peut modifier le contenu d'une chaîne.
  • L'utilisation de chaînes immuables entraîne une diminution de la charge mémoire, car il n'est pas nécessaire de stocker 2 instances de la même chaîne. Par conséquent, moins de mémoire est utilisée et la comparaison est effectuée plus rapidement, car seules les références sont comparées. Dans .NET, ce mécanisme est appelé string interning (string pool). Nous en reparlerons un peu plus tard.
  • Lorsque vous passez un paramètre immuable à une méthode, nous pouvons cesser de nous inquiéter qu'il soit modifié (s'il n'a pas été passé en tant que ref ou out, bien sûr).

Les structures de données peuvent être divisées en deux types :éphémères et persistantes. Les structures de données éphémères ne stockent que leurs dernières versions. Les structures de données persistantes enregistrent toutes leurs versions précédentes lors de la modification. Ces derniers sont, en effet, immuables, puisque leurs opérations ne modifient pas la structure sur site. Au lieu de cela, ils renvoient une nouvelle structure basée sur la précédente.

Étant donné que les chaînes sont immuables, elles pourraient être persistantes, mais elles ne le sont pas. Les chaînes sont éphémères dans .Net.

À titre de comparaison, prenons les chaînes Java. Ils sont immuables, comme dans .NET, mais en plus ils sont persistants. L'implémentation de la classe String en Java ressemble à ceci :

public final class String { private final char value[] ; décalage int final privé ; décompte final privé ; hachage int privé ; ..... } 

En plus des 8 octets dans l'en-tête de l'objet, y compris une référence au type et une référence à un objet de synchronisation, les chaînes contiennent les champs suivants :

  1. Une référence à un tableau de caractères ;
  2. Un index du premier caractère de la chaîne dans le tableau de caractères (décalé depuis le début)
  3. Le nombre de caractères dans la chaîne ;
  4. Le code de hachage calculé après le premier appel de HashCode() méthode.

Les chaînes en Java utilisent plus de mémoire que dans .NET, car elles contiennent des champs supplémentaires leur permettant d'être persistantes. En raison de la persistance, l'exécution de la String.substring() la méthode en Java prend O(1) , car il ne nécessite pas de copie de chaîne comme dans .NET, où l'exécution de cette méthode prend O(n) .

Implémentation de la méthode String.substring() en Java :

public String substring(int beginIndex, int endIndex) { if (beginIndex <0) throw new StringIndexOutOfBoundsException(beginIndex); if (endIndex> count) throw new StringIndexOutOfBoundsException(endIndex); if (beginIndex> endIndex) lance une nouvelle StringIndexOutOfBoundsException (endIndex - beginIndex); return ((beginIndex ==0) &&(endIndex ==count)) ? this :new String(offset + beginIndex, endIndex - beginIndex, value);}public String(int offset, int count, char value[]) { this.value =value; this.offset =décalage ; this.count =count;}

Cependant, si une chaîne source est suffisamment grande et que la sous-chaîne découpée comporte plusieurs caractères, le tableau entier de caractères de la chaîne initiale sera en attente en mémoire jusqu'à ce qu'il y ait une référence à la sous-chaîne. Ou, si vous sérialisez la sous-chaîne reçue par des moyens standard et que vous la transmettez sur le réseau, tout le tableau d'origine sera sérialisé et le nombre d'octets transmis sur le réseau sera important. Par conséquent, au lieu du code

s =ss.substring(3)

le code suivant peut être utilisé :

s =nouvelle chaîne(ss.substring(3)),

Ce code ne stockera pas la référence au tableau de caractères de la chaîne source. Au lieu de cela, il copiera uniquement la partie réellement utilisée du tableau. Au fait, si nous appelons ce constructeur sur une chaîne dont la longueur est égale à la longueur du tableau de caractères, la copie n'aura pas lieu. Au lieu de cela, la référence au tableau d'origine sera utilisée.

Il s'est avéré que l'implémentation du type chaîne a été modifiée dans la dernière version de Java. Maintenant, il n'y a pas de champs de décalage et de longueur dans la classe. Le nouveau hash32 (avec un algorithme de hachage différent) a été introduit à la place. Cela signifie que les chaînes ne sont plus persistantes. Maintenant, la String.substring créera une nouvelle chaîne à chaque fois.

Chaîne redéfinir Onbject.Equals

La classe de chaîne redéfinit la méthode Object.Equals. En conséquence, la comparaison a lieu, mais pas par référence, mais par valeur. Je suppose que les développeurs sont reconnaissants aux créateurs de la classe String d'avoir redéfini l'opérateur ==, car le code qui utilise ==pour la comparaison de chaînes semble plus profond que l'appel de méthode.

si (s1 ==s2)

Comparé à

if (s1.Equals(s2))

Au fait, en Java, l'opérateur ==compare par référence. Si vous avez besoin de comparer des chaînes par caractère, nous devons utiliser la méthode string.equals().

Stage en chaîne

Enfin, considérons l'internement des chaînes. Examinons un exemple simple :un code qui inverse une chaîne.

var s ="Les chaînes sont immuables";int length =s.Length;for (int i =0; i  

Évidemment, ce code ne peut pas être compilé. Le compilateur lancera des erreurs pour ces chaînes, car nous essayons de modifier le contenu de la chaîne. Toute méthode de la classe String renvoie une nouvelle instance de la chaîne, au lieu de sa modification de contenu.

La chaîne peut être modifiée, mais nous devrons utiliser le code non sécurisé. Prenons l'exemple suivant :

var s ="Les chaînes sont immuables";int length =s.Length; unsafe { fixe (char* c =s) { for (int i =0; i  

Après exécution de ce code, elbatummi era sgnirtS sera écrit dans la chaîne, comme prévu. La mutabilité des chaînes conduit à un cas fantaisiste lié à l'internement des chaînes.

Stage en chaîne est un mécanisme où des littéraux similaires sont représentés en mémoire comme un seul objet.

En bref, le but de l'internement des chaînes est le suivant :il existe une seule table interne hachée dans un processus (pas dans un domaine d'application), dans laquelle les chaînes sont ses clés et les valeurs sont des références à celles-ci. Lors de la compilation JIT, les chaînes littérales sont placées séquentiellement dans une table (chaque chaîne d'une table ne peut être trouvée qu'une seule fois). Lors de l'exécution, les références aux chaînes littérales sont attribuées à partir de cette table. Lors de l'exécution, nous pouvons placer une chaîne dans la table interne avec le String.Intern méthode. De plus, nous pouvons vérifier la disponibilité d'une chaîne dans la table interne à l'aide de String.IsInterned méthode.

var s1 ="habrahabr";var s2 ="habrahabr";var s3 ="habra" + "habr";Console.WriteLine(object.ReferenceEquals(s1, s2));//trueConsole.WriteLine(object. ReferenceEquals(s1, s3));//true

Notez que seuls les littéraux de chaîne sont internés par défaut. Étant donné que la table interne hachée est utilisée pour l'implémentation interne, la recherche dans cette table est effectuée lors de la compilation JIT. Ce processus prend un certain temps. Ainsi, si toutes les chaînes sont internées, cela réduira l'optimisation à zéro. Lors de la compilation en code IL, le compilateur concatène toutes les chaînes littérales, car il n'est pas nécessaire de les stocker en parties. Par conséquent, la seconde égalité renvoie true .

Maintenant, revenons à notre cas. Considérez le code suivant :

var s ="Les chaînes sont immuables";int length =s.Length;unsafe { fixe (char* c =s) { for (int i =0; i  

Il semble que tout soit assez évident et que le code devrait renvoyer Les chaînes sont immuables . Cependant, ce n'est pas le cas ! Le code renvoie elbatummi era sgnirtS . Cela se produit exactement à cause du stage. Lorsque nous modifions des chaînes, nous modifions son contenu, et puisqu'il est littéral, il est interne et représenté par une seule instance de la chaîne.

Nous pouvons abandonner l'internement des chaînes si nous appliquons le CompilationRelaxationsAttribute attribut à l'assemblage. Cet attribut contrôle la précision du code créé par le compilateur JIT de l'environnement CLR. Le constructeur de cet attribut accepte les CompilationRelaxations énumération, qui inclut actuellement uniquement CompilationRelaxations.NoStringInterning . En conséquence, l'assemblage est marqué comme celui qui ne nécessite pas d'internement.

Soit dit en passant, cet attribut n'est pas traité dans .NET Framework v1.0. C'est pourquoi, il était impossible de désactiver l'internat. A partir de la version 2, le mscorlib l'assemblage est marqué avec cet attribut. Ainsi, il s'avère que les chaînes dans .NET peuvent être modifiées avec le code non sécurisé.

Et si nous oublions ce qui n'est pas sûr ?

En l'occurrence, nous pouvons modifier le contenu de la chaîne sans le code non sécurisé. Au lieu de cela, nous pouvons utiliser le mécanisme de réflexion. Cette astuce a réussi dans .NET jusqu'à la version 2.0. Par la suite, les développeurs de la classe String nous ont privés de cette opportunité. Dans .NET 2.0, la classe String a deux méthodes internes :SetChar pour la vérification des limites et InternalSetCharNoBoundsCheck qui ne fait pas de vérification des bornes. Ces méthodes définissent le caractère spécifié par un certain index. L'implémentation des méthodes se présente de la manière suivante :

internal unsafe void SetChar(int index, char value) { if ((uint)index>=(uint)this.Length) throw new ArgumentOutOfRangeException("index", Environment.GetResourceString("ArgumentOutOfRange_Index")); fixe (char* chPtr =&this.m_firstChar) chPtr[index] =valeur ; }internal unsafe void InternalSetCharNoBoundsCheck (int index, char value) { fixe (char* chPtr =&this.m_firstChar) chPtr[index] =value; } 

Par conséquent, nous pouvons modifier le contenu de la chaîne sans code non sécurisé à l'aide du code suivant :

var s ="Les chaînes sont immuables";int length =s.Length;var method =typeof(string).GetMethod("InternalSetCharNoBoundsCheck", BindingFlags.Instance | BindingFlags.NonPublic);for (int i =0; i  

Comme prévu, le code renvoie elbatummi era sgnirtS .

La question des versions :dans différentes versions de .NET Framework, string.Empty peut être intégré ou non. Considérons le code suivant :

string str1 =String.Empty;StringBuilder sb =new StringBuilder().Append(String.Empty);string str2 =String.Intern(sb.ToString()); if (object.ReferenceEquals(str1, str2)) Console.WriteLine("Equal");else Console.WriteLine("Not Equal");

Dans .NET Framework 1.0, .NET Framework 1.1 et .NET Framework 3.5 avec le service pack 1 (SP1), str1 et str2 ne sont pas égaux. Actuellement, string.Empty n'est pas interné.

Aspects des performances

Il y a un effet secondaire négatif du stage. Le fait est que la référence à un objet interne String stocké par CLR peut être enregistrée même après la fin du travail de l'application et même après la fin du travail du domaine d'application. Par conséquent, il est préférable d'omettre d'utiliser de grandes chaînes littérales. S'il est toujours requis, le stage doit être désactivé en appliquant les CompilationRelaxations attribut à l'assemblage.