sql >> Databáze >  >> RDS >> Database

Aspekty řetězců v .NET

Datový typ řetězec je jedním z nejvýznamnějších datových typů v jakémkoli programovacím jazyce. Bez něj jen stěží napíšete užitečný program. Přesto mnoho vývojářů určité aspekty tohoto typu nezná. Zvažme proto tyto aspekty.

Reprezentace řetězců v paměti

V .Net jsou řetězce umístěny podle pravidla BSTR (Basic string nebo binary string). Tato metoda reprezentace řetězcových dat se používá v COM (slovo ‚základní‘ pochází z programovacího jazyka Visual Basic, ve kterém bylo původně použito). Jak víme, PWSZ (Pointer to Wide-character String, Zero-terminated) se v C/C++ používá pro reprezentaci řetězců. S takovým umístěním v paměti je na konci řetězce umístěna nula ukončená. Tento terminátor umožňuje určit konec řetězce. Délka řetězce v PWSZ je omezena pouze objemem volného místa.

V BSTR je situace mírně odlišná.

Základní aspekty reprezentace řetězce BSTR v paměti jsou následující:

  1. Délka řetězce je omezena určitým číslem. V PWSZ je délka řetězce omezena dostupností volné paměti.
  2. Řetězec BSTR vždy ukazuje na první znak ve vyrovnávací paměti. PWSZ může ukazovat na jakýkoli znak ve vyrovnávací paměti.
  3. V BSTR, podobně jako v PWSZ, je znak null vždy umístěn na konci. V BSTR je znakem null platný znak a lze jej nalézt kdekoli v řetězci.
  4. Vzhledem k tomu, že nulový terminátor je umístěn na konci, je BSTR kompatibilní s PWSZ, ale ne naopak.

Proto jsou řetězce v .NET reprezentovány v paměti podle pravidla BSTR. Vyrovnávací paměť obsahuje 4bajtový řetězec délky následovaný dvoubajtovými znaky řetězce ve formátu UTF-16, po kterém následují dva prázdné bajty (\u0000).

Použití této implementace má mnoho výhod:délka řetězce se nesmí přepočítávat, protože je uložena v záhlaví, řetězec může obsahovat prázdné znaky kdekoli. A nejdůležitější je, že adresa řetězce (připnutá) může být snadno předána přes nativní kód, kde WCHAR* se očekává.

Kolik paměti zabírá objekt typu string?

Setkal jsem se s články uvádějícími, že velikost objektu typu string se rovná size=20 + (délka/2)*4, ale tento vzorec není zcela správný.

Začněte tím, že řetězec je typ odkazu, takže první čtyři bajty obsahují SyncBlockIndex a další čtyři bajty obsahují ukazatel typu.

Velikost řetězce =4 + 4 + …

Jak jsem uvedl výše, délka řetězce je uložena ve vyrovnávací paměti. Je to pole typu int, proto potřebujeme přidat další 4 bajty.

Velikost řetězce =4 + 4 + 4 + …

Pro rychlé předání řetězce do nativního kódu (bez kopírování) je na konci každého řetězce, který zabírá 2 bajty, umístěn null terminátor. Proto,

Velikost řetězce =4 + 4 + 4 + 2 + …

Jediné, co zbývá, je připomenout, že každý znak v řetězci je v kódování UTF-16 a také zabírá 2 bajty. Proto:

Velikost řetězce =4 + 4 + 4 + 2 + 2 * délka =14 + 2 * délka

Ještě jedna věc a máme hotovo. Paměť přidělená správcem paměti v CLR je násobkem 4 bajtů (4, 8, 12, 16, 20, 24, …). Pokud tedy délka řetězce zabere celkem 34 bajtů, bude přiděleno 36 bajtů. Musíme naši hodnotu zaokrouhlit na nejbližší větší číslo, které je násobkem čtyř. K tomu potřebujeme:

Velikost řetězce =4 * ((14 + 2 * délka + 3) / 4) (dělení celým číslem)

Problém s verzemi :do verze .NET v4 existovala další m_arrayLength pole typu int ve třídě String, které trvalo 4 bajty. Toto pole je skutečnou délkou vyrovnávací paměti přidělené pro řetězec, včetně null terminátoru, tj. je to délka + 1. V .NET 4.0 bylo toto pole z třídy vypuštěno. Výsledkem je, že objekt typu řetězec zabírá o 4 bajty méně.

Velikost prázdného řetězce bez m_arrayLength pole (tj. v .Net 4.0 a vyšší) se rovná =4 + 4 + 4 + 2 =14 bajtů a s tímto polem (tj. nižším než .Net 4.0) se jeho velikost rovná =4 + 4 + 4 + 4 + 2 =18 bajtů. Pokud zaokrouhlíme 4 bajty, velikost bude 16 a 20 bajtů.

Aspekty řetězce

Zvažovali jsme tedy reprezentaci řetězců a jejich velikost v paměti. Nyní si promluvme o jejich zvláštnostech.

