Pascal hat einen schönen String – etwas „Stringtheorie“ für Programmierer
Übersicht: folgende Datentypen (anklickbar) werden von modernen Pascal-Compilern angeboten, um Zeichenketten zu speichern. Auf dieser Seite werden außerdem folgende Themen behandelt: PChar, const-Parameter sowie Speicher und Referenzzählung.
| Array of Char | historisch |
| ShortString | UTF-8 oder ANSI, max. 255 Bytes, ohne Referenzzählung |
| String | systemabhängig AnsiString oder UnicodeString |
| AnsiString | UTF-8 oder ANSI |
| UTF8String | UTF-8-typisierter AnsiString |
| RawByteString | untypisierter AnsiString, übernimmt Codepage der Quelle |
| WideString | UTF-16, ohne Referenzzählung |
| UnicodeString | UTF-16 |
| UCS4String | UTF-32, Array of UCS4Char |
Einleitung
Mit den Typen AnsiString und UnicodeString haben Delphi und FreePascal/Lazarus sehr mächtige, praktische und schnelle String-Typen bekommen. Beide Typen führen eine Längenangabe mit sich (ein „counted string“ im Gegensatz zu einem Null-terminierten String), können dynamisch so viel Speicher reservieren wie die Systemarchitektur und das Betriebssysstem hergeben, wissen mit welcher Zeichenkodierung ihr Inhalt kodiert wurde, und haben eine Referenzzählung, um unnötiges Kopieren des Inhalts zu vermeiden.
Das ist alles sehr komfortabel und lässt kaum Wünsche offen – außer vielleicht, dass die Sprache des Inhalts, z.B. de, en, fr, etc., nicht in den Metadaten hinterlegt werden kann. Und in einem besonderen Fall kann die Referenzzählung von Nachteil sein, nämlich wenn ein String-Inhalt in mehreren Threads (Multi-Threading) genutzt werden soll, muss sich der Programmierer vorab darum kümmern, jedem Thread eine eigene String-Kopie zu übergeben.
Aus heutiger Sicht ist die Namensgebung einiger Stringtypen etwas unglücklich.
Der UnicodeString müsste eigentlich UTF16String heißen (analog zum UTF8String), denn potentiell alle Stringtypen können
Unicode-Zeichen enthalten, weil UTF-8 und UTF-32 ebenso zum Unicode-System gehören wie UTF-16.
Und der UCS4String sollte besser UTF32String heißen, denn UCS4 wurde vom UTF-32-Standard abgelöst
(funktionell identisch).
String
Der Typ String ist kein eindeutiger Typ, sondern hängt von der Umgebung und den Compiler-Einstellungen ab. In FreePascal und Lazarus enthält eine neue Unit standardmäßig den Compiler-Schalter {$H+}, was bedeutet, dass der Typ String ein langer String sein soll. Ein langer String ist in FreePascal ein AnsiString (UTF-8). Dahingegen ist ein langer String in Delphi ab Version 2009 ein UnicodeString (UTF-16).
Ist dieser Compiler-Schalter in einer Unit nicht vorhanden oder als {$H-} angegeben,
ist der Typ String ein ShortString mit 255 fest reservierten Bytes für den Inhalt.
Heap-Speicher und Referenzzählung
Die modernen String-Typen in Delphi und FreePascal, d.h. AnsiString und UnicodeString, sind referenzgezählt. Ausgenommen sind die Typen ShortString, WideString und UCS4String.
Die Referenzierung eines Inhalts bedeutet, dass eine String-Variable intern nur ein Zeiger ist, der auf den Inhalt zeigt, welcher evtl. von mehreren String-Variablen gemeinsam genutzt wird.
Wird einem AnsiString ein konstanter Inhalt zugewiesen, z.B.: s := ‚Hallo‘;, erhält die Variable s nur einen Zeiger auf die Konstante, die im read-only Code-Segment des Programms gespeichert ist. Die Funktion StringRefCount(s) gibt hierbei -1 als Referenzzähler zurück, woran man eine Konstantenzuweisung erkennen kann.
Heap-allokiert: Wird der String-Inhalt zur Laufzeit erzeugt, z.B. durch eine Verkettung wie s := s + ‚ Welt!‘;, wird dafür Speicherplatz auf dem Heap (↗dynamischer Speicher) reserviert, wo der neue Inhalt als vollständige Zeichenkette „Hallo Welt!“ abgelegt wird. Solange die neue Zeichenkette erst einer einzigen Variablen zugewiesen ist, liefert StringRefCount(s) den Wert 1 als Referenzzähler.
Die AnsiString-Variable selbst ist immer nur ein Zeiger auf den Inhalt. Der Zeiger ist 4 Byte (32 Bit) bzw. 8 Byte (64 Bit) groß und wird im Gültigkeitsbereich der Variable gespeichert, d.h. bei einer lokalen Variablen auf dem Stack, oder auch z.B. in einem Record oder Objekt.
Die Metadaten werden zusammen mit dem Inhalt im Speicher abgelegt. Die folgende Tabelle zeigt die Speicherbelegung,
wie sie bei AnsiString-Inhalten verwendet wird.
Die String-Variable als Pointer zeigt direkt auf den Inhalt, d.h. für den Zugriff auf die Metadaten wird intern entsprechend vor der Pointer-Adresse gelesen.
Auch die Typ-Umwandlung (Type Cast) mittels PChar(s) liefert bekanntermaßen den Zeiger direkt auf den Inhalt (auf das erste Zeichen).
| CodePage | ElementSize | ReferenceCount | Length | Inhalt | Terminator |
| 2 Bytes | 2 Bytes | 4 oder 8 Bytes | 4 oder 8 Bytes | Chars | Char #0 |
Der String-Inhalt kann in einer 32-Bit-Anwendung theoretisch bis zu 2 GB groß sein. In einer 64-Bit-Anwendung könnte ein String theoretisch sogar bis zu 9 Exabyte groß sein – das wären 9 Milliarden GB.
Wird einer String-Variablen ein String gleichen Typs zugewiesen, wird der Inhalt nicht kopiert, sondern nur ein Zeiger auf den bestehenden Inhalt gesetzt und der Referenzzähler für diesen Inhalt erhöht. Das geht schneller als den Inhalt zu kopieren, insbesondere wenn der String 9 Exabyte groß ist. 😉
Der „Copy on Write“-Mechanismus kopiert den Inhalt automatisch erst dann, wenn eine Inhaltsänderung es erforderlich macht. Wenn die String-Variable einen anderen Inhalt zugewiesen bekommt, wird der Referenzzähler des bisherigen Inhalts wieder verringert. Wenn dabei der Zähler auf 0 fällt, bedeutet es, dass der alte Inhalt von keiner Variablen mehr verwendet (nicht mehr referenziert) wird und deshalb der bisher belegte Speicher freigegeben wird.
Ein Inhalt kann bis zu 2 Milliarden mal referenziert werden (32 Bit, signed?), auf 64-Bit-Systemen sogar 9 Trillionen-fach referenziert werden
(laut Wiki:
„On 64-bit targets, fields RefCount/Length consume 8 bytes each, not 4.“).
const-Parameter
procedure MyProcedure(const s: String);
In einem Funktionskopf sollte eine String-Übergabe möglichst als konstanter Parameter mit „const“ deklariert werden, sofern innerhalb der Prozedur keine Änderung am String-Inhalt nötig ist. Der Vorteil ist eine höhere Ausführungsgeschwindigkeit. Dies gilt auch bei anderen strukturierten Typen wie Record oder Array.
Wird der Übergabeparameter ohne „const“ deklariert, wird der Inhalt i.d.R. für die lokale Variable kopiert. Das dient dem Zweck, dass Änderungen innerhalb der Prozedur bleiben und sich der Inhalt für den Aufrufer nicht ändert, welcher wahrscheinlich mit seinen unveränderten Daten weiterarbeiten möchte. Eine lokale Kopie ist jedoch unnötig, wenn Inhalte nur gelesen und nicht verändert werden. Dies zeigt die const-Deklaration dem Compiler an.
Allerdings ist die const-Deklaration bei modernen Stringtypen mit Referenzzähler (AnsiString und UnicodeString) weniger wichtig als bei früheren Typen. Ohne „const“ wird z.B. ein ShortString komplett kopiert. Aber bei einem referenzgezählten String wird ohne „const“ einfach nur der Zähler erhöht. Trotzdem hat auch hier ein Konstantparameter einen Vorteil, denn dann wird sogar auf das Hoch- und Runterzählen des Referenzzählers verzichtet, was immerhin noch einen kleinen Geschwindigkeitsvorteil bringt.
Array[1..n] of Char
Rückblick: In den ersten Pascal-Versionen in den 1970ern gab es noch keinen String-Typ. Stattdessen musste man für eine Zeichenkette ein statisches Array selber definieren, mit einer festen Länge je nach Bedarf.
In Pascal wurde das Char-Array traditionell mit Index 1 beginnend definiert, weil der ↗Pascal-Erfinder die Zählweise ab 1 als für Menschen natürlicher empfand. Auch heute noch hat sich der Startindex 1 bei allen neueren Stringtypen in Pascal erhalten – mit Ausnahme des UCS4String, der bloß ein Array of UCS4Char mit Startindex 0 ist.
Wenn der Inhalt nicht die volle Länge des Arrays belegt, muss man die tatsächlich belegte Länge irgendwo gesondert speichern.
Das war wirklich sehr unpraktisch.
Oder man setzt das Char hinter dem Inhalt auf #0 – als Null-terminierter String wie in der Programmiersprache C.
PChar
PChar ist kein Stringtyp, sondern lediglich ein Pointer (Zeiger) auf ein Zeichen. PChar wird hauptsächlich dazu verwendet, einen Zeiger an eine Funktion in einer API oder einer sonstigen Funktionsbibliothek (Library) zu übergeben, z.B. für Funktionen in einer .dll (z.B. der Windows-API) oder einer .so-Datei.
Der Typ PChar reserviert keinen eigenen Speicherplatz für den Inhalt, d.h. bei der Verwendung von PChar muss man sicherstellen, dass der Speicher anderweitig reserviert ist und unverändert reserviert bleibt – nämlich solange wie der PChar verwendet werden könnte, um auf den Inhalt zuzugreifen.
PChar ist kein eindeutiger Typ, sondern kann umgebungsabhängig ein PAnsiChar oder PWideChar sein – entsprechend dem Compiler-Schalter, ob ein String ein AnsiString oder UnicodeString ist.
Wendet man PChar(s) als Typ-Umwandlung (Type Cast) an, erhält man einen Zeiger auf das erste Zeichen im String bzw. NIL bei einem leeren String. Man muss stets beachten, dass nur die String-Variable für die Reservierung des Speichers zuständig ist, d.h. solange man den PChar als Zeiger verwenden oder weitergeben möchte, muss die String-Variable unverändert bleiben.
Ein PChar sollte auf einen Null-terminierten Inhalt zeigen, weil ein PChar keine Meta-Informationen mitführt und somit die Länge des Inhalts nicht kennt.
Glücklicherweise sind die Typen AnsiString und UnicodeString automatisch Null-terminiert, so dass man sich nicht darum zu kümmern braucht.
Wenn man keinen Null-terminierten Inhalt hat, wie z.B. in einem ShortString, muss man die Längen-Angabe gesondert speichern und weitergeben.
ShortString
String[n]
Ein ShortString war der erste String-Typ, der in Turbo Pascal eingeführt wurde. Im Prinzip funktioniert ein ShortString ähnlich wie ein Array of Char, mit dem Unterschied, dass das erste Element s[0] die Länge enthält, die vom Inhalt tatsächlich belegt ist. Weil die Längenangabe nur ein Char (1 Byte) ist, kann die Länge des Inhalts maximal 255 Bytes betragen (Char-Wertebereich #0..#255). Das erste Zeichen befindet sich im Element s[1], also weiterhin Pascal-typisch ein 1-basierter Index für den Inhalt.
Die zu reservierende Kapazität des kurzen Strings muss vorab festgelegt werden, z.B. als var s: String[40]; für maximal 40 Bytes Inhalt. Hinzu kommt das Längen-Char, so dass der Typ String[40] immer 41 Bytes belegt, auch wenn der String leer ist. Verwendet man ShortString als Typ, entspricht es String[255], also dem maximalen ShortString für bis zu 255 Zeichen und mit festem Speicherplatzbedarf von 256 Bytes.
Der Speicherort des ShortString richtet sich nach dem Gültigkeitsbereich, in dem die Variable deklariert wird, d.h. als globale Variable wird der gesamte ShortString im Data-Segment abgelegt, als lokale Variable auf dem Stack, als Record-Feld im Record, und als Klassen-Eigenschaft im erzeugten Objekt im Heap.
Die Zeichenkodierung des Inhalts richtet sich nach dem System bzw. erhält einen zugewiesenen Inhalt in dessen vorliegender Kodierung. In FreePascal und Lazarus gelangen heutzutage meistens UTF-8-kodierte Zeichen in den ShortString. In Delphi hingegen ist der Inhalt ANSI-kodiert, d.h. in Westeuropa typischerweise mit der Codepage Windows-1252.
Ein ShortString ist nicht Null-terminiert, d.h. wenn man ihn im Zusammenhang mit PChar nutzen möchte, müsste man dem Inhalt manuell ein #0 anhängen oder die Länge des ShortStrings gesondert weitergeben (falls möglich).
Bei Verwendung eines kurzen Strings mit nur wenigen Zeichen, kann String[n] einen Vorteil bezüglich Speicherbedarf und Geschwindigkeit
gegenüber referenzierten Strings haben.
Z.B. ist für ein Sprachkürzel wie „de“ oder ein Landeskürzel wie „CH“ ein String[3] mit gefälligen 4 Byte Speicherbedarf
sehr kompakt und schneller kopiert als eine Referenz hoch- und runtergezählt ist.
Das Gleiche gilt für Codes wie „de-AT“ oder kurze Wörter, die in String[7] passen.
Hierbei sind die fest belegten 8 Byte Speicher nicht länger als der Pointer eines AnsiStrings in einem 64-Bit-System.
Jedoch wird für die Verwendung in String-Funktionen eine Umwandlung in einen AnsiString oder UnicodeString erforderlich,
was jeden Vorteil ins Gegenteil verkehrt.
AnsiString
Ein AnsiString enthält 1-Byte-Zeichen vom Typ AnsiChar. Der Typ AnsiString wurde 1996 von Delphi 2 eingeführt, dem ersten 32-Bit-Delphi. Vorher musste man sich mit ShortString begnügen. Bezüglich der Speicherverwaltung des AnsiStrings siehe: Heap und Referenzzählung.
Zeichenkodierung (Character Encoding): In Delphi folgt die 1-Byte-Kodierung der Codepage des Betriebssystems, z.B. Codepage „Windows-1252“ in Westeuropa. In Lazarus wechselte man 2016 auf UTF-8 als Standard-Zeichenkodierung für AnsiString. In Konsolen-Anwendungen wird durch den Compiler-Schalter {$codepage utf8} ebenfalls UTF-8 zur Standardkodierung für AnsiString.
In UTF-8 belegt ein Zeichen zwischen 1 Byte und 4 Bytes. Der Vorteil ist, dass sich darin jedes Alphabet und jedes Schriftzeichen abbilden lässt.
Hingegen verwenden die herkömmlichen Codepage-Kodierungen nur 1 Byte pro Zeichen.
Dadurch ist der Zugriff auf ein einzelnes Zeichen sehr einfach, aber jede Codepage ist auf nur ein Alphabet beschränkt.
AnsiString(Codepage)
Typisierung mittels Codepage-Nummer: Es ist möglich, in einem AnsiString eine spezifische Zeichenkodierung zu verwenden, indem man den AnsiString durch die Angabe einer gewünschten Codepage typisiert.
var
s_cp1251: AnsiString(1251); // nur kyrillisch
s_cp1252: AnsiString(1252); // nur Latin-1 (westeuropäisch)
s_cp1253: AnsiString(1253); // nur griechisch
s_utf8 : AnsiString(CP_UTF8); // = UTF8String
s_raw : AnsiString(CP_NONE); // = RawByteString
begin
s_utf8 := 'Γειά σου, Κόσμε!'; // griechisch: "Hallo Welt!"
s_cp1253 := s_utf8; // passt: cp1253 ist für griechische Buchstaben
s_raw := s_cp1253; // passt: ein RawByteString übernimmt jede Codepage
s_cp1252 := s_cp1253; // falsch: cp1252 kennt keine griechischen Buchstaben
end;
Wird einer AnsiString-Variablen eine andere AnsiString-Variable zugewiesen, wird bedarfsweise eine Neukodierung auf die Ziel-Codepage vorgenommen. Das o.g. Beispiel demonstriert solche Spezialfälle. Dagegen ist im Normalfall keine Neukodierung erforderlich, weil in den meisten Programmen alles in derselben Standard-Codepage verwendet wird.
Wird einem UTF8String etwas zugewiesen, funktioniert das immer, weil UTF-8 sämtliche Unicode-Zeichen kodieren kann (in 1 bis 4 Bytes übersetzt). Wird jedoch in dem Beispiel dem s_cp1252 (nur lateinisch) ein s_cp1253 (griechisch) zugewiesen, finden sich für die griechischen Buchstaben keine Entsprechungen in der westeuropäischen Codepage, weshalb solche Zeichen für s_cp1252 ersatzweise in „?“ übersetzt werden. Somit kann man damit nicht mehr viel anfangen.
Vorsicht ist bei der Zuweisung einer Konstanten an einen typisierten String geboten: Es wird immer der konstante, UTF-8-kodierte Inhalt zugewiesen, auch wenn der Ziel-String auf eine andere Codepage typisiert ist und eigentlich nach einer Neukodierung verlangt. Meiner Meinung nach ist das ein Compiler-Fehler, denn beim Auslesen der Variablen wird der konstante UTF-8-Inhalt durch die spezielle Codepage-Interpretation unbrauchbar.
Auf ein anderes Problem bin ich bei TStringList.Text gestoßen: Fügt man einer StringList Strings hinzu,
die nicht der Standard-Codepage des Systems entsprechen, wird dies bei der Verarbeitung durch .Text
nicht berücksichtigt, d.h. man erhält einen unbrauchbaren Gesamt-String, weil seine Standard-Codepage
nicht zum anders kodierten Inhalt passt.
RawByteString
Ein RawByteString ist ein spezieller AnsiString, dem keine übliche Sprach-Codepage zugewiesen ist. Wird einem RawByteString ein AnsiString zugewiesen, übernimmt er einfach dessen Codepage und es findet garantiert keine Umkodierung des Inhalts statt.
function ToWesternEuropean(const s: RawByteString): RawByteString;
begin
Result := s; // hier nur Referenzierung
if StringCodePage(Result) <> 1252 then
SetCodePage(Result, 1252); // De-referenzierung und Umkodierung
end;
Von diesem Funktionskopf wird jede beliebige Codepage akzeptiert, ohne dass eine automatische Umkodierung z.B. in UTF-8 stattfindet.
Innerhalb der Funktion: Wenn der Eingabe-String s bereits in der Codepage 1252 (westeuropäisch) vorliegt, findet keinerlei Umkodierung statt
und es wird die gleiche String-Referenz wieder ausgegeben, quasi ohne Zeitverlust.
UTF8String
Ein UTF8String ist ein typisierter AnsiString(CP_UTF8), d.h. er enthält garantiert einen UTF-8-kodierten Inhalt. Dieser Typ ist nur für Delphi relevant, wo ein AnsiString nicht standardmäßig in UTF-8, sondern in der ANSI-Codepage des Systems kodiert ist.
In Lazarus ist UTF-8 seit 2016 die Standard-Kodierung für AnsiString, der zugleich dem Typ String entspricht. Somit kann man in Lazarus i.d.R. einfach den Typ String verwenden und benötigt UTF8String nicht.
Wenn jedoch eine in FreePascal programmierte Unit Delphi-kompatibel sein soll und aus bestimmten Gründen auf UTF-8 angewiesen ist,
sollte man explizit UTF8String angeben, damit es von beiden Compilern gleich behandelt wird.
WideString
WideString ist ein UTF-16-String, der mit Delphi 3 (1997) speziell für Windows eingeführt wurde. WideString ist die Umsetzung des Windows BSTR-Typ (Basic String), der als nativer String-Typ für die COM-Schnittstelle (OLE-Automation) verwendet wird.
Unter Windows wird der WideString-Inhalt nicht auf dem eigenen Heap abgelegt, sondern über die Windows-API mittels SysAllocString() und SysFreeString() verwaltet. WideString hat deshalb keine Referenzzählung und zudem ist die indirekte Speicherverwaltung über die Windows-API etwas langsamer.
Da es in Linux keine COM-Schnittstelle gibt, wird der String-Inhalt in FreePascal unter Linux im eigenen Heap abgelegt.
Wer also nicht auf das Windows COM-Interface zugreift, aber einen UTF-16-String nutzen möchte,
sollte lieber den Typ UnicodeString verwenden.
UnicodeString
Der UnicodeString ist ausschließlich für die UTF-16-Kodierung (WideChar-Elemente) und wurde mit Delphi 2009 eingeführt. Die Bezeichnung „Unicode“String ist unglücklich gewählt, da UTF-8 und UTF-32 gleichermaßen zum Unicode-System gehören, aber bei diesem Stringtyp speziell nur UTF-16 gemeint ist.
Für die Speicherung gilt: Heap und Referenzzählung wie bei AnsiString. Im Gegensatz zu AnsiString kann der UnicodeString keine unterschiedlichen Kodierungen enthalten, sondern beinhaltet immer UTF-16 (Nachfolger der UCS2-Norm).
In Delphi ist UTF-16 nützlich, um die Windows-API zu bedienen. Deshalb ist der Typ String in Delphi seit 2009 mit UnicodeString gleichgesetzt. In FreePascal/Lazarus, das auch für Linux-Programmierung genutzt wird, hat man sich hingegen für UTF-8 entschieden und ist deshalb bei AnsiString geblieben. Deshalb spielt der UnicodeString in Lazarus kaum eine Rolle (hauptsächlich zur Delphi-Kompatibilität).
Leider hat UTF-16 ein ähnliches Problem wie UTF-8, nämlich dass ein Zeichen aus mehreren Elementen bestehen kann.
Somit bietet der UnicodeString beim Zugriff auf ganze Zeichen keinen Vorteil gegenüber
einem UTF8String.
UCS4String
Der UCS4String ist lediglich ein Array of UCS4Char (je 32 Bit = 4 Byte) und ist dafür gedacht, eine UTF-32-Zeichenkette aufzunehmen. UTF-32 hat den unschlagbaren Vorteil, dass jedes Element ein ganzes Zeichen ist.
Doch leider wird der Typ nicht gut unterstützt. Es gibt nur wenige Funktionen für den UCS4String. Das letzte Element soll eine Null enthalten, um kompatibel mit einem Null-terminierten String zu sein. Leider muss man sich darum komplett selbst kümmern. Die String-Länge ergibt sich aus Length(Array) ‑ 1 und kann ohne Inhalt 0 oder ‑1 sein.
Somit ist UCS4String leider eine ähnliche Bastelei wie das historische Array of Char, das niemand mehr verwenden möchte.
Für UCS4String ist es eigentlich schade, denn 4 Byte pro Zeichen sind heutzutage kein Problem mehr.
Würde es eine gute Unterstützung für UCS4String geben – ähnlich wie für AnsiString – könnte es eigentlich ein moderner, praktischer Typ sein.
Vermutlich ist der Bedarf aber durch die anderen String-Typen ausreichend gedeckt.
Außerdem ist die bestehende Definition als Array of UCS4Char bezüglich Kompatibilität leider eine Sackgasse.
Fazit
Der Möglichkeiten gibt es viele, aber meistens verwendet man doch nur den Typ String, weil er kaum Wünsche offen lässt und als Standard-Stringtyp des Systems von allen String-Funktionen unterstützt wird.
Falls man beim Programmieren doch auf Probleme stößt, hat es häufig etwas mit einer Typ-Umwandlung zu PChar zu tun, oder dass man bei UTF-8 nicht berücksichtigt hat, dass ein Zeichen bis zu 4 Bytes belegen kann, ebenso bei UTF-16. Codepage-Konvertierungem werden meistens nur für Spezialfälle benötigt, z.B. für ANSI-Textdateien oder für Datenbanken.
