delphidelphi-7delphi-xe5

How to work with 0-based strings in a backwards compatible way since Delphi XE5?


I'm trying to convert my current Delphi 7 Win32 code to Delphi XE5 Android with minimal changes, so that my project can be cross-compiled to Win32 from a range of Delphi versions and Android from XE5.

Starting from XE5 there are breaking changes in language aimed at future. One of such changes is zero-based strings.

In older versions with 1-based strings the following code was correct:

function StripColor(aText: string): string;
begin
  for I := 1 to Length(aText) do

but now this is obviously not right. Suggested solution is to use:

for I := Low(aText) to High(aText) do

This way XE5 Win32 handles 1-based strings and XE5 Android handles 0-based strings right. However there's a problem - previous Delphi versions (e.g. XE2) output an error on such code:

E2198 Low cannot be applied to a long string
E2198 High cannot be applied to a long string

I have quite a lot of string manipulation code. My question is - how to modify and keep above code to be compileable in Delphi 7 Win32 and Delphi XE5 Android?

P.S. I know I can still disable ZEROBASEDSTRINGS define in XE5, but that is undesired solution since in XE6 this define will probably be gone and all strings will be forced to be 0-based.


Solution

  • This is rather a sum up of the two answers:

    As pointed out by Remy Lebeau, ZEROBASEDSTRINGS is a per-block conditional. That means that the following code will not work as expected:

    const
      s: string = 'test';
    
    function StringLow(const aString: string): Integer; inline; // <-- inline does not help
    begin
      {$IF CompilerVersion >= 24} 
      Result := Low(aString); // Delphi XE3 and up can use Low(s)
      {$ELSE}
      Result := 1;  // Delphi XE2 and below can't use Low(s), but don't have ZEROBASEDSTRINGS either
      {$ENDIF}
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    begin
      {$ZEROBASEDSTRINGS OFF}
      Memo1.Lines.Add(Low(s).ToString);        // 1
      Memo1.Lines.Add(StringLow(s).ToString);  // 1
      {$ZEROBASEDSTRINGS ON}
    end;
    
    procedure TForm1.Button2Click(Sender: TObject);
    begin
      {$ZEROBASEDSTRINGS ON}
      Memo1.Lines.Add(Low(s).ToString);        // 0
      Memo1.Lines.Add(StringLow(s).ToString);  // 1  <-- Expected to be 0
      {$ZEROBASEDSTRINGS OFF}
    end;
    

    There are 2 possible solutions:

    A. Every time there's string items access or iteration place an IFDEF around it, which is indeed a lot of clutter for the code, but will work properly irregardless of ZEROBASEDSTRINGS setting around it:

    for I := {$IFDEF XE3UP}Low(aText){$ELSE}1{$ENDIF} to {$IFDEF XE3UP}High(aText){$ELSE}Length(aText){$ENDIF} do
    

    B. Since the ZEROBASEDSTRINGS conditional is per-block it never gets spoiled by 3rd party code and if you don't change it in your code you are fine (above StringLow will work fine as long as the caller code has the same ZEROBASEDSTRINGS setting). Note that if target is mobile, you should not apply ZEROBASEDSTRINGS OFF globally in your code since RTL functions (e.g. TStringHelper) will return 0-based results because mobile RTL is compiled with ZEROBASEDSTRINGS ON.

    On a side note - One might suggest to write an overloaded versions of Low/High for older versions of Delphi, but then Low(other type) (where type is array of something) stops working. It looks like since Low/High are not usual functions then can not be overloaded that simply.

    TL;DR - Use custom StringLow and don't change ZEROBASEDSTRINGS in your code.