Het string-gegevenstype is een van de belangrijkste gegevenstypen in elke programmeertaal. Je kunt nauwelijks een nuttig programma schrijven zonder. Toch kennen veel ontwikkelaars bepaalde aspecten van dit type niet. Laten we daarom eens kijken naar deze aspecten.
Representatie van strings in het geheugen
In .Net worden strings gelokaliseerd volgens de BSTR-regel (Basic string of binary string). Deze methode voor het weergeven van stringgegevens wordt gebruikt in COM (het woord 'basic' is afkomstig van de Visual Basic-programmeertaal waarin het aanvankelijk werd gebruikt). Zoals we weten, wordt PWSZ (Pointer to Wide-character String, Zero-terminated) gebruikt in C/C++ voor de weergave van strings. Met een dergelijke locatie in het geheugen bevindt een null-terminated zich aan het einde van een string. Met deze terminator kan het einde van de string worden bepaald. De stringlengte in PWSZ wordt alleen beperkt door een hoeveelheid vrije ruimte.
In BSTR is de situatie iets anders.
De basisaspecten van de BSTR-tekenreeksrepresentatie in het geheugen zijn de volgende:
- De lengte van de tekenreeks wordt beperkt door een bepaald aantal. In PWSZ wordt de stringlengte beperkt door de beschikbaarheid van vrij geheugen.
- BSTR-tekenreeks wijst altijd naar het eerste teken in de buffer. PWSZ kan naar elk teken in de buffer verwijzen.
- In BSTR, vergelijkbaar met PWSZ, staat het nulteken altijd aan het einde. In BSTR is het null-teken een geldig teken en kan het overal in de tekenreeks worden gevonden.
- Omdat de nul-terminator zich aan het einde bevindt, is BSTR compatibel met PWSZ, maar niet omgekeerd.
Daarom worden strings in .NET in het geheugen weergegeven volgens de BSTR-regel. De buffer bevat een tekenreekslengte van 4 bytes gevolgd door tekens van twee bytes van een tekenreeks in het UTF-16-formaat, die op zijn beurt wordt gevolgd door twee nullbytes (\u0000).
Het gebruik van deze implementatie heeft veel voordelen:de lengte van de string hoeft niet opnieuw te worden berekend omdat deze in de header is opgeslagen, een string kan overal null-tekens bevatten. En het belangrijkste is dat het adres van een string (vastgezet) gemakkelijk kan worden doorgegeven aan native code waar WCHAR* wordt verwacht.
Hoeveel geheugen neemt een string-object in beslag?
Ik kwam artikelen tegen waarin stond dat de tekenreeksobjectgrootte gelijk is aan size=20 + (length/2)*4, maar deze formule is niet helemaal correct.
Om te beginnen is een string een linktype, dus de eerste vier bytes bevatten SyncBlockIndex en de volgende vier bytes bevatten de typeaanwijzer.
Snaargrootte =4 + 4 + …
Zoals ik hierboven al zei, wordt de stringlengte opgeslagen in de buffer. Het is een veld van het type int, daarom moeten we nog 4 bytes toevoegen.
Snaargrootte =4 + 4 + 4 + …
Om een string snel door te geven aan native code (zonder te kopiëren), bevindt de null-terminator zich aan het einde van elke string die 2 bytes in beslag neemt. Daarom,
Snaargrootte =4 + 4 + 4 + 2 + …
Het enige wat je nog moet onthouden, is dat elk teken in een string in de UTF-16-codering staat en ook 2 bytes in beslag neemt. Daarom:
Snaargrootte =4 + 4 + 4 + 2 + 2 * lengte =14 + 2 * lengte
Nog één ding en we zijn klaar. Het geheugen toegewezen door de geheugenbeheerder in CLR is een veelvoud van 4 bytes (4, 8, 12, 16, 20, 24, …). Dus als de stringlengte in totaal 34 bytes in beslag neemt, worden 36 bytes toegewezen. We moeten onze waarde afronden op het dichtstbijzijnde grotere getal dat een veelvoud van vier is. Hiervoor hebben we nodig:
Snaargrootte =4 * ((14 + 2 * lengte + 3) / 4) (gehele deling)
De kwestie van versies :tot .NET v4 was er een extra m_arrayLength veld van het type int in de klasse String dat 4 bytes in beslag nam. Dit veld is een echte lengte van de buffer die is toegewezen aan een string, inclusief de nul-terminator, d.w.z. het is lengte + 1. In .NET 4.0 is dit veld uit de klasse verwijderd. Als resultaat neemt een object van het stringtype 4 bytes minder in beslag.
De grootte van een lege tekenreeks zonder de m_arrayLength veld (d.w.z. in .Net 4.0 en hoger) is gelijk aan =4 + 4 + 4 + 2 =14 bytes, en met dit veld (d.w.z. lager dan .Net 4.0) is de grootte gelijk aan =4 + 4 + 4 + 4 + 2 =18 bytes. Als we 4 bytes afronden, wordt de grootte dienovereenkomstig 16 en 20 bytes.
String Aspecten
We hebben dus gekeken naar de representatie van strings en de grootte die ze in het geheugen innemen. Laten we het nu hebben over hun eigenaardigheden.
De basisaspecten van strings in .NET zijn de volgende:
- Tekenreeksen zijn referentietypen.
- Snaren zijn onveranderlijk. Eenmaal gemaakt, kan een string niet worden gewijzigd (met redelijke middelen). Elke aanroep van de methode van deze klasse retourneert een nieuwe string, terwijl de vorige string een prooi wordt voor de vuilnisman.
- Strings herdefiniëren de Object.Equals methode. Als resultaat vergelijkt de methode tekenwaarden in strings, geen linkwaarden.
Laten we elk punt in detail bekijken.
Tekenreeksen zijn referentietypen
Strings zijn echte referentietypes. Dat wil zeggen, ze bevinden zich altijd in de hoop. Velen van ons verwarren ze met waardetypes, aangezien zij zich op dezelfde manier gedragen. Ze zijn bijvoorbeeld onveranderlijk en hun vergelijking wordt uitgevoerd op waarde, niet op referenties, maar we moeten in gedachten houden dat het een referentietype is.
Strings zijn onveranderlijk
- Snaren zijn onveranderlijk voor een bepaald doel. De onveranderlijkheid van de string heeft een aantal voordelen:
- Stringtype is thread-safe, aangezien geen enkele thread de inhoud van een string kan wijzigen.
- Het gebruik van onveranderlijke strings leidt tot een afname van de geheugenbelasting, aangezien het niet nodig is om 2 instanties van dezelfde string op te slaan. Het resultaat is dat er minder geheugen wordt uitgegeven en vergelijkingen sneller worden uitgevoerd, omdat alleen referenties worden vergeleken. In .NET wordt dit mechanisme string interning (string pool) genoemd. We zullen er later over praten.
- Als we een onveranderlijke parameter aan een methode doorgeven, hoeven we ons geen zorgen meer te maken dat deze wordt gewijzigd (als deze niet als ref of out is doorgegeven natuurlijk).
Gegevensstructuren kunnen worden onderverdeeld in twee typen:kortstondig en persistent. Kortstondige datastructuren slaan alleen hun laatste versies op. Persistente datastructuren bewaren al hun vorige versies tijdens wijziging. Deze laatste zijn in feite onveranderlijk, aangezien hun operaties de structuur ter plaatse niet wijzigen. In plaats daarvan geven ze een nieuwe structuur terug die gebaseerd is op de vorige.
Gezien het feit dat strings onveranderlijk zijn, kunnen ze persistent zijn, maar dat zijn ze niet. Strings zijn kortstondig in .Net.
Laten we ter vergelijking Java-strings nemen. Ze zijn onveranderlijk, zoals in .NET, maar bovendien zijn ze persistent. De implementatie van de String-klasse in Java ziet er als volgt uit:
public final class String { private final char value[]; private final int offset; private final int count; private int hash; ..... }
Naast 8 bytes in de header van het object, inclusief een verwijzing naar het type en een verwijzing naar een synchronisatie-object, bevatten strings de volgende velden:
- Een verwijzing naar een char-array;
- Een index van het eerste teken van de tekenreeks in de char-array (offset vanaf het begin)
- Het aantal tekens in de tekenreeks;
- De hashcode die wordt berekend nadat de HashCode() voor het eerst is aangeroepen methode.
Strings in Java nemen meer geheugen in beslag dan in .NET, omdat ze extra velden bevatten waardoor ze persistent kunnen zijn. Vanwege persistentie is de uitvoering van de String.substring() methode in Java duurt O(1) , omdat het niet nodig is om strings te kopiëren zoals in .NET, waar de uitvoering van deze methode O(n) kost .
Implementatie van de methode String.substring() in 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) throw new 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 = offset; this.count = count; }
Als een brontekenreeks echter groot genoeg is en de uitgesneden subtekenreeks meerdere tekens lang is, zal de hele reeks tekens van de initiële tekenreeks in het geheugen worden bewaard totdat er een verwijzing naar de subtekenreeks is. Of, als u de ontvangen substring op standaard wijze serialiseert en via het netwerk doorgeeft, zal de gehele originele array worden geserialiseerd en zal het aantal bytes dat via het netwerk wordt doorgegeven groot zijn. Daarom, in plaats van de code
s =ss.substring(3)
de volgende code kan worden gebruikt:
s =nieuwe String(ss.substring(3)),
Deze code slaat de verwijzing naar de reeks tekens van de bronreeks niet op. In plaats daarvan kopieert het alleen het daadwerkelijk gebruikte deel van de array. Trouwens, als we deze constructor aanroepen op een string waarvan de lengte gelijk is aan de lengte van de array van karakters, zal er niet gekopieerd worden. In plaats daarvan wordt de verwijzing naar de originele array gebruikt.
Het bleek dat de implementatie van het stringtype is gewijzigd in de laatste versie van Java. Nu zijn er geen velden voor offset en lengte in de klas. De nieuwe hash32 (met ander hash-algoritme) is in plaats daarvan geïntroduceerd. Dit betekent dat strings niet meer persistent zijn. Nu, de String.substring methode zal elke keer een nieuwe string aanmaken.
String herdefinieert Onbject.Equals
De klasse string definieert de methode Object.Equals opnieuw. Als resultaat vindt vergelijking plaats, maar niet op basis van referentie, maar op waarde. Ik veronderstel dat de ontwikkelaars de makers van de klasse String dankbaar zijn voor het herdefiniëren van de ==operator, aangezien code die ==gebruikt voor het vergelijken van strings er dieper uitziet dan de methodeaanroep.
if (s1 == s2)
Vergeleken met
if (s1.Equals(s2))
Trouwens, in Java vergelijkt de ==operator op referentie. Als u tekenreeksen per teken wilt vergelijken, moeten we de methode string.equals() gebruiken.
String Stage
Laten we tot slot eens kijken naar stringstage. Laten we een eenvoudig voorbeeld bekijken:een code die een string omkeert.
var s = "Strings are immutuble"; int length = s.Length; for (int i = 0; i < length / 2; i++) { var c = s[i]; s[i] = s[length - i - 1]; s[length - i - 1] = c; }
Het is duidelijk dat deze code niet kan worden gecompileerd. De compiler zal fouten voor deze strings genereren, omdat we proberen de inhoud van de string te wijzigen. Elke methode van de klasse String retourneert een nieuwe instantie van de tekenreeks, in plaats van de wijziging van de inhoud ervan.
De string kan worden gewijzigd, maar we moeten de onveilige code gebruiken. Laten we het volgende voorbeeld bekijken:
var s = "Strings are immutable"; int length = s.Length; unsafe { fixed (char* c = s) { for (int i = 0; i < length / 2; i++) { var temp = c[i]; c[i] = c[length - i - 1]; c[length - i - 1] = temp; } } }
Na uitvoering van deze code, elbatummi era sgnirtS wordt zoals verwacht in de tekenreeks geschreven. Veranderlijkheid van strings leidt tot een mooie zaak met betrekking tot string interning.
String stage is een mechanisme waarbij vergelijkbare letterlijke waarden in het geheugen worden weergegeven als een enkel object.
Kort gezegd, het punt van string-interning is het volgende:er is een enkele gehashte interne tabel binnen een proces (niet binnen een toepassingsdomein), waarin strings de sleutels zijn en waarden ernaar verwijzen. Tijdens JIT-compilatie worden letterlijke tekenreeksen opeenvolgend in een tabel geplaatst (elke tekenreeks in een tabel kan slechts één keer worden gevonden). Tijdens de uitvoering worden verwijzingen naar letterlijke tekenreeksen toegewezen vanuit deze tabel. Tijdens de uitvoering kunnen we een string in de interne tabel plaatsen met de String.Intern methode. We kunnen ook de beschikbaarheid van een string in de interne tabel controleren met behulp van de String.IsInterned methode.
var s1 = "habrahabr"; var s2 = "habrahabr"; var s3 = "habra" + "habr"; Console.WriteLine(object.ReferenceEquals(s1, s2));//true Console.WriteLine(object.ReferenceEquals(s1, s3));//true
Houd er rekening mee dat standaard alleen tekenreeksen worden geïnterneerd. Aangezien de gehashte interne tabel wordt gebruikt voor interne implementatie, wordt de zoekopdracht tegen deze tabel uitgevoerd tijdens JIT-compilatie. Dit proces duurt enige tijd. Dus als alle strings zijn geïnterneerd, wordt de optimalisatie tot nul gereduceerd. Tijdens het compileren in IL-code voegt de compiler alle letterlijke tekenreeksen samen, omdat het niet nodig is ze in delen op te slaan. Daarom retourneert de tweede gelijkheid true .
Laten we nu terugkeren naar ons geval. Overweeg de volgende code:
var s = "Strings are immutable"; int length = s.Length; unsafe { fixed (char* c = s) { for (int i = 0; i < length / 2; i++) { var temp = c[i]; c[i] = c[length - i - 1]; c[length - i - 1] = temp; } } } Console.WriteLine("Strings are immutable");
Het lijkt erop dat alles vrij duidelijk is en dat de code moet terugkeren Strings zijn onveranderlijk . Dat doet het echter niet! De code retourneert elbatummi era sgnirtS . Het gebeurt precies vanwege de internering. Wanneer we strings wijzigen, wijzigen we de inhoud ervan, en aangezien het letterlijk is, wordt het geïnterneerd en vertegenwoordigd door een enkele instantie van de string.
We kunnen stringintering opgeven als we het CompilationRelaxationsAttribute . toepassen toeschrijven aan de vergadering. Dit attribuut regelt de nauwkeurigheid van de code die is gemaakt door de JIT-compiler van de CLR-omgeving. De constructor van dit kenmerk accepteert de CompilationRelaxations opsomming, die momenteel alleen CompilationRelaxations.NoStringInterning . bevat . Als gevolg hiervan wordt de assemblage gemarkeerd als degene die geen internering vereist.
Dit kenmerk wordt overigens niet verwerkt in .NET Framework v1.0. Daarom was het onmogelijk om de stage uit te schakelen. Vanaf versie 2 is de mscorlib assembly is gemarkeerd met dit attribuut. Het blijkt dus dat strings in .NET kunnen worden aangepast met de onveilige code.
Wat als we onveilig vergeten?
We kunnen namelijk de stringinhoud wijzigen zonder de onveilige code. In plaats daarvan kunnen we het reflectiemechanisme gebruiken. Deze truc was succesvol in .NET tot versie 2.0. Daarna hebben ontwikkelaars van de String-klasse ons deze kans ontnomen. In .NET 2.0 heeft de klasse String twee interne methoden:SetChar voor grenscontrole en InternalSetCharNoBoundsCheck dat maakt geen grenscontrole. Deze methoden stellen het opgegeven teken in op een bepaalde index. De implementatie van de methoden ziet er als volgt uit:
internal unsafe void SetChar(int index, char value) { if ((uint)index >= (uint)this.Length) throw new ArgumentOutOfRangeException("index", Environment.GetResourceString("ArgumentOutOfRange_Index")); fixed (char* chPtr = &this.m_firstChar) chPtr[index] = value; } internal unsafe void InternalSetCharNoBoundsCheck (int index, char value) { fixed (char* chPtr = &this.m_firstChar) chPtr[index] = value; }
Daarom kunnen we de tekenreeksinhoud wijzigen zonder onveilige code met behulp van de volgende code:
var s = "Strings are immutable"; int length = s.Length; var method = typeof(string).GetMethod("InternalSetCharNoBoundsCheck", BindingFlags.Instance | BindingFlags.NonPublic); for (int i = 0; i < length / 2; i++) { var temp = s[i]; method.Invoke(s, new object[] { i, s[length - i - 1] }); method.Invoke(s, new object[] { length - i - 1, temp }); } Console.WriteLine("Strings are immutable");
Zoals verwacht, retourneert de code elbatummi era sgnirtS .
De kwestie van versies :in verschillende versies van .NET Framework kan string.Empty worden geïntegreerd of niet. Laten we de volgende code eens bekijken:
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");
In .NET Framework 1.0, .NET Framework 1.1 en .NET Framework 3.5 met het 1 (SP1) servicepack, str1 en str2 zijn niet gelijk. Momenteel string.Empty is niet geïnterneerd.
Aspecten van prestatie
Er is één negatief neveneffect van stage lopen. Het punt is dat de verwijzing naar een String geïnterneerd object dat is opgeslagen door CLR, zelfs na het einde van het applicatiewerk en zelfs na het einde van het applicatiedomeinwerk kan worden bewaard. Daarom is het beter om het gebruik van grote letterlijke tekenreeksen weg te laten. Als het nog steeds nodig is, moet stage worden uitgeschakeld door de CompilationRelaxations . toe te passen toeschrijven aan montage.