Základní aspekty řetězců v .NET jsou následující:

  1. Řetězce jsou referenční typy.
  2. Řetězce jsou neměnné. Jakmile je řetězec vytvořen, nelze jej upravit (spravedlivými prostředky). Každé volání metody této třídy vrací nový řetězec, zatímco předchozí řetězec se stává kořistí pro sběrač odpadu.
  3. Řetězce předefinují metodu Object.Equals. Výsledkem je, že metoda porovnává hodnoty znaků v řetězcích, nikoli hodnoty odkazů.

Podívejme se podrobně na každý bod.

Řetězce jsou referenční typy

Řetězce jsou skutečné referenční typy. To znamená, že jsou vždy umístěny v hromadě. Mnozí z nás si je pletou s hodnotovými typy, protože ty se chovají stejně. Jsou například neměnné a jejich porovnání se provádí podle hodnoty, nikoli podle odkazů, ale musíme mít na paměti, že jde o typ reference.

Řetězce jsou neměnné

  • Řetězce jsou pro určitý účel neměnné. Neměnnost řetězce má řadu výhod:
  • Typ řetězce je bezpečný pro vlákna, protože ani jedno vlákno nemůže upravit obsah řetězce.
  • Použití neměnných řetězců vede ke snížení zatížení paměti, protože není potřeba ukládat 2 instance stejného řetězce. V důsledku toho se spotřebuje méně paměti a porovnání se provádí rychleji, protože se porovnávají pouze reference. V .NET se tento mechanismus nazývá string interning (string pool). Promluvíme si o tom trochu později.
  • Při předávání neměnného parametru metodě se můžeme přestat bát, že bude upraven (pokud samozřejmě nebyl předán jako ref nebo out).

Datové struktury lze rozdělit do dvou typů:efemérní a perzistentní. Pomíjivé datové struktury ukládají pouze jejich poslední verze. Trvalé datové struktury ukládají během modifikace všechny své předchozí verze. Ty jsou ve skutečnosti neměnné, protože jejich operace nemění strukturu na místě. Místo toho vrátí novou strukturu, která je založena na předchozí.

Vzhledem k tomu, že řetězce jsou neměnné, mohly by být perzistentní, ale nejsou. Řetězce jsou v .Net pomíjivé.

Pro srovnání si vezměme Java řetězce. Jsou neměnné, jako v .NET, ale navíc jsou trvalé. Implementace třídy String v Javě vypadá následovně:

public final class String
	{
	    private final char value[];
	    private final int offset;
 	private final int count;
 	private int hash; 
  	.....
	}

Kromě 8 bajtů v záhlaví objektu, včetně odkazu na typ a odkazu na synchronizační objekt, obsahují řetězce následující pole:

  1. Odkaz na pole znaků;
  2. Index prvního znaku řetězce v poli znaků (odsazený od začátku)
  3. Počet znaků v řetězci;
  4. Hash kód vypočítaný po prvním volání HashCode() metoda.

Řetězce v Javě zabírají více paměti než v .NET, protože obsahují další pole, která jim umožňují být perzistentní. Vzhledem k vytrvalosti se provádí String.substring() metoda v Javě trvá O(1) , protože nevyžaduje kopírování řetězců jako v .NET, kde provedení této metody trvá O(n) .

Implementace metody String.substring() v Javě:

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;
}

Pokud je však zdrojový řetězec dostatečně velký a vyříznutý podřetězec má několik znaků, celé pole znaků počátečního řetězce bude čekat v paměti, dokud se neobjeví odkaz na podřetězec. Nebo, pokud serializujete přijatý podřetězec standardními prostředky a předáváte jej přes síť, bude serializováno celé původní pole a počet bajtů, které jsou předány přes síť, bude velký. Proto místo kódu

s =ss.substring(3)

lze použít následující kód:

s =nový řetězec(ss.substring(3)),

Tento kód neuloží odkaz na pole znaků zdrojového řetězce. Místo toho zkopíruje pouze skutečně použitou část pole. Mimochodem, pokud zavoláme tento konstruktor na řetězci, jehož délka se rovná délce pole znaků, kopírování neproběhne. Místo toho bude použit odkaz na původní pole.

Jak se ukázalo, implementace typu string byla v poslední verzi Javy změněna. Nyní ve třídě nejsou žádná pole offsetu a délky. Nový hash32 (s jiným hashovacím algoritmem) byl místo toho zaveden. To znamená, že řetězce již nejsou perzistentní. Nyní String.substring metoda pokaždé vytvoří nový řetězec.

Předefinování řetězce Onbject.Equals

Třída string předefinuje metodu Object.Equals. V důsledku toho dochází ke srovnání, ale ne podle reference, ale podle hodnoty. Předpokládám, že vývojáři jsou vděční tvůrcům třídy String za předefinování operátoru ==, protože kód, který používá ==pro porovnání řetězců, vypadá hlouběji než volání metody.

if (s1 == s2)

V porovnání s

if (s1.Equals(s2))

Mimochodem, v Javě operátor ==porovnává podle odkazu. Pokud potřebujete porovnat řetězce podle znaků, musíme použít metodu string.equals().

String Interning

Nakonec se podívejme na internování strun. Podívejme se na jednoduchý příklad – kód, který obrátí řetězec.

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;
}

