【问题标题】:String.Split works strange when last value is empty当最后一个值为空时,String.Split 的工作很奇怪
【发布时间】:2015-04-09 06:13:23
【问题描述】:

我想将我的字符串拆分为数组,但当最后一个“值”为空时效果不佳。请看我的例子。是错误还是功能?有什么方法可以在没有变通方法的情况下使用此功能?

var
  arr: TArray<string>;

  arr:='a;b;c'.Split([';']); //length of array = 3, it's OK
  arr:='a;b;c;'.Split([';']); //length of array = 3, but I expect 4
  arr:='a;b;;c'.Split([';']); //length of array = 4 since empty value is inside
  arr:=('a;b;c;'+' ').Split([';']); //length of array = 4 (primitive workaround with space)

【问题讨论】:

  • 这就是它的设计方式。如果你不喜欢它,写你自己的拆分函数。
  • 好的,谢谢大卫。
  • ';x' 会发生什么?你得到一两个值吗?如果你得到两个,那么设计是不对称的,这是一件坏事。
  • 奇怪,但它返回 2。
  • System.StrUtils.SplitString 以您希望的方式实现。

标签: delphi delphi-xe6


【解决方案1】:

此行为无法更改。您无法自定义此拆分功能的工作方式。我怀疑您需要提供自己的拆分实现。 Michael Erikkson 在评论中很有帮助地指出 System.StrUtils.SplitString 的行为方式符合您的期望。

在我看来,设计很差。比如

Length('a;'.Split([';'])) = 1

还有

Length(';a'.Split([';'])) = 2

这种不对称是设计不佳的明显迹象。令人惊讶的是,测试没有发现这一点。

设计如此明显令人怀疑的事实意味着可能值得提交错误报告。我希望它会被拒绝,因为任何更改都会影响现有代码。但你永远不知道。

我的建议:

  1. 根据需要使用您自己的拆分实现。
  2. 提交错误报告。

虽然System.StrUtils.SplitString 做你想做的事,但它的性能并不好。这很可能无关紧要。在这种情况下,您应该使用它。但是,如果性能很重要,那么我提供这个:

{$APPTYPE CONSOLE}

uses
  System.SysUtils, System.Diagnostics, System.StrUtils;

function MySplit(const s: string; Separator: char): TArray<string>;
var
  i, ItemIndex: Integer;
  len: Integer;
  SeparatorCount: Integer;
  Start: Integer;
begin
  len := Length(s);
  if len=0 then begin
    Result := nil;
    exit;
  end;

  SeparatorCount := 0;
  for i := 1 to len do begin
    if s[i]=Separator then begin
      inc(SeparatorCount);
    end;
  end;

  SetLength(Result, SeparatorCount+1);
  ItemIndex := 0;
  Start := 1;
  for i := 1 to len do begin
    if s[i]=Separator then begin
      Result[ItemIndex] := Copy(s, Start, i-Start);
      inc(ItemIndex);
      Start := i+1;
    end;
  end;
  Result[ItemIndex] := Copy(s, Start, len-Start+1);
end;

const
  InputString = 'asdkjhasd,we1324,wqweqw,qweqlkjh,asdqwe,qweqwe,asdasdqw';

var
  i: Integer;
  Stopwatch: TStopwatch;

const
  Count = 3000000;

begin
  Stopwatch := TStopwatch.StartNew;
  for i := 1 to Count do begin
    InputString.Split([',']);
  end;
  Writeln('string.Split: ', Stopwatch.ElapsedMilliseconds);

  Stopwatch := TStopwatch.StartNew;
  for i := 1 to Count do begin
    System.StrUtils.SplitString(InputString, ',');
  end;
  Writeln('StrUtils.SplitString: ', Stopwatch.ElapsedMilliseconds);

  Stopwatch := TStopwatch.StartNew;
  for i := 1 to Count do begin
    MySplit(InputString, ',');
  end;
  Writeln('MySplit: ', Stopwatch.ElapsedMilliseconds);
end.

在我的 E5530 上使用 XE7 构建的 32 位版本的输出是:

字符串。拆分:2798 StrUtils.SplitString:7167 我的分裂:1428

