String Indexing in Delphi

Delphi has an interesting history when it comes to string indexing. Traditional Pascal strings are one-based, while many C-style APIs and modern helper methods use zero-based indexing.

Today, native Delphi strings are one-based by default on all supported platforms. However, TStringHelper still uses zero-based indexing for methods and properties such as Chars[].


C-Style Strings

In C, traditional strings are arrays of characters terminated by a null character. The first character is at index 0, and functions such as strlen are used to determine the string length.


Pascal Strings

Traditional Pascal ShortString values can contain up to 255 characters. Their representation stores the string length separately from the character data, and the character contents are indexed starting at 1.

This one-based indexing convention became the traditional Delphi model for strings.


String Indexing in Modern Delphi

Older Delphi mobile compilers used zero-based native string indexing, while desktop compilers continued to use one-based indexing.

This could make shared FireMonkey code surprisingly easy to get wrong.

Since Delphi 10.4, native string indexing has been unified across platforms and is one-based by default. This means that the following refers to the first character on Windows, macOS, Android, and iOS:

Ch := S[1];

This removed one of the more awkward differences between the old desktop and mobile compilers.


TStringHelper Uses Zero-Based Indexing

There is still an important distinction to remember: TStringHelper uses zero-based indexing.

For example, the Chars[] property treats index 0 as the first character:

Ch := S.Chars[0];

This means that both of the following refer to the first character of the same string:

Ch := S[1];

Ch := S.Chars[0];

That difference can be surprising, and a potential source of bugs.


Looping with TStringHelper

Because Chars[] is explicitly zero-based, a loop using Length can naturally start at zero:

I := 0;

while (FState <> psComplete) and (I < Xml.Length) do
begin
    Ch := Xml.Chars[I];

    // ...

    Inc(I);
end;

This is safe across current Delphi platforms because the indexing convention belongs to TStringHelper itself rather than depending on the compiler's native string indexing mode.


Direct String Indexing

The Chars[] property is read-only. If you need to modify individual characters directly, you must use the normal string indexer.

For example, the following converts each character in a string to lowercase. Using Low and High avoids hard-coding the string's lower bound:

for I := Low(S) to High(S) do
    S[I] := Char.ToLower(S[I]);

Here the normal [] indexer is required because the characters are being modified. The equivalent Chars[] property cannot be used on the left-hand side of an assignment because it is read-only.


A Few Practical Rules

A few simple rules help avoid indexing mistakes:

  • native Delphi string indexing is one-based by default on current platforms
  • TStringHelper indexing APIs such as Chars[] are zero-based
  • when using the native string indexer directly, prefer Low and High where appropriate
  • avoid changing {$ZEROBASEDSTRINGS} simply to make code fit an indexing assumption; make the intended indexing convention explicit instead

The key point is that Delphi now has consistent native string indexing across platforms, but traditional string indexing and TStringHelper still use different conventions.