【问题标题】:Delphi's TPerlRegEx.EscapeRegExChars() always return an empty string?Delphi 的 TPerlRegEx.EscapeRegExChars() 总是返回一个空字符串?
【发布时间】:2014-04-15 14:20:14
【问题描述】:

使用 Delphi XE4,尝试以下代码:

procedure TForm3.Button1Click(Sender: TObject);
var
  myStr: string;
begin
  Edit1.Text := TPerlRegEx.EscapeRegExChars('test');
end;

结果 (Edit1.Text) 为空。

这是一个错误还是我遗漏了什么?我以前对这个TPerlRegEx.EscapeRegExChars 函数没有问题,它的版本来自regular-expressions.info pre-DelphiXE。

更新 2:刚刚升级了一个用 D2010 编写的应用程序并遇到了这个错误,但只是想知道这么明显的错误怎么会存在这么长时间......现在我正在认真考虑让我的代码兼容Free Pascal,但我真的很喜欢这种反义词...

更新 1:我正在使用 Delphi XE4 更新 1。

【问题讨论】:

  • 你能把TPerlRegEx.EscapeRegExChars的代码贴出来吗?它位于System.RegularExpressionsCore。我手头没有 XE4 的资源。您的代码在 XE3 中运行良好。我想知道他们是否通过使用新的字符串辅助方法改进了代码,并忘记了它们返回新值而不是修改现有值。顺便说一句,您的 SSCCE 在使用 Writeln 的控制台应用程序中会好得多。
  • @David:我已经发布了原始代码(注释掉了一行)作为我的答案的一部分。您可以通过取消注释该行并删除其后面的行来获得 XE4/XE5 版本。该错误存在于 XE4 和 XE5 中。
  • @DavidHeffernan,建议已被接受,下次我将使用 WriteLn :)

标签: delphi delphi-xe4


【解决方案1】:

这似乎是一个错误。如果是这种情况,XE4 和 XE5 版本都包含它。我已经打开了QC report 来报告 XE4..XE6。

问题似乎出在函数的最后一行:

Result.Create(Tmp, 0, J);

在调试器中单步执行显示 Tmp(一个 TCharArray)此时正确包含 't','e','s','t', #0, #0, #0, #0,但当函数实际返回时 Result 包含 '',因为在 end; 上设置断点该行之后表示结果在该点(以及函数返回时)包含''

在类助手中提供一个替换版本,稍作更改以实际存储来自对 Create 的调用的返回值解决了该问题:

type
  TPerlRegExHelper = class helper for TPerlRegEx
  public
    class function EscapeRegExCharsEx(const S: string): string; static;
  end;

class function TPerlRegExHelper.EscapeRegExCharsEx(const S: string): string;
var
  I, J: Integer;
  Tmp: TCharArray;
begin
  SetLength(Tmp, S.Length * 2);
  J := 0;
  for I := Low(S) to High(S) do
  begin
    case S[I] of
      '.', '[', ']', '(', ')', '?', '*', '+', '{', '}', '^', '$', '|', '\':
        begin
          Tmp[J] := '\';
          Inc(j);
          Tmp[J] := S[I];
        end;
      #0:
        begin
          Tmp[J] := '\';
          Inc(j);
          Tmp[J] := '0';
        end;
      else
        Tmp[J] := S[I];
    end;
    Inc(J);
  end;
  { Result.Create(Tmp, 0, J); }  // The problem code from the original
  Result := String.Create(Tmp, 0, J);
end;

XE3(和你提到的开源版本)实现逻辑完全不同,使用更标准的Result操作,从Result := S;函数的第一行开始,然后使用System.Insert作为需要为转义字符增加空间。

【讨论】:

  • @David:好的。这是一个我可以确认但无法解释的错误——正如我所说,“似乎是”AFAICT。你能解释一下这个问题,以及为什么我的修复有效吗? (不争论 - 询问信息。)根据调试器,静态类似乎返回一个新值,该值在函数返回之前立即被丢弃。
  • Ken 和 David,实际上我使用 EscapeRegExChars() 的开源版本解决了这个问题,但这并不能消除我对你的帮助的感激之情以及我对你们这些知识渊博的人的钦佩!还是谢谢你们!
【解决方案2】:

这是 XE4 版本中引入的错误,在 XE6 中仍然存在。以前的版本很好。看起来所做的更改是为将来切换到不可变字符串做好准备。

具有讽刺意味的是,该错误是由根本没有为字符串分配值引起的。着手不改变字符串是一回事,但从不初始化它又是另一回事!

