【问题标题】:Move() to Insert/Delete item(s) from a dynamic array of string移动()从字符串的动态数组中插入/删除项目
【发布时间】:2010-09-16 20:50:50
【问题描述】:

使用 System.Move() 从字符串数组中插入/删除项目并不像从其他简单数据类型数组中插入/删除项目那么容易。问题是......字符串在Delphi中被引用计数。对引用计数数据类型使用 Move() 需要更深入地了解内部编译器行为。

这里有人可以解释我实现该目标所需的步骤,或者使用一些 sn-p 代码更好,或者指导我在互联网上找到一个好的参考吗?

哦,请不要告诉我使用“懒惰但慢的方式”,即for循环,我知道。

【问题讨论】:

    标签: delphi pascal


    【解决方案1】:

    我之前已经演示过如何从动态数组中删除项目:

    在那篇文章中,我从以下代码开始:

    type
      TXArray = array of X;
    
    procedure DeleteX(var A: TXArray; const Index: Cardinal);
    var
      ALength: Cardinal;
      i: Cardinal;
    begin
      ALength := Length(A);
      Assert(ALength > 0);
      Assert(Index < ALength);
      for i := Index + 1 to ALength - 1 do
        A[i - 1] := A[i];
      SetLength(A, ALength - 1);
    end;
    

    不会出错该代码。使用您想要的X 的任何值;在您的情况下,将其替换为 string。如果您想变得更高级并使用Move,那么也可以这样做。

    procedure DeleteX(var A: TXArray; const Index: Cardinal);
    var
      ALength: Cardinal;
      TailElements: Cardinal;
    begin
      ALength := Length(A);
      Assert(ALength > 0);
      Assert(Index < ALength);
      Finalize(A[Index]);
      TailElements := ALength - Index;
      if TailElements > 0 then
        Move(A[Index + 1], A[Index], SizeOf(X) * TailElements);
      Initialize(A[ALength - 1]);
      SetLength(A, ALength - 1);
    end;
    

    由于XstringFinalize 调用相当于将空字符串分配给该数组元素。不过,我在这段代码中使用了Finalize,因为它适用于所有数组元素类型,甚至包括记录、接口、字符串和其他数组的类型。

    对于插入,您只需将事物向相反的方向移动:

    procedure InsertX(var A: TXArray; const Index: Cardinal; const Value: X);
    var
      ALength: Cardinal;
      TailElements: Cardinal;
    begin
      ALength := Length(A);
      Assert(Index <= ALength);
      SetLength(A, ALength + 1);
      Finalize(A[ALength]);
      TailElements := ALength - Index;
      if TailElements > 0 then begin
        Move(A[Index], A[Index + 1], SizeOf(X) * TailElements);
      Initialize(A[Index]);
      A[Index] := Value;
    end;
    

    当您要执行超出语言范围的操作时使用Finalize,例如使用非类型安全的Move 过程覆盖编译器管理类型的变量。重新输入语言的定义部分时使用Initialize。 (该语言定义了当数组使用SetLength 增大或缩小时会发生什么,但它没有定义如何在不使用字符串赋值语句的情况下复制或删除字符串。)

    【讨论】:

    • 非常感谢。 :-) 我会试试你的 sn-p 代码,如果成功,我会接受这个作为接受的答案。但我现在必须去办公室。 :-)
    • 只是一个建议,或者可能是一个请求,如果您能更新您提到的文章以包括插入,我们将很高兴。我喜欢你的网页设计。
    • 我修改了 Rob 的代码以提供一种使用较新的 TArray 构造来执行此操作的方法。罗伯,你怎么看?有什么我错过的问题吗?
    • 应该是:TailElements := ALength - Index - 1;在 DeleteX() 中
    【解决方案2】:

    您没有说明保持数组元素的顺序是否对您很重要。 如果订单不相关,您可以像这样非常非常快

    procedure RemoveRecord(Index: integer);  
    begin
     FRecords[Index]:= FRecords[High(FRecords)];  { Copy the last element over the 'deleted' element }
     SetLength(FRecords, Length(FRecords)-1);     { Cut the last element }
    end;
    
    { I haven't tested the code to see it compiles, but you got the idea anyway... }
    

    对列表进行排序
    如果您有一个巨大的列表需要用户修改,您可以使用与上述类似的方法(打破列表顺序)。当用户完成编辑(多次删除后)时,您会向它显示一个名为“排序列表”的按钮。现在他可以进行冗长的(排序)操作了。
    当然,我在上面假设您的列表可以按某个参数排序。

    自动排序列表
    另一种方法是使分拣过程自动化。当用户从列表中删除内容时,启动一个计时器。如果用户继续删除项目,请继续重置计时器。当计时器成功触发事件时,进行排序,停止计时器。

    【讨论】:

      【解决方案3】:

      要插入一个字符串,只需在数组(即指针数组)的末尾添加一个字符串(惰性方式),然后使用Move改变这个数组元素的顺序(的指针)。

      【讨论】:

        【解决方案4】:

        如果我想在字符串列表的中间插入一个字符串,我会使用 TStringList.Insert。 (它使用 System.Move 快速完成。)

        您使用数组而不是 TStringList 的任何特殊原因?

        【讨论】:

        • 出于性能原因,因为我的数组可能会被大量项目填充。我意识到 TStringList.Capacity 会有所帮助,但 TStringList 仍然会进行不必要的检查——根据文档——“......分配一个小于 Count 的值会从列表末尾删除字符串。分配一个大于 Count 的值分配用于添加更多字符串的空间。”就我而言,我不需要这样的检查。
        • @Phantom:好吧,只要您测量TStringList 方法的实际开销即可。许多人对几毫秒的延迟有着惊人的容忍度。
        • @Phantom:很公平,但还有其他性能影响需要考虑。例如,如果您必须用大量字符串填充数组,您是否每次都使用SetLength(MyArray, length(MyArray) + 1)?这是大量的重新分配和副本,它确实会降低性能。要解决这个问题,您必须分配比使用更多的空间,因此您需要一些东西来跟踪容量和当前计数。要管理它,您应该将它封装在一个对象中……然后您就可以重新发明 TStringList。 :P
        • 是的,梅森是绝对正确的。他评论中的示例是最常见和最严重的 Delphi 性能问题之一。但是,@Phantom,既然你在谈论Move,我猜你已经意识到了这个问题。
        • @Mason,不,我会为 16384 个项目分配内存,刚才说过,如果显然需要,我会分配更多的 16384。
        【解决方案5】:

        在弄乱它之前调用 UniqueString()。

        http://docwiki.embarcadero.com/VCL/en/System.UniqueString

        那么你有一个带有单个引用的字符串。

        删除和插入也是如此,我怀疑你会更快。

        【讨论】:

        • “大概率”表示不太可能。比如:“你觉得我们老板今年会给我们发奖金吗?” “好机会!”在更改字符串的字符内容时,调用 UniqueString 正是 Delete 和 Insert 将执行的操作。但是,在更改动态数组的元素内容时无需调用它。
        • 哎呀,外语表达总是很痛苦:-) 无论如何,我错过了“数组”位。然后只需完成已删除的位并初始化未删除的位(或用零填充它们)即可
        【解决方案6】:

        只是想为将来来到这里的任何人添加这个。

        修改 Rob 的代码,我想出了这种使用较新的 TArray&lt;T&gt; 类型构造的方法。

        type
          TArrayExt = class(TArray)
            class procedure Delete<T>(var A: TArray<T>; const Index: Cardinal; Count: Cardinal = 1);
          end;
        
        implementation
        
        class procedure TArrayExt.Delete<T>(var A: TArray<T>; const Index: Cardinal;
            Count: Cardinal = 1);
        var
          ALength: Cardinal;
          i: Cardinal;
        begin
          ALength := Length(A);
          Assert(ALength > 0);
          Assert(Count > 0);
          Assert(Count <= ALength - Index);
          Assert(Index < ALength);
        
          for i := Index + Count to ALength - 1 do
            A[i - Count] := A[i];
        
          SetLength(A, ALength - Count);
        end;
        

        可以为插入做类似的事情。

        (不希望将其标记为答案,只是希望提供一个示例,该示例太长而无法放入 Rob 出色答案的 cmets 中。)

        (已修复以解决 Rob 的以下 cmets。)

        【讨论】:

        • 您试图一次进行太多更改。让我的代码使用通用的TArray 会很好,但是你决定添加另一个功能,这样做,你破坏了整个事情。我不认为有 任何 输入可以使此功能起作用。
        • 您还有什么比这更具体的要说的吗?我实际上修复了我认为存在的一个错误(我将我的 0/1 转换为 if Index = 0 then 条件)。我现在实际上正在使用它,它似乎工作正常。如果有错误,在调试/运行时肯定不是很明显。欢迎提出建设性的批评。
        • 您能解释一下这如何解决“使用 Move() 从字符串的动态数组中插入/删除项目”的问题吗?我看到了一个与使用 Move 无关的迭代循环,没有解决 dynamic array of stringgeneric TArray&lt;string&gt; 不是 dynamic array of string),并且问题特别要求 不要使用循环,因为张贴者已经知道该怎么做。 IOW,这与这里提出的问题的相关性绝对为零;发布随机不相关的问题答案并不是真正的 SO 工作方式。
        • 转置没有帮助。任何时候Index 不为零,您最终都会写信给A[-1]。您写信的第一个位置应该始终是A[Index]。看我的代码。为什么起始索引首先是有条件的?通过返回我的代码并将每次出现的1 替换为Count,您可以更接近您的预期功能集。
        • 谢谢,这样更有建设性。实际上,它主要是在睡了一夜之后看我的代码,然后“等等,我为什么要写那个?”我现在不知道,但答案很明显。我已经经历了几轮笔和纸演练,所以我有信心在这一点上是正确的。
        【解决方案7】:

        Move() 适用于引用计数类型(如字符串或接口),实际上在 Delphi 的数组和列表中内部使用。但是,现在,在一般情况下,由于托管记录功能,Move() 不再有效。

        【讨论】:

          【解决方案8】:

          如果您使用 System.Move 将项目放入字符串数组中,您应该知道在 Move 之前(现在已被覆盖)的字符串的引用计数为 -1 用于常量字符串,或者> 0 用于变量字符串。不应更改常量字符串,但应相应处理变量字符串:您应该手动降低它们的引用计数(在它们被覆盖之前!)。为此,您应该尝试以下操作:

          Dec(PStrRec(IntPtr(SomeString)-12).refCnt);
          

          但是如果引用计数达到零,您还应该确定相关的内存 - 如果您让它工作,Delphi 本身会做得更好,它是字符串的编译器魔法。哦,还有:如果您要复制的字符串与您写入的字符串来自同一个数组,那么所需的管理将变得非常麻烦,非常快!

          因此,如果可以通过某种方式避免所有这些手动内务处理,我建议让 Delphi 自己处理。

          【讨论】:

          • @PatrickvL:那么,即使对于我的情况,您是否仍然建议我使用 TStringList,也就是说,数组将被连接到计算机的所有硬盘驱动器中的所有文件填充。 TStringList 会足够快吗?
          • @Phantom:TStringList 是为处理大量字符串而设计的。如果你有 Delphi,你就有 Classes.pas;浏览代码到 TStringList 和 TStrings 并查看您可以找到多少位代码,这些代码是专门出于性能原因而存在的。它是整个 RTL 中最常用的类之一,并且已经进行了 15 年的广泛测试。您必须非常努力地提出任何在字符串处理方面明显快于 TStringList 的解决方案。如果您想知道它是否慢,请尝试使用它,看看是否感觉慢。
          • -1 建议手动修改内部的、实现定义的数据结构。您可以分配 SomeString := '' 并让编译器处理零引用计数情况以及线程安全。
          • @Rob :我只是在回答 Phantom 的问题如何手动减少引用计数。我相信这是一个需要回答的有效问题——即使不建议这样做! (我在上一个声明中没有提到这一点吗?)当然,设置 Somestring := '' 也可以,但这又依赖于编译器的魔法,所以我的回答有一定的可信度,你不觉得吗?
          • 不,Patrick,问题没有没有询问如何减少字符串的引用计数。 Phantom 只是表示他知道引用计数是使用 Move 时需要关注的一个问题,所以他询问如何使用 Move 来调整字符串数组。没有任何迹象表明该数组实际上只是一个无类型内存块,因此没有理由认为编译器魔法不可用。
          猜你喜欢
          • 2017-05-02
          • 1970-01-01
          • 2019-10-15
          • 1970-01-01
          • 1970-01-01
          • 2021-12-28
          • 1970-01-01
          • 2021-05-30
          • 2015-11-14
          相关资源
          最近更新 更多