Object Pascal
String-Typen
Unterschiedliche String-Typen
In Delphi gibt es verschiedene Möglichkeiten, Strings zu deklarieren:
- String
- ShortString
- AnsiString
- WideString
- Array[0..x] of Char
- PChar
In diesem Abschnitt soll es darum gehen, welchen Typ man wann benutzt und wie sie sich überhaupt voneinander unterscheiden.
Pascal-Strings
Zunächst muss nach Delphi-Version unterschieden werden. Den Datentyp String gibt es in Pascal schon immer, jedoch hat sich mit der Umstellung auf 32 Bit (Delphi 2) und bei .NET sein Aufbau verändert.
In den 16-Bit-Versionen (bis Delphi 1) bestand ein String aus maximal 255 Zeichen, für den statisch Arbeitsspeicher reserviert wurde. Im ersten Byte des Strings befand sich die Längenangabe. Mit Delphi 2 hat sich das geändert, der alte String-Typ kann trotzdem noch verwendet werden, sein Typ ist ShortString:
var kurzertext: ShortString;
Ebenso wird ein String als ShortString angesehen, wenn man ihm bei der Deklaration eine feste Länge verordnet:
var kurzertext: String[20];
Die "altmodische", aus DOS-Tagen stammende Begrenzung auf 255 Zeichen wurde mit Delphi 2 aufgehoben. Wird seitdem in Win32 eine Variable vom Typ String deklariert, handelt es sich um einen AnsiString. Die Besonderheit hiervon ist, dass es sich bei der Variable nur noch um einen Zeiger auf eine Stelle im Arbeitsspeicher handelt, die dynamisch vergeben wird. Der String kann somit (rein theoretisch) bis zu 2 GB groß werden, wobei sich der Speicherverbrauch automatisch der String-Länge anpasst. Enthält der String keine Zeichen, verbraucht er auch keinen Speicherplatz, abgesehen von 4 Byte für den Zeiger auf nil, also ins Leere. AnsiStrings enden mit einem Nullzeichen (#0), sind also voll kompatibel zu nullterminierten Strings, wie sie manchmal von API-Funktionen gefordert werden.
WideStrings werden in der .NET-Umgebung als Standard-String verwendet, existieren aber auch schon in früheren Delphi-Versionen. Sie bieten jedoch pro Zeichen zwei statt einem Byte Speicher, sind also Unicode-tauglich. AnsiStrings mit einem Byte pro Zeichen können nur Ansi-Zeichen aufnehmen. Mit Unicode sollen jedoch alle Schriftzeichen der Welt dargestellt werden können, weshalb auf zwei Byte pro Zeichen umgestellt wurde. WideStrings werden wie AnsiStrings dynamisch verwaltet. Unter Win32 unterliegen WideStrings nicht der Referenzzählung. Unter Linux und .NET jedoch schon.
Grundsätzlich ist es angebracht, in eigenen Programmen immer den generischen Typ String zur Deklaration von Strings zu verwenden. Kurze Strings (ShortString) sollten wegen ihrer statischen Größe nur in bestimmten Fällen (z.B. Datenaustausch mit DLLs) verwendet werden oder der Speichbedarf bekannt ist und deshalb auf den Verwaltungsaufwand für die Speicherverwaltung von AnsiStrings verzichtet werden kann.
Nullterminierte Strings
Die folgenden String-Typen sind innerhalb eines normalen Delphi-Programms nicht erforderlich, gelegentlich werden sie jedoch zur Kommunikation mit der "Außenwelt", der in C++ geschriebenen Win32-API benötigt.
Bei nullterminierten Strings handelt es sich um Zeichenketten, die ihr Ende durch eine ASCII-Null (#0) kennzeichnen. Man deklariert sie so:
var text: array [0..100] of Char;
Da es sich hierbei um keine normalen Pascal-Strings handelt, müssen solche nullterminierten Strings mit speziellen Funktionen bearbeitet werden, z.B. StrPCopy, um einen Pascal-String in einen nullterminierten String zu kopieren.
Bei PChar handelt es sich um einen Zeiger auf ein C-kompatibles Zeichenarray. Dieser Typ wird von einigen API-Funktionen gefordert. Man erhält ihn ganz einfach, indem man einen AnsiString mittels PChar(langerText) umwandelt.
Arbeiten mit Pascal-Strings
Die Arbeit mit den anfangs erwähnten Pascal-Stings (ShortString, AnsiString, WideString) ist recht einfach:
Zeichenketten zusammenhängen
var text1, text2: String;
begin
text1 := 'toll';
text2 := 'Ich finde Delphi '+text1+'!!';
// text2 enthält nun den Text 'Ich finde Delphi toll!!'
Zugreifen auf ein bestimmtes Zeichen eines Strings
Der Zugriff auf ein einzelnes String-Zeichen erfolgt über dessen Index:
var text: String;
zeichen: Char;
begin
text := 'Ich finde Delphi toll!';
zeichen := text[1];
// zeichen enthält nun den Buchstaben 'I'
Vergleich zweier Strings
Das Vergleichen von zwei Strings erfolgt mit dem Gleichheitszeichen. Dabei wird Groß- und Kleinschreibung beachtet.
var text1, text2: string;
...
if text1 = text2 then ...
Die Delphi-Laufzeitumgebung bietet noch einige weitere Funktionen, z.B. AnsiCompareText zum Vergleich zweier AnsiStrings ohne Berücksichtigung der Groß- und Kleinschreibung. pos hilft beim Auffinden eines Teilstrings; copy zum Kopieren eines Teilstrings und delete zum Löschen eines Teilstrings sind ebenfalls wichtige Bearbeitungsmöglichkeiten.
Arbeiten mit .NET-Strings
Zur effektiven Arbeit mit Strings unter .NET bietet das .NET-Framework die Klasse StringBuilder (Namensraum System.Text). Das Zusammenfügen mehrerer Strings sollte unter .NET nicht mit dem Plus-Operator erfolgen, sondern so:
uses System.Text;
var s1, s2: string;
sb: StringBuilder;
begin
s1 := 'Hallo';
s2 := 'Delphi';
sb := StringBuilder.Create(s1);
s1 := sb.Append(s2).ToString;
Hintergrund: Strings sind unter .NET unveränderbar. Wenn zwei Strings zu einem verbunden werden soll bedeutet das, dass Speicher für den neuen String reserviert werden muss, dann werden die beiden alten Strings in den neuen kopiert und der Speicher der alten Strings freigegeben. Das ist Aufwand, der sich bei Verwendung der StringBuilder-Klasse reduziert.
Referenzzählung bei AnsiStrings
Um Speicherplatz und Rechenleistung zu sparen, besitzen AnsiStrings einen Referenzzähler. Dieser Referenzzähler gibt an, wie viele String-Variablen auf diese Zeichenkette zeigen. Wird einer Variable ein String zugewiesen, so wird der Referenzzähler erhöht. Ändert man nun diesen String, so ruft die Compiler-Magic je nach Zugriffsart die Funktion UniqueStr auf, die eine identische Kopie der Zeichenkette anlegt, deren Referenzzähler auf Eins setzt und den Referenzzähler der vorherigen Zeichenkette verringert. Erreicht ein Referenzzähler den Wert Null, so wird der Speicher für die Zeichenkette freigegeben und die Variable auf den Wert nil gesetzt. Bei konstanten Strings enthält der Referenzzähler den Wert -1. Man bekommt dank der Compiler-Magic von diesem Mechanismus nicht viel mit. Jedoch kann es Situationen geben, in denen das Hintergrundwissen einiges erklären kann und man den Fehler so schneller finden kann.
Wann ruft die Compiler-Magic nun UniqueStr genau auf? Zum einen wird UnitqueStr beim Schreibzugriff auf ein einzelnes Zeichen per S[x] := 'a'; aufgerufen und zum anderen beim Ermitteln des Zeigers eines Zeichen per offset := @S[x]; Nutzt man jedoch PChar(S[x]), so wird kein UniqueStr aufgerufen, womit auch kein const-Parameter geschützt ist.
Bei der Übergabe eines Strings als Parameter wird der String nicht kopiert, sondern nur seine Adresse an die Funktion übergeben. Diese erhöht nun je nach Modifizierer (var, out, const, "ByValue") den Referenzzähler der Zeichenkette. Bei einem var, out und const bleibt der Referenzzähler unverändert, wohingegen bei einem "ByValue" der Referenzzähler erhöht wird. Zu out sollte noch erwähnt werden, dass die Compiler-Magic dafür sorgt, dass der übergebene String beim Eintritt in die Funktion keinen Inhalt hat, also den Leerstring ('') enthält.
Wo wird die Referenzzählung gespeichert?
Bei Shortstrings wird die Länge im ersten Byte, auf das per s[0] zugegriffen werden kann, gespeichert.
Bei AnsiStrings ist das nicht möglich, da diese länger als 255 Zeichen sein können. Aus diesem Grund liegt vor dem eigentlichen String im Speicher noch ein Record, in dem Länge und Referenzzahl weggeschrieben werden.
TStrRec = packed record
RefCount: Longint;
Length: Longint;
end;
Da eine Variable vom Typ AnsiString nur ein Pointer ist, kann man auf den Record zugreifen, indem man den Pointer um 8 Byte dekrementiert.
Eine entsprechende Funktion, die auf diesem Weg den Referenzzähler abfragt, würde so aussehen.
function GetRefCount(const S: string): Integer;
var
P: ^TStrRec;
begin
Result := -1;
if Pointer(S) <> nil then
begin
P := Pointer(Integer(S) - SizeOf(TStrRec));
Result := P^.RefCount;
end;
end;
Tipps zur Optimierung
Man sollte bei String-Parametern so weit wie möglich den Modifizierer const angeben, da bei einem ByVal String der Referenzzähler erhöht und ein Exception-Block eingerichtet wird, was der Performance nicht gerade sonderlich dient.
Änderungen in Delphi für .NET
Der Type string ist in Delphi für .NET ein WideString, der wiederum ein System.String ist. Unter .NET ist jeder String unveränderbar. Dies führt dazu, dass beim Ändern eines Strings ständig neue Strings erzeugt werden müssen. Das bedeutet für S[x] := 'a';, dass ein neuer String angelegt werden muss, bei dem das x-te Zeichen durch ein 'a' ersetzt ist. Dafür muss der gesamte String in ein veränderliches Format kopiert, modifiziert und wieder in einen neuen String umgewandelt werden. Dies ist nicht besonders schnell. Aus diesem Grund sollte man für String-Operationen auf die Klasse System.Text.StringBuilder ausweichen, die für String-Operationen extra geschaffen wurde. Das Ganze gilt auch für die unter Pascal-Programmierern beliebte for-Schleife zum Aneinanderhängen von mehreren Strings.