所以到bug的分析。 TPerlRegEx.EscapeRegExChars 中有问题的方法在 System.RegularExpressionsCore 单元中定义。这是一个返回字符串的类函数。它的签名是:

class function EscapeRegExChars(const S: string): string;

XE4 实现只对结果变量进行一次引用。如下:

Result.Create(Tmp, 0, J);

这里,Tmp 是一个包含要返回的转义文本的 char 数组,J 是该文本的长度。

因此,作者显然打算将此代码分配给函数返回变量Result。可悲的是,这并没有发生。为什么不?好吧,被调用的Create 方法是在string 的帮助器中定义的。这是在System.SysUtils 单元中定义的TStringHelper。有三个Create 重载,这里起作用的是:

class function Create(const Value: array of Char; StartIndex: Integer; 
  Length: Integer): string; overload; static;

请注意,这是一个类静态函数。这意味着它不是实例方法并且没有Self 指针。所以当这样调用时:

Result.Create(Tmp, 0, J);

它只是一个函数调用,其返回值被忽略。可能会设置结果变量,但请记住这个Create 是一个类静态方法。因此它没有实例。编译器只使用Result 的类型来解析该方法。代码相当于:

string.Create(Tmp, 0, J);

没有什么比调用一个返回值被忽略的函数更令人兴奋的了。被允许我们忽略函数返回值的扩展语法打败了。

对代码的修复很简单。将最后一行替换为

Result := string.Create(Tmp, 0, J);

您可以在该单元的副本中应用修复,并将该单元包含在您的代码中。我首选的替代方法是使用代码挂钩。像这样:

unit FixTPerlRegExEscapeRegExChars;

interface

implementation

uses
  System.SysUtils, Winapi.Windows, System.RegularExpressionsCore;

procedure PatchCode(Address: Pointer; const NewCode; Size: Integer);
var
  OldProtect: DWORD;
begin
  if VirtualProtect(Address, Size, PAGE_EXECUTE_READWRITE, OldProtect) then
  begin
    Move(NewCode, Address^, Size);
    FlushInstructionCache(GetCurrentProcess, Address, Size);
    VirtualProtect(Address, Size, OldProtect, @OldProtect);
  end;
end;

type
  PInstruction = ^TInstruction;
  TInstruction = packed record
    Opcode: Byte;
    Offset: Integer;
  end;

procedure RedirectProcedure(OldAddress, NewAddress: Pointer);
var
  NewCode: TInstruction;
begin
  NewCode.Opcode := $E9;//jump relative
  NewCode.Offset := NativeInt(NewAddress)-NativeInt(OldAddress)-SizeOf(NewCode);
  PatchCode(OldAddress, NewCode, SizeOf(NewCode));
end;

function EscapeRegExChars(Self: TPerlRegEx; const S: string): string;
var
  I, J: Integer;
  Tmp: TCharArray;
begin
  SetLength(Tmp, S.Length * 2);
  J := 0;
  for I := Low(S) to High(S) do
  begin
    case S[I] of
      '.', '[', ']', '(', ')', '?', '*', '+', '{', '}', '^', '$', '|', '\':
        begin
          Tmp[J] := '\';
          Inc(j);
          Tmp[J] := S[I];
        end;
      #0:
        begin
          Tmp[J] := '\';
          Inc(j);
          Tmp[J] := '0';
        end;
      else
        Tmp[J] := S[I];
    end;
    Inc(J);
  end;
  Result := string.Create(Tmp, 0, J);
end;

initialization
  RedirectProcedure(@TPerlRegEx.EscapeRegExChars, @EscapeRegExChars);

end.

将此单元添加到您的项目中,对TPerlRegEx.EscapeRegExChars 的调用将再次开始工作。

{$APPTYPE CONSOLE}

uses
  System.RegularExpressionsCore,
  FixTPerlRegExEscapeRegExChars in 'FixTPerlRegExEscapeRegExChars.pas';

begin
  Writeln(TPerlRegEx.EscapeRegExChars('test'));
  Readln;
end.

输出

测试

QC#124091

【讨论】:

  • 质检报告已提交。 #124091。 (我之前提交过,当时看到新版本的XE6还存在这个bug。
  • @ken 谢谢。我已经链接到它了。我写这个答案是为了解决您在 cmets 中对您的回答提出的问题。并为未来的访问者提供基于钩子的解决方案。
猜你喜欢
  • 2018-11-18
  • 1970-01-01
  • 2022-01-22
  • 2013-10-09
  • 1970-01-01
  • 2016-12-16
  • 2019-07-25
  • 1970-01-01
  • 2013-04-01
相关资源
最近更新 更多