【问题标题】:Cast static array to open array of different element type将静态数组转换为打开不同元素类型的数组
【发布时间】:2015-07-22 07:59:08
【问题描述】:

(我已经在 CodeReview 询问过这个问题,它在哪里被关闭为题外话。希望它在这里成为话题。)

我有一个派生类型的静态数组(如以下示例代码中的LabelsA: array[0..3] of TLabel;)和一个接受基本类型的开放数组(如procedure DoSomethingWithControls(const AControls: array of TControl);)的例程,我想调用DoSomethingWithControls那些静态数组。请看我的示例:

procedure DoSomethingWithControls(const AControls: array of TControl);
var
  i: Integer;
begin
  for i := Low(AControls) to High(AControls) do
    Writeln(AControls[i].Name);
end;

procedure Test;
var
  LabelsA: array[0..3] of TLabel;
  LabelsB: array[0..1] of TLabel;

  procedure Variant1;
  type
    TArray1 = array[Low(LabelsA)..High(LabelsA)] of TControl;
    TArray2 = array[Low(LabelsB)..High(LabelsB)] of TControl;
  begin
    DoSomethingWithControls(TArray1(LabelsA));
    DoSomethingWithControls(TArray2(LabelsB));
  end;

  procedure Variant2;
  type
    TControlArray = array[0..Pred(MaxInt div SizeOf(TControl))] of TControl;
    PControlArray = ^TControlArray;
  begin
    DoSomethingWithControls(Slice(PControlArray(@LabelsA)^, Length(LabelsA)));
    DoSomethingWithControls(Slice(PControlArray(@LabelsB)^, Length(LabelsB)));
  end;

  procedure Variant3;
  var
    ControlsA: array[Low(LabelsA)..High(LabelsA)] of TControl absolute LabelsA;
    ControlsB: array[Low(LabelsB)..High(LabelsB)] of TControl absolute LabelsB;
  begin
    DoSomethingWithControls(ControlsA);
    DoSomethingWithControls(ControlsB);
  end;

begin
  Variant1;
  Variant2;
  Variant3;
end;

调用DoSomethingWithControls有一些可能的变体:

  • 变体 1 非常简单,但需要像 TArray1 这样的“适配器”类型 对于每个大小的 TLabel 数组。我希望它更灵活。

  • 变体 2 更加灵活和统一,但丑陋且容易出错。

  • 变体 3(由 TOndrej) 类似于 变体 1 - 它不需要显式转换,但变体 1 提供了 如果你搞砸了一些东西(例如 复制粘贴时数组边界出错)。

有什么想法可以在没有这些缺点的情况下制定这些调用(不改变数组的元素类型)?它应该适用于 D2007 和 XE6。

【问题讨论】:

  • 为什么不使用 TObjectList.ToArray?
  • @whosrdaddy:对不起,忘了提到 D2007。 :-/
  • 我很惊讶你不能只传递TLabel的数组
  • @David:是的,我想这就是 co(ntra?)-variance 的事情。 :-)
  • @J... 当您将静态数组传递给开放数组参数时,编译器会传递长度信息

标签: arrays delphi casting open-array-parameters


【解决方案1】:

这些演员都很丑。它们都会起作用,但使用它们会让你感到肮脏​​。使用辅助函数是完全合理的:

type
  TControlArray = array of TControl;

function ControlArrayFromLabelArray(const Items: array of TLabel): TControlArray;
var 
  i: Integer;
begin
  SetLength(Result, Length(Items));
  for i := 0 to high(Items) do
    Result[i] := Items[i];
end;

然后你像这样调用你的函数:

DoSomethingWithControls(ControlArrayFromLabelArray(...));

当然,如果您可以使用泛型,这将更加简洁。

【讨论】:

  • 我的强迫症反应是抱怨(我知道可以忽略不计)内存分配和动态数组的复制开销,理想情况下是不需要的。如果可以返回一个像Slice 这样的开放数组(在编译器的帮助下),那就太好了。 :-)
  • 您还可以为DoSomethingWithControls 进行重载,使用array of TLabel 使其更整洁。
  • 我同意堆分配,但这是 GUI 代码,肯定不会成为您的瓶颈。
  • @UliGerhardt 我会检查强迫症,因为premature optimization 不好。
  • @NewWorld,我试试。但是,如果有一个漂亮、优雅、零开销的解决方案不是很好吗? ;-)
【解决方案2】:

也不是特别漂亮,但你可以这样欺骗编译器:

procedure Variant3;
var
  ControlsA: array[Low(LabelsA)..High(LabelsA)] of TControl absolute LabelsA;
begin
  DoSomethingWithControls(ControlsA);
end;

【讨论】:

  • 谢谢,@TOndrej!不幸的是,这具有与 Variant1 相同的缺点 - 我必须为每次调用声明一个 absolute 变量。
  • 我同意这是一个缺点,但严格来说,它与Variant1 不同。你只是说你想避免声明一个新的适配器类型。 ;-)
  • 好的,Variant3 有一个附加 缺点:即使我弄错了数组大小,它也能编译。 Variant1 无法编译。 ;-)
  • 如果我在Variant1 中错误地写了TArray2 = array[Low(LabelsA)..High(LabelsA)] of TControl;(注意A 而不是正确的B)我得到“E2089 Invalid typecast”。 ControlsB: array[Low(LabelsA)..High(LabelsA)] of TControl absolute LabelsB;(同样的错误!)愉快地编译。
  • IMO, absolute 应该从语言中删除。
