Něco o řetězcích a unicode

vložil Radek Červinka 30. března 2011 22:17

Zkusím napsat něco typu string, unicode a příbuzných typech v návaznosti na minulý článek o TEncoding. Předem upozorňuji, že:

  • většinou (cca 99%) si člověk vystačí s typem string a zde uvedený text ho nemusí moc trápit
  • nebude to 100% výklad, ale třeba to někomu pomůže a popř. to někdo upřesní, kdybych něco popletl
  • je to trochu hutnější text, tak snad to nebude zmatené

Řetězec alias string je složený datový typ a jedná se o posloupnost znaků (Char). Delphi z historických a kompatibilních důvodů podporuje několik druhů řetězců a navíc se to komplikuje v závislosti na verzi Delphi. Ale opět opakuji, ve velké většině si člověk vystačí s typem string.

Obecně máme tři kategorie kompilátoru Delphi:

  • Delphi 1 (a Turbo Pascal) - 16 bit verze
  • Delphi 2 - 2007 - 32 bit ne-UNICODE verze
  • Delphi 2009 a novější - 32 bit UNICODE

Ohledně 64bit kompilátoru pravděpodobně moc rozdílů nebude (max. velikost řetězce, ale obecně ne všechny 64bit kompilátory mají 64bit string, např. C# nebo Java jsou vždy 32bit délky).

Všechny verze Delphi a FPC mají automatickou alokaci řetězců, Delphi navíc i počítání referencí a Copy-On-Write (ohledně FPC nevím, pokud to u některého typu neplatí tak na to upozorním). První znak v řetězci má index 1.

Automatické počítání referencí znamená, že jednoduché přiřazení nezkopíruje řetezec, jen pouze inkrementuje počítadlo použití. Při zániku řetězce se dekrementuje počet a pokud je 0 je řetězec uvolněn.

Copy-On-Write znamená, že např. až při změně jednoho znaku je (pokud je počet referencí > 1) řetězec zkopírován.

Ve všech verzích existuje typ string, ale může být mapován na různé řetězcové typy - viz. dále. Dále uvedený program ukazuje mně známé řetězcové typy.

Delphi automaticky provádí konverze (resp. kompilátor zařadí skryté volání konverze) mezi jednotlivým typ stringu při přiřazení (max. uvede warning). Mezi stejnými typem se konverze neprovádí. Je tedy vhodné mít vše v jednom typu pro zamezení různých konverzí.

program Project1;

{$APPTYPE CONSOLE}
uses
  SysUtils;
var
  s: string; // základní typ, ekvivalent UnicodeString pro D2009+
  // nebo AnsiString pro D2007-
  sUnicode: UnicodeString;
  sShort: ShortString;  //255 znaků
  sShort2: String[100]; // shortstring 100 znaku
  sWide: WideString;   // unicode pro COM
  sAnsi: AnsiString;  // AnsiString podle aktuální kódové stránky
  sUtf8: Utf8String;   //  UTF8String = type AnsiString(65001);
  sRaw: RawByteString; //type AnsiString($ffff);
  p1: PChar; // = PUnicodeChar = pointer na 16bit string
  p2: PAnsiChar; // = pointer na 8byte string

type
  T1250String = type AnsiString(1250);
var
  s1250: T1250String; // AnsiString v explicitním kodování CP1250

begin
  s := 'Příliš žluťoučký kůň úpěl ďábelské ódy.';
  writeln('string:',length(s), ' bytes:', ByteLength(s));
  sUnicode := s;
  sShort := s;
  sShort2 := s;
  s1250 := s;
  sAnsi := s;
  writeln('Ansi:',length(s));
  sUtf8 := s;
  writeln('UTF8:',length(sUtf8));
  sUtf8 := UTF8Encode(s);
  sRaw := s;
  sRaw := sAnsi;
end.

Delphi 1 (a Turbo Pascal)

Podporují jen jeden typ - v aktuálních Delphi je označován jako ShortString. Jedná se o 8bit typ s maximální délkou 255 znaků, kde na index 0 obsahuje délku řetězce. Lze definovat i kratší řetězce, viz. program.

string = shortstring

NeUnicode Delphi - Delphi 2 až 2007

32bit Delphi přidávají podporu dlouhých řetězců. V aktuálních Delphi jsou označovány jako AnsiString. Na rozdíl od ShortString je AnsiString dlouhý max. 2GB. Znak (Char) má 8bitů. Proměnná je ve skutečnosti pointer na datovou strukturu obsahující vlastní data. Prázdný řetězec je nil. Používá se u něho počítání výskytů a Copy-On-Write. Řetezec je navíc ukončen nulou (#0).

Pozor: Data pro tyto verze (u Unicode se trochu liší) obsahují:

  • 32bit délky
  • 32bit počitadlo
  • vlastní data

Dále se přidává typ WideString, první unicode typ, primárně určený pro COM. Nepodporuje počítání referencí ani Copy-On-Write.

PChar je pointer na řetězec ukončený nulou. Primárně je určen pro spolupráci s API nebo C. Přetypování řetezce je PChar(s). V případě, že s je AnsiString není v podstatě žádná konverze, jelikož AnsiString je ukončen 0.

PWideChar je pointer na WideString. Pokud používáte UTF8, tak to je uloženo jako AnsiString, ale neplatí, že jeden byte = 1 znak.

string = AnsiString

Unicode Delphi - Delphi 2009+

S příchodem unicode Delphi je navíc typ UnicodeString, který se ale skoro vůbec nepoužívá, jelikož se používá string: string = UnicodeString.

{$IFDEF UNICODE} - rozlišíme unicode kód

UnicodeString je řetězec, kdy znak (Char) má 2byte. Tj. velikost řetězce v bytech = 2 x počet znaků. AnsiChar je ekvivalentem Char z neunicode Delphi.

Pozor: struktura pro AnsiString byla změněna (a je sdílena i pro UnicodeString):

  • 32bit délka
  • 32bit počitadlo
  • 16bit určující počet byte na znak (1 - AnsiString, 2 - UnicodeString)
  • 16bit Codepage (tj. kódová stránka)

Můžeme to popsat takto:

type StrRec = record
      CodePage: Word;
      ElemSize: Word;
      refCount: Integer;
      Len: Integer;
      case Integer of
          1: array[0..0] of AnsiChar;
          2: array[0..0] of WideChar;
end;

PChar je nyní ukazatel na 2byte Char, inkrementování posouvá adresu o 2 byte. PAnsiChar je ekvivalentem PChar z neunicode Delphi.

Nově lze definovat AnsiString určité kódové stránky - viz. ukázkový program. Existují dva předdefinované typy: Utf8String a RawByteString. První specifikuje kódovou stránku na UTF8 (v ukázkovém programu je vypsáno 54 byte místo 39 jako u jiných typů). No resp. Length vrací počet znaků - nikoliv počet byte - viz. příklad. Druhý (RawByteString) říká, že tento string se nemá nijak konvertovat ohledně kódových stránek, měl by se používat jen jako parametr procedur, které nijak nezávisí na kódové stránce.

Výsledek běhu programu:

string:39 bytes:78
Ansi:39
UTF8:54

Pokud potřebujete zjistit kódovou stránku AnsiString, použijte funkci StringCodePage. Pokud u AnsiString nepoužijete specifikaci kodové stránky, je použita DefaultSystemCodePage, u UnicodeString je DefaultUnicodeCodePage, ta je nastavena na CP_UTF16.

  writeln(StringCodePage(sAnsi));
  writeln(DefaultSystemCodePage);

Kompilátor je vybaven řadou varování při kompilaci. Uvedený program vyprodukuje následující log:

dcc command line for "Project1.dpr"
[DCC Warning] Project1.dpr(28): W1058 Implicit string cast with 
  potential data loss from 'string' to 'ShortString'
[DCC Warning] Project1.dpr(29): W1058 Implicit string cast with 
  potential data loss from 'string' to 'ShortString'
[DCC Warning] Project1.dpr(30): W1058 Implicit string cast with 
  potential data loss from 'string' to 'T1250String'
[DCC Warning] Project1.dpr(31): W1058 Implicit string cast with 
  potential data loss from 'string' to 'AnsiString'
[DCC Warning] Project1.dpr(33): W1057 Implicit string cast from 
  'string' to 'UTF8String'
[DCC Warning] Project1.dpr(36): W1058 Implicit string cast with 
  potential data loss from 'string' to 'RawByteString'
[DCC Hint] Project1.dpr(15): H2164 Variable 'p1' is declared 
  but never used in 'Project1'
[DCC Hint] Project1.dpr(16): H2164 Variable 'p2' is declared but 
  never used in 'Project1'
Success

Kompilátor vypíše vždy varování, když se snažíte nacpat UnicodeString do AnsiStringu. Obecně je vhodné používat základní string co nejvíce.

Množina a Char

Dalším varováním je problém s množinou a Char. V tomto případě kompilátor jen upozorňuje na nebezpečí, ale výsledný kód je perfektní. Jedná se o to, že množina je 8bitová, kdežto Char je 16bit. Často používaná konstrukce je

 var c:Char;
 
 if c in ['.', ';', '0'..'1'] then
  …

Toto nyní vygeneruje

[DCC Warning] Project1.dpr(44): W1050 WideChar reduced to byte char 
  in set expressions.  Consider using 'CharInSet' function in 'SysUtils' unit.

Kompilátor v tomto případě vezme dolních 8bitů, což je většinou správné. Pokud warning nevypnete, lze jednoduše nahradit za doporučované

  if CharInSet(c, ['.', ';', '0'..'1']) then
   ..

nebo za pomocí něčeho z TCharacter (Character.pas), např. TCharacter.IsLetterOrDigit. Nevytvářejte instance TCharacter, obsahuje pouze class metody.

No to by snad stačilo.

Tagy: , , , , ,

Praxe

Komentáře

31.3.2011 11:01:16 #

JaroB

Jen pár poznámek
V Delphi 1 byl řetězec něco jako buffer a pokud se nadeklarovala pevná délka např.
Buff: string[50];
tak měl opravdu 50 Char (resp AnsiChar nebo Byte) a nešlo to změnit.

V Delphi for .Net (Delphi 8, 2005/2006 a asi i 2007) byly řetězce vlastně už jen jako unicode, takže se změnila spousta vlastností, byl zaveden StringBuilder, Marshall (aby se dalo přistupovat k systémovým funkcím jako byla hnusná SHGetPathFromIDList() nebo WNetGetConnection(), kde se povaha parametrů dost změnila) atp.
Na jednu stranu to byla výhoda, ale na druhou hromada otravných drobností, které měly základy někde jinde ale rozhodně ne v pascalu. Borland sice připravil spoustu pomocných funkcí a helperů, mám ale pocit, že je nezdokumentoval dobře (tím nebyslím, že by je nepopsali, ale že v době vzniku chyběla spousta vzorových příkladů jak např. třeba jak upravit standardní procedury nebo jak některé konstrukce nahradit třeba StringBuildrem), takže se spousta lidí od toho odvrátila a dělali třeba v C#, protože to byl mainstream.
Ale je fakt, že je to už 7-8 let zpět a to je už asi historie.

JaroB

14.4.2011 13:49:34 #

Duha

V D2009 mám definován s: string. Chci do něj přidávat text (s:=s+'xxxxx'), ale vezme mi to max 2048 znaků. Kdepak může být chyba? Je to nějakým přetypováním, nastavením kompileru či compiler directive? (ač mám nastaveno {$H+} )

Duha

14.4.2011 13:56:00 #

radekc

Nechapu důvod proč by to mělo brát max. 2048 znaků. Nikde takové limity neexistuji, žádný přepínač - prostě nic. Jediné co mne napadá, že text zapisujete někam, kde je omezená velikost - typicky sloupec DB.

Limit řetězce je 2GB (32bit).

radekc

10.12.2012 17:55:49 #

Daniel Andrascik

Radku dik za ten clanok. Z inych clankov bolo dost zrejme ze sa ti do toho nechcelo. Ani mne sa nechcelo luskat tu oficialnu 40stranovu TechNote od embarcadera: "Delphi Unicode Migration for Mere Mortals", ktora je sice kvalitne spracovana, ale citat ju po anglicky zabere o dost viac casu. Tvoj clanok mozno je hutnejsi ako si spominal, ale ucel splnil dokonale. Zmeteny som z neho rozhodne nebol a za 5 minut som bol v obraze...

Daniel Andrascik

Komentování ukončeno

Naše nabídka

MVP
Ing. Radek Červinka - Embarcadero MVP
profil na linkedin, Twitter:@delphicz

Nabízím placené poradenství a konzultace v oblasti programování a vývoje SW.
Dále nabízíme i vývoj speciálního software na zakázku.

Neváhejte nás kontaktovat (i ohledně reklamy).

love Delphi

O Delphi.cz

Delphi je moderní RAD nástroj podporující tvorbu nativních aplikací pro platformu Win32, Win64, Mac OSX, Linux a na iPhone a Android.

Delphi.cz je nezávislý portál pro uživatele Delphi. Portál není koncipován pro úplné začátečníky, i když i ti se zde nebudou nudit, ale spíše na programátory, kteří již něco znají a chtějí své znalosti dále rozvíjet a sledovat novinky.

Poslední komentáře

Comment RSS

Dle měsíců