Search code examples
stringdelphicharcompiler-warningsdelphi-xe7

How to solve W1047/W1068 warnings when assigning char to string?


I have a simple function that converts a number to a string (similar to how Excel column numbers 1..n can be converted into column names A-ZZZ). As I use this a lot, I have removed local variables as a speed optimization and only work with the Result directly.

When I compile, I get these 2 warnings:

W1047 Unsafe code 'String index to var param'

W1068 Modifying strings in place may not be supported in the future

Here is the shortened function:

function Idx2Code_(Idx: integer): string;
begin
  SetLength(Result, 3); // init Result size
  if Idx <= 26 then
  begin
    // single char: A-Z
    Result := Chr(64 + Idx);
    SetLength(Result, 1);
  end
  else if Idx <= 676 then
  begin
    // 2 chars codes: BA-ZZ
    Result[1] := Chr((Idx div 26) + 64 + 1); // <- warnings for this line
    Result[2] := Chr((Idx mod 26) + 64);     // <- warnings for this line
    SetLength(Result, 2);
  end
  else
  begin
    // 3 chars codes: BAA-ZZZ
    Result[1] := Chr((Idx div 676) + 64 + 1);            // <- warnings for this line
    Result[2] := Chr(((Idx mod 676) div 26) + 64 + 26);  // <- warnings for this line
    Result[3] := Chr((Idx mod 26) + 64 + 26);            // <- warnings for this line
  end;
end;

I can use local variables, but its slower, which I do not want.

I don't want to use code that might become "obsolete" in a future Delphi version.

How can I solve the warnings, but keep the simplicity and speed as much as possible?


Solution

  • In mobile development, direct indexing of Delphi's string type is 0-based by default (this can be disabled with the {$ZEROBASEDSTRINGS OFF} directive).

    In desktop development, direct indexing is still 1-based by default, but may become 0-based in the future.

    Because of this discrepancy between platforms, direct indexing of string is discouraged moving forward, thus the warning messages. To avoid this, there is a 0-based TStringHelper.Chars[] property available on all platforms. However, this property is read-only, so you cannot use it to modify the content of a string (at one point, Embarcadero was considering making string immutable, but then later decided against it, but the warning remains).

    In this situation, you will have to either:

    1. disable the warnings, and use platform-appropriate indexing. You can use Low(string) to help you with that:

      {$WARN UNSAFE_CODE OFF}
      {$WARN IMMUTABLE_STRINGS OFF}
      function Idx2Code_(Idx: integer): string;
      begin
        if Idx <= 26 then
        begin
          // single char: A-Z
          SetLength(Result, 1);
          Result[Low(string)] := Chr(64 + Idx);
        end
        else if Idx <= 676 then
        begin
          // 2 chars codes: BA-ZZ
          SetLength(Result, 2);
          Result[Low(string)] := Chr((Idx div 26) + 64 + 1);
          Result[Low(string)+1] := Chr((Idx mod 26) + 64);
        end
        else
        begin
          // 3 chars codes: BAA-ZZZ
          SetLength(Result, 3);
          Result[Low(string)] := Chr((Idx div 676) + 64 + 1);
          Result[Low(string)+1] := Chr(((Idx mod 676) div 26) + 64 + 26);
          Result[Low(string)+2] := Chr((Idx mod 26) + 64 + 26);
        end;
      end;
      {$WARN IMMUTABLE_STRINGS DEFAULT}
      {$WARN UNSAFE_CODE DEFAULT}
      
    2. use a TStringBuilder:

      uses
        System.SysUtils;
      
      function Idx2Code_(Idx: integer): string;
      var
        SB: TStringBuilder;
      begin
        SB := TStringBuilder.Create(3);
        try
          if Idx <= 26 then
          begin
            // single char: A-Z
            SB.Append(Chr(64 + Idx));
          end
          else if Idx <= 676 then
          begin
            // 2 chars codes: BA-ZZ
            SB.Append(Chr((Idx div 26) + 64 + 1));
            SB.Append(Chr((Idx mod 26) + 64));
          end
          else
          begin
            // 3 chars codes: BAA-ZZZ
            SB.Append(Chr((Idx div 676) + 64 + 1));
            SB.Append(Chr(((Idx mod 676) div 26) + 64 + 26));
            SB.Append(Chr((Idx mod 26) + 64 + 26));
          end;
          Result := SB.ToString;
        finally
          SB.Free;
        end;
      end;
      

      Alternatively, TStringbuilder does have its own Chars[] property that is writable:

      uses
        System.SysUtils;
      
      function Idx2Code_(Idx: integer): string;
      var
        SB: TStringBuilder;
      begin
        SB := TStringBuilder.Create(3);
        try
          if Idx <= 26 then
          begin
            // single char: A-Z
            SB.Length := 1;
            SB[0] := Chr(64 + Idx);
          end
          else if Idx <= 676 then
          begin
            // 2 chars codes: BA-ZZ
            SB.Length := 2;
            SB[0] := Chr((Idx div 26) + 64 + 1);
            SB[1] := Chr((Idx mod 26) + 64);
          end
          else
          begin
            // 3 chars codes: BAA-ZZZ
            SB.Length := 3;
            SB[0] := Chr((Idx div 676) + 64 + 1);
            SB[1] := Chr(((Idx mod 676) div 26) + 64 + 26);
            SB[2] := Chr((Idx mod 26) + 64 + 26);
          end;
          Result := SB.ToString;
        finally
          SB.Free;
        end;
      end;
      

    See Embarcadero's documentation for more information:

    Migrating Delphi Code to Mobile from Desktop: Use 0-Based Strings