Je zřejmé, že tento kód nelze zkompilovat. Kompilátor vyvolá chyby pro tyto řetězce, protože se snažíme upravit obsah řetězce. Jakákoli metoda třídy String vrací novou instanci řetězce namísto jeho úpravy obsahu.

Řetězec lze upravit, ale budeme muset použít nebezpečný kód. Podívejme se na následující příklad:

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;
       }
      }
   }

Po provedení tohoto kódu elbatummi era sgnirtS bude zapsáno do řetězce podle očekávání. Proměnlivost strun vede k efektnímu případu souvisejícímu s internováním strun.

Internování řetězců je mechanismus, kde jsou podobné literály reprezentovány v paměti jako jeden objekt.

Stručně řečeno, bod internování řetězců je následující:v procesu (nikoli v aplikační doméně) existuje jediná hashovaná interní tabulka, kde řetězce jsou jeho klíče a hodnoty jsou odkazy na ně. Během kompilace JIT jsou doslovné řetězce umístěny do tabulky postupně (každý řetězec v tabulce lze nalézt pouze jednou). Během provádění jsou z této tabulky přiřazeny odkazy na doslovné řetězce. Během provádění můžeme umístit řetězec do interní tabulky pomocí String.Intern metoda. Také můžeme zkontrolovat dostupnost řetězce v interní tabulce pomocí String.IsInterned metoda.

var s1 = "habrahabr";
var s2 = "habrahabr";
var s3 = "habra" + "habr";

Console.WriteLine(object.ReferenceEquals(s1, s2));//true
Console.WriteLine(object.ReferenceEquals(s1, s3));//true

Všimněte si, že ve výchozím nastavení jsou internovány pouze řetězcové literály. Vzhledem k tomu, že se pro implementaci interningu používá hashovaná interní tabulka, vyhledávání proti této tabulce se provádí během kompilace JIT. Tento proces nějakou dobu trvá. Pokud jsou tedy všechny řetězce internovány, sníží to optimalizaci na nulu. Během kompilace do IL kódu kompilátor zřetězí všechny doslovné řetězce, protože není potřeba je ukládat po částech. Proto druhá rovnost vrátí hodnotu pravda .

Nyní se vraťme k našemu případu. Zvažte následující kód:

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");

Zdá se, že vše je zcela zřejmé a kód by měl vrátit Řetězce jsou neměnné . Nicméně není! Kód vracíelbatummi era sgnirtS . Děje se to právě kvůli stáži. Když upravujeme řetězce, upravujeme jejich obsah, a protože je doslovný, je internován a reprezentován jedinou instancí řetězce.

Pokud použijeme CompilationRelaxationsAttribute, můžeme vkládání řetězců opustit atribut k shromáždění. Tento atribut řídí přesnost kódu, který je vytvořen kompilátorem JIT prostředí CLR. Konstruktor tohoto atributu přijímá CompilationRelaxations výčet, který v současnosti zahrnuje pouze CompilationRelaxations.NoStringInterning . Výsledkem je, že sestava je označena jako sestava, která nevyžaduje internování.

Mimochodem, tento atribut není zpracován v .NET Framework v1.0. Proto nebylo možné zakázat internování. Od verze 2, mscorlib sestava je označena tímto atributem. Ukazuje se tedy, že řetězce v .NET lze upravit pomocí nebezpečného kódu.

Co když zapomeneme na nebezpečné?

Jak se to stane, můžeme upravit obsah řetězce bez nebezpečného kódu. Místo toho můžeme použít reflexní mechanismus. Tento trik byl úspěšný v .NET až do verze 2.0. Poté nás vývojáři třídy String o tuto možnost připravili. V .NET 2.0 má třída String dvě interní metody:SetChar pro kontrolu hranic a InternalSetCharNoBoundsCheck která neklade meze kontroly. Tyto metody nastavují zadaný znak určitým indexem. Implementace metod vypadá následovně:

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;
 }

Proto můžeme upravit obsah řetězce bez nebezpečného kódu pomocí následujícího kódu:

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");

Podle očekávání kód vrací elbatummi era sgnirtS .

Problém s verzemi :v různých verzích .NET Framework, string.Empty může být integrován nebo ne. Podívejme se na následující kód:

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");

V .NET Framework 1.0, .NET Framework 1.1 a .NET Framework 3.5 s aktualizací service pack 1 (SP1), str1 a str2 nejsou si rovni. Aktuálně string.Empty není internován.

Aspekty výkonu

Stážování má jeden negativní vedlejší účinek. Jde o to, že odkaz na objekt String internovaný uložený v CLR lze uložit i po ukončení práce aplikace a dokonce i po ukončení práce s doménou aplikace. Proto je lepší vynechat použití velkých doslovných řetězců. Pokud je to stále vyžadováno, musí být stáž deaktivována použitím CompilationRelaxations atribut k sestavení.


  1. Vyberte MySQL s podmínkou CONCAT

  2. Oracle Apex 5.0 – Zobrazení statického obrazu

  3. Konfigurace připojení skupiny dostupnosti

  4. Postgres FOR LOOP