【问题标题】:Passing slice of a static/dynamic array by reference with start and length specifier使用起始和长度说明符通过引用传递静态/动态数组的切片
【发布时间】:2013-04-06 06:48:11
【问题描述】:

使用open array parameters 将数组(动态或静态)传递给方法/过程/函数, 声明可能如下所示:

procedure WorkWithArray( const anArray : array of Integer);
(* or procedure WorkWithArray( var anArray : array of Integer); *)
var
  i : Integer;
begin
  for i := Low(anArray) to High(anArray) do
  begin
    // Do something with the "open array" anArray
    WriteLn(anArray[i]);
  end;
end;

...
var
  staticArray : array[0..2] of Integer;
  dynArray : array of integer;
  dynArrayG : TArray<Integer>;
begin
  SetLength(dynArray,10);
  SetLength(dynArrayG,10);

  WorkWithArray(staticArray);  // Using a static array
  WorkWithArray(dynArray);     // Using a dynamic array
  WorkWithArray(dynArrayG);    // Using a dynamic generic array
  ...
end;

像这样传递数组是整个 Delphi RTL 中使用的一个非常常见的习惯用法,包括一些非常优化的函数/过程来处理数据数组。


假设我们需要使用数组的子范围调用WorkWithArray。然后我们可以使用内在的Slice() 函数。

第一个没有偏移,从第一个索引开始:

Type
  // Helper declarations
  TIntLongArray = array[0..MaxInt div SizeOf(Integer) - 1] of integer;
  PIntLongArray = ^TIntLongArray;

WorkWithArray(Slice(staticArray,2)); // No type cast needed for static arrays
WorkWithArray(Slice(PIntLongArray(@dynArray)^,2));
WorkWithArray(Slice(PIntLongArray(@dynArrayG)^,2));

注意:动态数组不能直接放入Slice() 函数中, 见"Slice does not work with dynamic arrays"。 所以必须使用类型转换的解决方法。


如果我们想使用不是从第一个元素开始的子范围怎么办?

也可行:

WorkWithArray(Slice(PIntLongArray(@staticArray[1])^,2));
WorkWithArray(Slice(PIntLongArray(@dynArray[1])^,2));
WorkWithArray(Slice(PIntLongArray(@dynArrayG[1])^,2));

注意:偏移量和切片的总和不能超过数组的元素个数。

我知道使用 Copy(myArray,x1,x2) 可以在输入声明为 const 的情况下使用, 但这会制作数组的副本,并且对于大型数组来说效率低下。 (也有堆栈溢出的风险)。


最后,我的问题:

虽然这演示了一种使用起始索引和长度说明符通过引用传递数组子范围的方法, 看起来有点尴尬。 有没有更好的选择?如果有,怎么做?

