给定一组记录,我如何获得一个表示每个记录的数组的数组?

时间:2012-08-08 07:15:54

标签: arrays delphi delphi-2010 record projection

我必须翻译一些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中有一些我错过的语言功能。

3 个答案:

答案 0 :(得分:8)

使用扩展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;

答案 1 :(得分:7)

我发布这个作为答案,因为评论有点过于局限于此。

这个答案试图解释FORTRAN和Delphi中数组和记录的内存布局的差异,并修改answer Todd Grigsbyanswer Remy Lebeau(I {}赞成两者。)

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
  3. 的WallInfo记录实例数组中的信息中复制它们
  4. 将它们发送给函数
  5. 如果这些是var参数:将两个数组中的更改复制回Wall
  6. Todd GrigsbyRemy Lebeau使用直接Delphi代码或Delphi记录RTTI显示了他们答案的前三个步骤。
    第4步以类似的方式工作。

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

    注意(再次):在将算法从FORTRAN转换为Delphi时,请确保在列中/行主要更改时观察数组中的循环和其他索引。

答案 2 :(得分:2)

要回答你的问题,不,没有语言构造或便利方法将单个列从记录数组拆分成一个简单的数组。

我会推荐以下内容:

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;