【问题标题】:Given an array of records, how can I get an array representing a field from each one?给定一个记录数组,如何从每个记录中获取一个表示字段的数组?
【发布时间】:2012-08-08 07:15:53
【问题描述】:

我不得不翻译一些 Fortran 90 代码,发现了一个有趣的语言特性。

例如,它们定义了以下类型和动态数组变量:

TYPE WallInfo
  CHARACTER(len=40) :: Name
  REAL              :: Azimuth
  REAL              :: Tilt
  REAL              :: Area
  REAL              :: Height
END TYPE WallInfo

TYPE(WallInfo), ALLOCATABLE, DIMENSION(:) :: Wall

在代码的后面,他们调用了一个函数:

CALL HeatFlow(Wall%Area, Wall%Azimuth)

作为一个 Delphi 程序员,这让我有点吃惊,因为 Wall 是一个记录数组!

从例程中的用法可以看出,Fortran 可以将记录数组中的字段投影为自己的数组。

SUBROUTINE HeatFlow( Area, Azimuth )
  REAL, INTENT(IN), DIMENSION(:) :: Area
  REAL, INTENT(IN), DIMENSION(:) :: Azimuth

有谁知道是否有办法用 Delphi 做到这一点(我使用的是 2010 版)?

我可以编写一个函数来将记录值提取为一个数组,但这有点乏味,因为我必须为每个字段编写一个专用例程(而且有很多)。

我希望 Delphi 2010 中有一些我错过的语言功能。

【问题讨论】:

  • 您是否尝试在 WallInfo 的案例数组中使用数组或记录? Delphi 支持动态数组。在输入新值之前,首先使用 SetLength 设置数组的大小。
  • 尽管 Remy 的 RTTI 答案很酷,但我很想将上面的记录类型转换为单独的线性数组:'Name:Array of String;方位角:双精度数组; ...' 然后我就不必使用这个可爱的 RTTI hack 来收集数据,因为它已经被收集了。

标签: arrays delphi delphi-2010 record projection


【解决方案1】:

使用扩展 RTTI,可以创建一个通用函数,该函数将数组和字段名称作为输入,并使用数组的 RTTI 仅提取该字段的值并使用它们创建一个包含正确数据的新数组类型。

以下代码在 XE2 中适用于我:

uses
  System.SysUtils, System.Rtti;

type
  FieldArray<TArrElemType, TFieldType> = class
  public
    class function Extract(const Arr: TArray<TArrElemType>; const FieldName: String): TArray<TFieldType>;
  end;

class function FieldArray<TArrElemType, TFieldType>.Extract(const Arr: TArray<TArrElemType>; const FieldName: String): TArray<TFieldType>;
var
  Ctx: TRttiContext;
  LArrElemType: TRttiType;
  LField: TRttiField;
  LFieldType: TRttiType;
  I: Integer;
begin
  Ctx := TRttiContext.Create;
  try
    LArrElemType := Ctx.GetType(TypeInfo(TArrElemType));
    LField := LArrElemType.GetField(FieldName);
    LFieldType := Ctx.GetType(TypeInfo(TFieldType));
    if LField.FieldType <> LFieldType then
      raise Exception.Create('Type mismatch');
    SetLength(Result, Length(Arr));
    for I := 0 to Length(Arr)-1 do
    begin
      Result[I] := LField.GetValue(@Arr[I]).AsType<TFieldType>;
    end;
  finally
    Ctx.Free;
  end;
end;

.

type
  WallInfo = record
    Name: array[0..39] of Char;
    Azimuth: Real;
    Tilt: Real;
    Area: Real;
    Height: Real;
  end;

procedure HeatFlow(const Area: TArray<Real>; const Azimuth: TArray<Real>);
begin
  // Area contains (4, 9) an Azimuth contains (2, 7) as expected ...
end;

var
  Wall: TArray<WallInfo>;
begin
  SetLength(Wall, 2);

  Wall[0].Name := '1';
  Wall[0].Azimuth := 2;
  Wall[0].Tilt := 3;
  Wall[0].Area := 4;
  Wall[0].Height := 5;

  Wall[1].Name := '6';
  Wall[1].Azimuth := 7;
  Wall[1].Tilt := 8;
  Wall[1].Area := 9;
  Wall[1].Height := 10;

  HeatFlow(
    FieldArray<WallInfo, Real>.Extract(Wall, 'Area'),
    FieldArray<WallInfo, Real>.Extract(Wall, 'Azimuth')
    );
end;

【讨论】:

  • 谢谢你的帮助,我要试试这个方法。我开始意识到直接翻译并不是那么简单。 Fortran 似乎有一些不错的矩阵特性,但试图在 Delphi 中模拟这些特性并不是很有效。我没有传递数组,而是在一个参数中简单地传递了整个墙数组。
  • Remmy,我不得不将循环更改为 for I := 0 to Length(Arr)-1 do 以使其在 Delphi 2010 中工作。我印象深刻,它确实解决了问题并且是很整洁。
  • 代码是在 XE2 中编写和测试的。您是说High() 不适用于 D2010 中的动态数组吗?我用这个变化更新了我的答案。
  • @DavidHeffernan:问题是关于模拟 Delphi 中的等效功能,而不是其性能的效率。
  • 当然,但很可能性能很重要。
【解决方案2】:

我将其发布为答案,因为 cmets 有点太有限,无法表达这个。

此答案试图解释 FORTRAN 和 Delphi 中数组和记录的内存布局的差异,并将 answer 修改为 Todd Grigsbyanswer 修改为 Remy Lebeau(我都赞成)。

FORTRAN 和其他一些以计算为中心的语言将嵌套数组存储在 column major order 中。 Delphi 和许多其他语言使用row major order