【问题讨论】:

    标签: arrays delphi slice


    【解决方案1】:

    以防其他人像我一样被绊倒。在旧版本的 Delphi(D2007 和更早版本。不确定 XE 版本)中,如果使用重载,也会出现 E2193 错误:

      procedure Polygon(Points: array of TPoint); overload;
      procedure Polygon(Points: array of TDPoint); overload;
    

    如果移除重载则有效:

      procedure Polygon(Points: array of TPoint); 
      procedure PolygonD(Points: array of TDPoint);
    

    这已在 Delphi 10.3.0(可能还有其他旧版本)中修复。

    【讨论】:

      【解决方案2】:

      如何避免开放数组和切片并使用类似的东西?

      type
         TArrayRef<T> = record
         strict private
           type PointerOfT = ^T;
           FItems: PointerOfT;
           FCount: Integer;
         public  
           // FItems := @AItems[Offset]; FCount := Count;
           constructor Create(AItems: array of T; Offset, Count: Integer);
           property Items[Index: Integer]: T read GetItem; // Exit(FItems[Index])
           property Count: Integer read FCount; 
         end;
         TArrayRef = record // helpers
           class function Create<T>(AItems: array of T; Offset, Count: Integer); static;
           class function Create<T>(AItems: array of T; Count: Integer); static;
           class function Create<T>(AItems: array of T); static;
         end; 
      
      procedure WorkWithArray(const anArray : TArrayRef<Integer>);
      var
        I: Integer;
      begin
        for I := 0 to anArray.Count - 1 do WriteLn(anArray[I]);
      end;
      
      WorkWithArray(TArrayRef.Create(StaticArray, 3)); // first three items
      WorkWithArray(TArrayRef.Create(DynArray, 10, 3)); 
      

      【讨论】:

        【解决方案3】:

        已更新请参阅下面的泛型解决方案。

        这是一种替代方法,它将偏移量所需的类型转换封装在函数内,该函数驻留在声明为类函数的高级记录中。 除了隐藏类型转换之外,偏移量还会根据数组的高索引进行范围检查。

        如果需要,可以添加更多类型。

        Type
          SubRange = record
            Type
              TIntLongArray = array[0..MaxInt div SizeOf(Integer) - 1] of integer;
              PIntLongArray = ^TIntLongArray;
              TByteLongArray = array[0..MaxInt div SizeOf(Byte) - 1] of Byte;
              PByteLongArray = ^TByteLongArray;
        
            class function Offset( const anArray : array of Integer; 
                                         offset  : Integer) : PIntLongArray; overload; static;
            class function Offset( const anArray : array of Byte; 
                                         offset  : Integer) : PByteLongArray; overload; static;
            // ToDo: Add more types ...
          end;
        
        class function SubRange.Offset(const anArray : array of Integer; 
                                             offset  : Integer): PIntLongArray;
        begin
          Assert(offset <= High(anArray));
          Result := PIntLongArray(@anArray[offset]);
        end;
        
        class function SubRange.Offset(const anArray : array of Byte; 
                                             offset  : Integer): PByteLongArray;
        begin
          Assert(offset <= High(anArray));
          Result := PByteLongArray(@anArray[offset]);
        end;
        

        注意:偏移量和切片的总和不能超过数组的元素个数。

        示例调用:

        WorkWithArray( Slice(SubRange.Offset(staticArray,1)^,2));
        WorkWithArray( Slice(SubRange.Offset(dynArray,1)^,2));
        WorkWithArray( Slice(SubRange.Offset(dynArrayG,1)^,2));
        

        虽然这看起来更好,但我仍然不相信这是最佳解决方案。


        更新

        在编写上述解决方案时,我有一个泛型解决方案作为最终目标。

        这是一个利用匿名方法和泛型来实现Slice(anArray,startIndex,Count) 的答案 可用于静态和动态数组的方法。

        直接的泛型解决方案将依赖于在使用它的每个位置关闭范围检查, 这不是一个很好的解决方案。 原因是SizeOf(T)不能用于声明最大大小的静态数组类型:

        TGenericArray = array[0..MaxInt div SizeOf(T) - 1] of T; // SizeOf(T) 未解析

        所以我们必须使用:

        TGenericArray = T 的数组[0..0];

        相反。这会在打开时触发范围检查,索引 > 0。

        解决方案

        但问题可以通过另一种策略来解决,callbacks 或更现代的术语是 Inversion of Control (IoC) 或 Dependeny Injection (DI)。 最好用“不要打电话给我,我们打电话给你”来解释这个概念。

        我们不使用直接函数,而是将操作代码作为匿名方法与所有参数一起传递。 现在范围检查问题包含在Slice&lt;T&gt; 框架内。

        Slice<Integer>.Execute(
          procedure(const arr: array of Integer)
          begin
            WriteLn(Math.SumInt(arr));
          end, dArr, 2, 7);
        

        unit uGenericSlice;
        
        interface
        
        type
          Slice<T> = record
          private
            type
              PGenericArr = ^TGenericArr;
              TGenericArr = array [0..0] of T;
          public
            type
              TConstArrProc = reference to procedure(const anArr: array of T);
            class procedure Execute(       aProc: TConstArrProc;
                                     const anArray: array of T;
                                           startIndex,Count: Integer); static;
          end;
        
        implementation
        
        class procedure Slice<T>.Execute(aProc: TConstArrProc;
          const anArray: array of T; startIndex, Count: Integer);
        begin
          if (startIndex <= 0) then
            aProc(Slice(anArray, Count))
          else
          begin
            // The expression PGenericArr(@anArray[startIndex]) can trigger range check error
            {$IFOPT R+}
              {$DEFINE RestoreRangeCheck}
              {$R-}
            {$ENDIF}
            Assert((startIndex <= High(anArray)) and (Count <= High(anArray)-startIndex+1),
              'Range check error');
            aProc(Slice(PGenericArr(@anArray[startIndex])^, Count));
            {$IFDEF RestoreRangeCheck}
              {$UNDEF RestoreRangeCheck}
              {$R+}
            {$ENDIF}
          end;
        end;
        
        end.
        

        以下是一些示例用例:

        program ProjectGenericSlice;
        
        {$APPTYPE CONSOLE}
        
        uses
          Math,
          uGenericSlice in 'uGenericSlice.pas';
        
        function Sum(const anArr: array of Integer): Integer;
        var
          i: Integer;
        begin
          Result := 0;
          for i in anArr do
            Result := Result + i;
        end;
        
        procedure SumTest(const arr: array of integer);
        begin
          WriteLn(Sum(arr));
        end;
        
        procedure TestAll;
        var
          aProc: Slice<Integer>.TConstArrProc;
          dArr: TArray<Integer>;
          mySum: Integer;
        const
          sArr: array [1 .. 10] of Integer = (
            1,2,3,4,5,6,7,8,9,10);
        
        begin
          dArr := TArray<Integer>.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
          aProc :=
            procedure(const arr: array of Integer)
            begin
              WriteLn(Sum(arr));
            end;
        
          // Test predefined anonymous method
          Slice<Integer>.Execute( aProc, dArr, 2, 7);
        
          // Test inlined anonymous method
          Slice<Integer>.Execute(
            procedure(const arr: array of Integer)
            begin
              WriteLn(Sum(arr));
            end, dArr, 2, 7);
        
          // Test call to Math.SumInt
          Slice<Integer>.Execute(
            procedure(const arr: array of Integer)
            begin
              WriteLn(Math.SumInt(arr));
            end, dArr, 2, 7);
        
          // Test static array with Low(sArr) > 0
          Slice<Integer>.Execute(
            procedure(const arr: array of Integer)
            begin
              WriteLn(Sum(arr));
            end, sArr, 3 - Low(sArr), 7);
        
          // Using a real procedure
          Slice<Integer>.Execute(
            SumTest, // Cannot be nested inside TestAll
            dArr, 2, 7);
        
          // Test call where result is passed to local var
          Slice<Integer>.Execute(
            procedure(const arr: array of Integer)
            begin
              mySum := Math.SumInt(arr);
            end, dArr, 2, 7);
          WriteLn(mySum);
        
        end;
        
        begin
          TestAll;
          ReadLn;
        end.
        

        【讨论】:

        • 为什么不简单地重新实现Slice 来接受动态数组?
        • @user3764855, Slice 是一个由编译器插入的魔术函数(内在),而不是一个正常的重载或改变函数。
        • 你不能只返回指向数组的指针而不是使用这些匿名方法。
        • 是的,问题中已经说明了这一点,但我想要一种没有类型转换的更好方法。
        • 你能用 typecast 发布解决方案吗?谢谢。
        猜你喜欢
        • 1970-01-01
        • 2021-12-27
        • 2013-08-12
        • 2014-04-06
        • 1970-01-01
        • 2021-10-26
        • 2012-04-20
        • 2018-01-02
        • 1970-01-01
        相关资源
        最近更新 更多