【讨论】:

  • 这种行为主要是基于过多思考程序员应该考虑什么,而不是建立在一个固定的逻辑规则上:“分隔符分隔两个值”,现在每个人都应该这样,结果数组将包含分隔符计数加一个值。
  • @SirRufo 虽然我完全同意这种观点,但我不认为这种行为是故意的。在 D5 TStrings.CommaText 有类似的问题。当我查看代码时,这是一个简单的错误:如果, 作为输入字符串的最后一个字符出现,它将:读取字符,因此下一个字符是#0(PChar 的终止符)并开始下一次迭代环形。但是NextChar = #0 是循环的终止条件,所以这将结束循环。到 2007 年,这个问题已经通过额外的代码修复,在这种情况下显式添加一个空字符串。
  • @CraigYoung 这确实是一个错误,但我在谈论这个错误的实现主要是如何引起的。有人实现它,我希望能测试它(我也是)。但我想没有 UnitTest 和/或没有对整个行为的描述。有时我认为 UnitTest 只需通过以下方式完成:“它编译!” - 这导致我为 RTL 构建了一些(目前不是太多但不断增长的)单元测试。
【解决方案2】:

以下与接受的答案非常相似,但 i)它是一个辅助方法,并且 ii)它接受一个分隔符数组。

由于这些原因,该方法比 David 的方法耗时约 30%,但无论如何可能有用。

program ImprovedSplit;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  TStringHelperEx = record helper for string
  public
    function SplitEx(const Separator: array of Char): TArray<string>;
  end;

var
  TestString : string;
  StringArray : TArray<String>;


{ TStringHelperEx }

function TStringHelperEx.SplitEx( const Separator: array of Char ): TArray<string>;
var
  Str : string;
  Buf, Token : PChar;
  i, cnt : integer;
  sep : Char;
begin
  cnt := 0;
  Str := Self;
  Buf := @Str[1];
  SetLength(Result, 0);

  if Assigned(Buf) then begin

    for sep in Separator do begin
      for i := 0 to Length(Self) do begin
        if Buf[i] = sep then begin
          Buf[i] := #0;
          inc(cnt);
        end;
      end;
    end;

    SetLength(Result, cnt + 1);

    Token := Buf;
    for i := 0 to cnt do begin
      Result[i] := StrPas(Token);
      Token := Token + Length(Token) + 1;
    end;

  end;
end;

begin
  try
    TestString := '';
    StringArray := TestString.SplitEx([';']);
    Assert(Length(StringArray) = 0, 'Failed test for Empty String');

    TestString := 'a';
    StringArray := TestString.SplitEx([';']);
    Assert(Length(StringArray) = 1, 'Failed test for Single String');

    TestString := ';';
    StringArray := TestString.SplitEx([';']);
    Assert(Length(StringArray) = 2, 'Failed test for Single Separator');

    TestString := 'a;';
    StringArray := TestString.SplitEx([';']);
    Assert(Length(StringArray) = 2, 'Failed test for Single String + Single End-Separator');

    TestString := ';a';
    StringArray := TestString.SplitEx([';']);
    Assert(Length(StringArray) = 2, 'Failed test for Single String + Single Start-Separator');

    TestString := 'a;b;c';
    StringArray := TestString.SplitEx([';']);
    Assert(Length(StringArray) = 3, 'Failed test for Simple Case');

    TestString := ';a;b;c;';
    StringArray := TestString.SplitEx([';']);
    Assert(Length(StringArray) = 5, 'Failed test for Start and End Separator');

    TestString := '0;1;2;3;4;5;6;7;8;9;0;1;2;3;4;5;6;7;8;9;0;1;2;3;4;5;6;7;8;9;0;1;2;3;4;5;6;7;8;9';
    StringArray := TestString.SplitEx([';', ',']);
    Assert(Length(StringArray) = 40, 'Failed test for Larger Array');

    TestString := '0;1;2;3;4;5;6;7;8;9;0;1;2;3;4;5;6;7;8;9;0,1,2,3,4,5,6,7,8,9,0;1;2;3;4;5;6;7;8;9';
    StringArray := TestString.SplitEx([';', ',']);
    Assert(Length(StringArray) = 40, 'Failed test for Array of Separators');

    Writeln('No Errors');

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;

  Writeln('Press ENTER to continue');
  Readln(TestString);

end.

【讨论】:

    猜你喜欢
    • 2014-08-15
    • 2011-05-07
    • 1970-01-01
    • 2016-07-10
    • 2023-01-26
    • 1970-01-01
    • 2020-12-03
    相关资源
    最近更新 更多