【解决方案3】:

声明一个重载的过程:

procedure DoSomethingWithControls(const AControls: array of TControl); overload;
var
  i: Integer;
begin
  for i := 0 to High(AControls) do
    if Assigned(AControls[i]) then
       Writeln(AControls[i].Name)
    else
      WriteLn('Control item: ',i);
end;

procedure DoSomethingWithControls(const ALabels: array of TLabel); overload;
type
  TControlArray = array[0..Pred(MaxInt div SizeOf(TControl))] of TControl;
  PControlArray = ^TControlArray;
begin
  DoSomethingWithControls(Slice(PControlArray(@ALabels)^, Length(ALabels)));
end;

这是您的变体 2 的通用解决方案。一个声明适用于所有情况,因此不易出错。

【讨论】:

    【解决方案4】:

    以下示例基于开放数组参数的内部实现方式。但是,它不适用于“键入的 @ 运算符”。

      procedure Variant4;
      type
        TCallProc = procedure (AControls: Pointer; HighBound: Integer);
      var
        CallProc: TCallProc;
      begin
        CallProc := @DoSomethingWithControls;
    
        CallProc(@LabelsA, Length(LabelsA) - 1);
        CallProc(@LabelsB, Length(LabelsB) - 1);
      end;
    

    只要所有静态数组都是基于 0 的,为 HighBound 传递 High(Labels) 可能会更好。

    【讨论】:

    • 嗯,它可以工作,但它并不优雅。顺便说一句,为什么不使用 high(..) 而不是 Length(..) - 1.
    • @David - 如果传递了基于非零的静态数组。否则,我同意正如我在最后一句中所说的那样,高更好。
    • 谢谢,我明白了
    【解决方案5】:

    由于动态数组可以作为开放数组传递给方法,因此可以选择将静态数组转换为动态数组。

    如果您不介意复制数组的开销,请考虑以下事项:

    编写一个函数,将一个开放的标签数组转换为一个动态的 TControlArray 数组。

    type
      TControlArray = array of TControl;
    
    {$IFOPT R+} {$DEFINE R_ON} {$R-} {$ENDIF}
    function MakeControlArray(const ALabels: array of TLabel): TControlArray;
    begin
      SetLength(Result, Length(ALabels));
      Move(ALabels[0], Result[0], Length(ALabels) * SizeOf(TObject));
    end;
    {$IFDEF R_ON} {$R+} {$UNDEF R_ON} {$ENDIF}
    

    现在Variant4可以写成:

    procedure Variant4;
    begin
      DoSomethingWithControls(MakeControlArray(LabelsA));
      DoSomethingWithControls(MakeControlArray(LabelsB));
    end;
    

    测试用例:

    procedure TAdHocTests.TestLabelsToControls;
    const
      LLabelsA: array[0..3] of TLabel = (Pointer(0),Pointer(1),Pointer(2),Pointer(3));
    var
      LLoopI: Integer;
      LLabelsB: array[0..9] of TLabel;
      LEmptyArray: TLabelArray;
    begin
      for LLoopI := Low(LLabelsB) to High(LLabelsB) do
      begin
        LLabelsB[LLoopI] := Pointer(LLoopI);
      end;
    
      DoSomethingWithControls(MakeControlArray(LLabelsA), Length(LLabelsA));
      DoSomethingWithControls(MakeControlArray(LLabelsB), Length(LLabelsB));
      DoSomethingWithControls(MakeControlArray([]), 0);
      DoSomethingWithControls(MakeControlArray(LEmptyArray), 0);
    end;
    
    procedure TAdHocTests.DoSomethingWithControls(
        const AControls: array of TControl;
        AExpectedLength: Integer);
    var
      LLoopI: Integer;
    begin
      CheckEquals(AExpectedLength, Length(AControls), 'Length incorrect');
      for LLoopI := Low(AControls) to High(AControls) do
      begin
        CheckEquals(LLoopI, Integer(AControls[LLoopI]));
      end;
    end;
    

    【讨论】:

    • @DavidHeffernan True,传递[] 或空动态数组可能会导致范围错误。但这很容易通过禁用专门针对该功能的范围检查来解决。此外,在指定的用例中,为什么(以及如何)声明一个零长度静态数组?
    • 您的函数不能在静态数组上运行。它接受一个开放数组并返回一个动态数组。您的编辑现在启用此函数下方的所有代码的范围检查。不是需要的。
    • @DavidHeffernan 我可以指出,如果我的函数不能在静态数组上运行,那么你的也不能。 (注意:你应该更仔细地检查你的函数......你会发现它甚至不会编译。)......但是,也许你应该看一下 DUnit我添加的测试用例并详细说明了我的函数如何“不在静态数组上运行”?。
    • 你误会了。您的函数接受的不仅仅是静态数组。它当然可以传递零长度数组。
    • 在这种情况下,类型检查有点学术性。我们都知道 TLabel 是从 TControl 派生的。但通常最好让编译器验证这些东西。我认为使用循环没有任何害处。性能不会是问题。与 Move 不同,循环将在 ARC 下工作。使用 Move 有什么好处?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-01-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多