从内存的角度来看,一条记录只不过是一个字段数组:

  • 有名字而不是索引
  • 可能有不同的类型

对于计算密集型操作,当您的算法有利于列时,存储嵌套数组的列主顺序可能是有意义的。与行主要订单相同。所以在循环中,你需要match the order of your indexes with the order of your storage

鉴于此记录和 FORTRAN 中的数组定义:

TYPE WallInfo
  CHARACTER(len=40) :: Name
  REAL              :: Azimuth
  REAL              :: Tilt
  REAL              :: Area
  REAL              :: Height
END TYPE WallInfo

TYPE(WallInfo), ALLOCATABLE, DIMENSION(:) :: Wall

以及 Delphi 中的等效功能定义:

type
  WallInfo = record
    Name: array[0..39] of Char;
    Azimuth: Real;
    Tilt: Real;
    Area: Real;
    Height: Real;
  end;

var
  Wall: array of WallInfo;

和一个由 3 个 WallInfo 元素组成的数组, 这就是内存布局的样子(它们都是连续的内存区域,我将它们分成几行以保持可读性):

在 FORTRAN 中:

Name[0,0]...Name[0,39], Name[1,0]...Name[1,39], Name[2,0]...Name[2,39],
Azimuth[0], Azimuth[1], Azimuth[2],
Tilt[0], Tilt[1], Tilt[2],
Area[0], Area[1], Area[2],
Height[0], Height[1], Height[2],

在德尔福中:

Name[0,0]...Name[0,39], Azimuth[0], Tilt[0], Area[0], Height[0],
Name[1,0]...Name[1,39], Azimuth[1], Tilt[1], Area[1], Height[1],
Name[2,0]...Name[2,39], Azimuth[2], Tilt[2], Area[2], Height[2],

所以这个 FORTRAN 调用:

CALL HeatFlow(Wall%Area, Wall%Azimuth)

只会将指向 Area[0] 和 Azimuth[0] 内存位置的指针以及这些内存区域的长度传递给函数。

在Delphi中,这是不可能的,所以你必须

  1. 构建新的区域和方位角阵列
  2. 从名为 Wall 的 WallInfo 记录实例数组中的信息中复制它们
  3. 将它们发送到函数
  4. 如果这些是 var 参数:将两个数组中的更改复制回 Wall

Todd GrigsbyRemy Lebeau 使用直接的 Delphi 代码或 Delphi 记录 RTTI 显示了他们答案中的前三个步骤。
第 4 步的工作方式类似。

这两种解决方案都使用了 Delphi 2009 中引入的泛型。
Until Delphi 2010, RTTI on records was very minimal),因此您得到了适用于这两个答案的正确 Delphi 版本。

注意(再次):将您的算法从 FORTRAN 转换为 Delphi 时,请确保注意数组中的循环和其他索引,因为列/行发生重大变化。

【讨论】:

  • Jeroen,感谢您关于循环索引的 cmets。我已经偶然发现了这个问题。我不知道 Delphi 和 Fortran 之间数组顺序的差异。现在更有意义了。
  • 我很高兴它有助于获得更多洞察力。继续提出好问题!
  • 我在问题中看不到任何与行主要和列主要问题相关的内容。它们出现在问题的什么地方?在我看来,问题是关于支持投影的 Fortran 设施。这可能会通过一个跨步技术(一个 numpy 数组)来实现。
  • 问题中隐含了主要/次要。潜在的问题是如何在 Delphi 中获得投影。在 Fortran 中,由于在 Fortran 中分配内存的方式,您不需要创建自己的投影。 Fortran 中的隐式投影还为您提供了参考(因此您可以读写)。如果你想在 Delphi 中阅读,你需要以一种方式进行投影。如果你想用 Delphi 编写,你必须用两种方式进行投影。我的回答解释了 Fortran 和 Delphi 之间的差异,从而解释了为什么您需要一些代码才能在 Delphi 中完成所有这些工作。
  • 感谢您将我指向 NumPy,numpy.Ravel 很有趣 docs.scipy.org/doc/numpy-1.5.x/reference/generated/… 和 strides scipy-lectures.github.com/advanced/advanced_numpy/… 您实际上可能会通过一组指向底层字段的指针来获得读/写访问权限.在某些情况下可能会更快。
【解决方案3】:

要回答您的问题,不,没有语言构造或方便的方法可以将记录数组中的单个列拆分为自己的简单数组。

我会推荐如下内容:

function SplitColumn( RecordArray : Array of {recordtype} ) : Array of {columntype};
var
  column : array of {type};
  x : Integer;
begin
  setlength( result, high( RecordArray ) + 1 );
  for x := 0 to high( RecordArray ) do
    result[ x ] := RecordArray[ x ].{columnname};
end;

如果你想使用动态数组。就个人而言,如果您要移植它,我会使用 List 和 List,如下所示:

type
   TWallList = class( TList<TWallInfo> );
   TDoubleList = class( TList<Double> );

function SplitColumn( WallList : TWallList; AreaList, AzimuthList : TDoubleList ); 
var
  x : Integer;
begin
  for x := 0 to RecList.Count-1 do
  begin
    AreaList.add( RecordArray[ x ].Area );
    Azimuth.add( RecordArray[ x ].Azimuth );
  end;
end;

【讨论】:

  • 使用扩展 RTTI,可以创建一个通用函数,将数组和字段名称作为输入,并使用数组的 RTTI 提取该字段的值并创建一个新数组他们。
  • 我发布了一个答案,其中包含扩展 RTTI 的演示。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-15
  • 2023-01-17
  • 1970-01-01
  • 2014-08-29
  • 2021-08-14
  • 2010-12-31
  • 1970-01-01
相关资源
最近更新 更多