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.