用DELPHI的RTTI实现数据集的简单对象化(zt)

本文介绍了一种使用DELPHI RTTI实现数据集简单对象化的方案,通过定义代理类,将数据集操作转化为对象操作,提高了代码可读性和减少了潜在错误。这种方法简化了传统数据集操作的冗长代码,提供了编译时字段名校验,增强了程序的健壮性。
 

DELPHI的RTTI实现数据集的简单对象化
[Mental Studio]猛禽

在《强大的DELPHI RTTI--兼谈需要了解多种开发语言》一文中,我说了一下我用DELPHI的RTTI实现了数据集的简单对象化。本文将详细介绍一下我的实现方法。

首先从一个简单的例子说起:假设有一个ADODataSet控件,连接罗斯文数据库,SQL为:

select * from Employee

现在要把它的内容中EmployeeID, FirstName, LastName,BirthDate四个字段显示到ListView里。传统的代码如下:

    //先要设置AdoDataSet的CommandText属性
    With ADODataSet1 Do
    Begin
        Open;
        While Not Eof Do
        Begin
            With ListView1.Add Do
            Begin
                Caption := IntToStr( FieldByName( 'EmployeeID' ).AsInteger );
                SubItems.Add( FieldByName( 'FirstName' ).AsString );
                SubItems.Add( FieldByName( 'LastName' ).AsString );
                SubItems.Add( FormatDateTime( FieldByName( 'BirthDate' ).AsDateTime ) );
            End;
            Next;
        End;
        Close;
    End;
这里主要存在几个方面的问题:

1、首先是有很多代码非常冗长。比如FieldByName和AsXXX等,特别是AsXXX,必须时时记得每个字段是什么类型的,很容易搞错。而且有些不兼容的类型如果不能自动转换的话,要到运行时才能发现错误。

2、需要自己在循环里处理当前记录的移动。如上面的Next,否则一旦忘记就会发生死循环,虽然这种问题很容易发现并处理,但程序员不应该被这样的小细节所纠缠。

3、最主要的是字段名通过String参数传递,如果写错的话,要到运行时才会发现,增加了潜在的BUG可能性,特别是如果测试没有完全覆盖所有的FieldByName,很可能使这样的问题拖到客户那边才会出现。而这种写错字段名的情况是很容易发生的,特别是当程序使用了多个表时,还容易将不同表的字段名搞混。

在这个由OO统治的时代里,碰到与数据集有关的操作时,我们还是不得不常常陷入上面说的这些关系数据库方面的细节问题中。当然现在也有摆脱它们的办法,那就是O/R mapping,但是O/R mapping毕竟与传统的开发方式差别太大,特别是对于一些小的应用来说,没必要这么夸张,在这种情况下,我们需要的只是一个简单的数据集对象化方案。


在JAVA及其它动态语言的启发下,我想到了用DELPHI强大的RTTI来实现这个简单的数据集对象化方案。下面是实现与传统代码同样功能的数据集对象化应用代码:

Type
    TDSPEmployee = class(TMDataSetProxy)
    published
        Property EmployeeID : Integer Index 0 Read GetInteger Write SetInteger;
        Property FirstName  : String  Index 1 Read GetString  Write SetString;
        Property LastName   : String  Index 2 Read GetString  Write SetString;
        Property BirthDate  : Variant Index 3 Read GetVariant Write SetVariant;
    end;

procedure TForm1.ListClick(Sender: TObject);
Var
    emp : TDSPEmployee;
begin
   //先要设置AdoDataSet1的CommandText属性
    emp := TDSPEmployee.Create( ADODataSet1 );
    Try
        While ( emp.ForEach ) Do
        With ListView1.Items.Add Do
        Begin
            Caption := IntToStr( emp.EmployeeID );
            SubItems.Add( emp.FirstName );
            SubItems.Add( emp.LastName );
            SubItems.Add( FormatDateTime( 'yyyy-mm-dd', TDateTime( emp.BirthDate ) ) );
        End;
    Finally
        emp.Free;
    End;
end;
用法很简单。最主要的是要先定义一个代理类,其中以Published的属性来定义所有的字段,包括其类型,之后就可以以对象的方式来操作数据集了。这个代理类是从TMDataSetProxy派生来的,其中用RTTI实现了从属性操作到字段操作的映射,使用时只要简单地Uses一下相应的单元即可。关于这个类的实现单元将在下面详细说明。

表面上看多了一个定义数据集的代理类,好像多了一些代码,但这是一件一劳永逸的事,特别是当程序中需要多次重用同样结构的数据集的情况下,将会使代码量大大减少。更何况这个代理类的定义非常简单,只是根据字段名和字段类型定义一系列的属性罢了,不用任何实现代码。其中用到的属性存取函数GetXXX/SetXXX都在基类TMDataSetProxy里实现了。

现在再来看那段与原代码对应的循环:

1、FieldByName和AsXXX都不需要了,变成了对代理类的属性操作,而且每个字段对应的属性的类型在前面已经定义好了,不用再每次用到时来考虑一下它是什么类型的。如果用错了类型,在编译时就会报错。

2、用一个ForEach来进行记录遍历,不用再担心忘记Next造成的死循环了。

3、最大的好处是字段名变成了属性,这样就可以享受到编译时字段名校验的好处了,除非是定义代理类时就把字段名写错,否则都能在编译时发现。


现在开始讨论TMDataSetProxy。其实现的代码如下:

(******************************************************************
用RTTI实现的数据集代理,可以简单地将数据集对象化。
Copyright (c) 2005 by Mental Studio.
Author : 猛禽
Date   : Jan.28-05
******************************************************************)
unit MDSPComm;
interface
Uses
    Classes, DB, TypInfo;
Type
    TMPropList = class(TObject)
    private
        FPropCount : Integer;
        FPropList  : PPropList;
    protected
        Function GetPropName( aIndex : Integer ) : ShortString;
        function GetProp(aIndex: Integer): PPropInfo;
    public
      constructor Create( aObj : TPersistent );
      destructor  Destroy; override;
      property PropCount : Integer Read FPropCount;
      property PropNames[aIndex : Integer] : ShortString Read GetPropName;
      property Props[aIndex : Integer] : PPropInfo Read GetProp;
    End;
    TMDataSetProxy = class(TPersistent)
    private
        FDataSet  : TDataSet;
        FPropList : TMPropList;
        FLooping  : Boolean;
    protected
        Procedure BeginEdit;
        Procedure EndEdit;
        Function  GetInteger( aIndex : Integer ) : Integer; Virtual;
        Function  GetFloat(   aIndex : Integer ) : Double;  Virtual;
        Function  GetString(  aIndex : Integer ) : String;  Virtual;
        Function  GetVariant( aIndex : Integer ) : Variant; Virtual;
        Procedure SetInteger( aIndex : Integer; aValue : Integer ); Virtual;
        Procedure SetFloat(   aIndex : Integer; aValue : Double  ); Virtual;
        Procedure SetString(  aIndex : Integer; aValue : String  ); Virtual;
        Procedure SetVariant( aIndex : Integer; aValue : Variant ); Virtual;
    public
      constructor Create( aDataSet : TDataSet );
      destructor  Destroy; override;
      Procedure AfterConstruction; Override;
      function  ForEach : Boolean;
      Property DataSet : TDataSet Read FDataSet;
    end;
implementation
{ TMPropList }
constructor TMPropList.Create(aObj: TPersistent);
begin
    FPropCount := GetTypeData(aObj.ClassInfo)^.PropCount;
    FPropList  := Nil;
    if FPropCount > 0 then
    begin
        GetMem(FPropList, FPropCount * SizeOf(Pointer));
        GetPropInfos(aObj.ClassInfo, FPropList);
    end;
end;
destructor TMPropList.Destroy;
begin
    If Assigned( FPropList ) Then
        FreeMem( FPropList );
    inherited;
end;
function TMPropList.GetProp(aIndex: Integer): PPropInfo;
begin
    Result := Nil;
    If ( Assigned( FPropList ) ) Then
        Result := FPropList[aIndex];
end;
function TMPropList.GetPropName(aIndex: Integer): ShortString;
begin
    Result := GetProp( aIndex )^.Name;
end;
{ TMRefDataSet }
constructor TMDataSetProxy.Create(aDataSet: TDataSet);
begin
    Inherited Create;
    FDataSet := aDataSet;
    FDataSet.Open;
    FLooping := false;
end;
destructor TMDataSetProxy.Destroy;
begin
    FPropList.Free;
    If Assigned( FDataSet ) Then
        FDataSet.Close;
    inherited;
end;
procedure TMDataSetProxy.AfterConstruction;
begin
    inherited;
    FPropList := TMPropList.Create( Self );
end;
procedure TMDataSetProxy.BeginEdit;
begin
    If ( FDataSet.State <> dsEdit ) AND ( FDataSet.State <> dsInsert ) Then
        FDataSet.Edit;
end;
procedure TMDataSetProxy.EndEdit;
begin
    If ( FDataSet.State = dsEdit ) OR ( FDataSet.State = dsInsert ) Then
        FDataSet.Post;
end;
function TMDataSetProxy.GetInteger(aIndex: Integer): Integer;
begin
    Result := FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsInteger;
end;
function TMDataSetProxy.GetFloat(aIndex: Integer): Double;
begin
    Result := FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsFloat;
end;
function TMDataSetProxy.GetString(aIndex: Integer): String;
begin
    Result := FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsString;
end;
function TMDataSetProxy.GetVariant(aIndex: Integer): Variant;
begin
    Result := FDataSet.FieldByName( FPropList.PropNames[aIndex] ).Value;
end;
procedure TMDataSetProxy.SetInteger(aIndex, aValue: Integer);
begin
    BeginEdit;
    FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsInteger := aValue;
end;
procedure TMDataSetProxy.SetFloat(aIndex: Integer; aValue: Double);
begin
    BeginEdit;
    FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsFloat := aValue;
end;
procedure TMDataSetProxy.SetString(aIndex: Integer; aValue: String);
begin
    BeginEdit;
    FDataSet.FieldByName( FPropList.PropNames[aIndex] ).AsString := aValue;
end;
procedure TMDataSetProxy.SetVariant(aIndex: Integer; aValue: Variant);
begin
    BeginEdit;
    FDataSet.FieldByName( FPropList.PropNames[aIndex] ).Value := aValue;
end;
function TMDataSetProxy.ForEach: Boolean;
begin
    Result := Not FDataSet.Eof;
    If FLooping Then
    Begin
        EndEdit;
        FDataSet.Next;
        Result := Not FDataSet.Eof;
        If Not Result Then
        Begin
            FDataSet.First;
            FLooping := false;
        End;
    End
    Else If Result Then
        FLooping := true;
end;
end.
其中TMPropList类是一个对RTTI的属性操作部分功能的封装。其功能就是利用DELPHI在TypInfo单元中定义的一些RTTI函数,实现为一个TPersistent的派生类维护其Published的属性列表信息。代理类就通过这个属性列表来取得属性名,并最终通过这个属性名与数据集中的相应字段进行操作。

TMDataSetProxy就是数据集代理类的基类。其最主要的部分就是在AfterConstruction里创建属性列表。

属性的操作在这里只实现了Integer, Double/Float, String, Variant这四种数据类型。如果需要,可以自己在此基础上派生自己的代理基类实现其它数据类型的实现,而且这几个已经实现的类型的属性操作实现都被定义为虚函数,也可以在派生基类里用自己的实现取代它。不过对于不是很常用的类型,建议可以定义实际的代理类时再实现。比如前面的例子中,假设TDateTime不是一个常用的类型,可以这样做:

    TDSPEmployee = class(TMDataSetProxy)
    protected
        function  GetDateTime(const Index: Integer): TDateTime;
        procedure SetDateTime(const Index: Integer; const Value: TDateTime);
    published
        Property EmployeeID : Integer Index 0 Read GetInteger Write SetInteger;
        Property FirstName  : String  Index 1 Read GetString  Write SetString;
        Property LastName   : String  Index 2 Read GetString  Write SetString;
        Property BirthDate  : TDateTime Index 3 Read GetDateTime Write SetDateTime;
    end;
{ TDSPEmployee }
function TDSPEmployee.GetDateTime(const Index: Integer): TDateTime;
begin
    Result := TDateTime( GetVariant( Index ) );
end;
procedure TDSPEmployee.SetDateTime(const Index: Integer;
  const Value: TDateTime);
begin
    SetVariant( Index, Value );
end;
这样下面就可以直接把BirthDate当作TDateTime类型使用了。

另外,利用这一点,还可以为一些自定义的特别的数据类型提供统一的操作。

另外,在所有的SetXXX之前都调用了一下BeginEdit,以避免忘记使用DataSet.Edit导致的运行时错误。

ForEach被实现成可以重复使用的,在每次ForEach完成一次遍历后,将当前记录移动最第一条记录上以备下次的循环。另外,在Next之前调用了EndEdit,自动提交所作的修改。


这个数据集对象化方案是一种很简单的方案,现在存在的最大的一个问题就是属性的Index参数必须严格按照属性在定义时的顺序,否则就会取错字段。这是因为DELPHI毕竟还是一种原生开发语言,调用GetXXX/SetXXX时区别同类型的不同属性的唯一途径就是通过Index,而这个Index参数是在编译时就确定地传给函数了,并没有一个动态的表来记录,所以只能采用现在这样的方法来将就。


---------------------------------------------------------------------------------
解释:

1.关于属性中的index specifier

index specifiers allow several properties to share the same access method while representing different values. An index

specifier consists of the directive index followed by an integer constant between -2147483647 and 2147483647. If a property

has an index specifier, its read and write specifiers must list methods rather than fields(如果使用了索引属性,则必须是存取

函数,而不是字段). For example,

type
  TRectangle = class
  private
    FCoordinates: array[0..3] of Longint;
    function GetCoordinate(Index: Integer): Longint;
    procedure SetCoordinate(Index: Integer; Value: Longint);
  public
    property Left: Longint index 0 read GetCoordinate write SetCoordinate;
    property Top: Longint index 1 read GetCoordinate write SetCoordinate;

    property Right: Longint index 2 read GetCoordinate write SetCoordinate;
    property Bottom: Longint index 3 read GetCoordinate write SetCoordinate;
    property Coordinates[Index: Integer]: Longint read GetCoordinate write SetCoordinate;
    ...
  end;

property Left: Longint index 0 read GetCoordinate write SetCoordinate; 表示属性Left使用
存取函数GetCoordinate, index 0 表示这个属性的索引是0, 并作为参数传入存取函数。

-----------------------------------------------------------------------------
2.改进

这个设计要求在建立TDSPEmployee对象时传入参数AdoDataSet1,里面必须填入CommandText,这个又涉及到数据表的细节了。
我觉得应该把Select字句封装在子类里面, 但留出Where字句的内容给客户(虽然封装不好,但灵活性较高)

所以在原先的定义中加了一个构造函数,多了一个criteria参数,就是where字句的内容
原先我想只传入criteria, 而不传入数据集,但发现要在类里面创建AdoDataSet,还要传入Connection,参数还是没有少,而且要动态生成会增加开销;所以还是传入现成的DataSet为好,可以设个专用的AdoDataset, 传入后修改它的CommandText。

-----------------------------------------------------------------------------------------
代码如下:


  TDSPEmployee = class(TMDataSetProxy)
  public
     constructor Create(aDataSet:TDataSet; criteria: String);
  published
      Property EmployeeID : Integer Index 0 Read GetInteger Write SetInteger;
      Property FirstName  : String  Index 1 Read GetString  Write SetString;
      Property LastName   : String  Index 2 Read GetString  Write SetString;
      Property BirthDate  : Variant Index 3 Read GetVariant Write SetVariant;
  end;

constructor TDSPEmployee.Create(aDataSet:TDataSet; criteria: String);
begin
  (aDataset as TAdoDataSet).CommandText:='select EmployeeID, FirstName, LastName, BirthDate from Employees'    //select 字句是固定的,最后可以由上面的属性来生成
   + criteria;
 
  inherited Create(aDataSet);
end;

------------------------------------------------------------------------------------------

在按Ctrl + Shift + C 来自动生成框架代码时,会在类的私有部分加上GetInteger这些函数的声明,要删掉它们,这是个bug,如下

  TDSPEmployee = class(TMDataSetProxy)
  private
    function GetInteger(const Index: Integer): Integer;
    function GetString(const Index: Integer): String;
    function GetVariant(const Index: Integer): Variant;
    procedure SetInteger(const Index, Value: Integer);
    procedure SetString(const Index: Integer; const Value: String);
    procedure SetVariant(const Index: Integer; const Value: Variant);
  public
     constructor Create(aDataSet:TDataSet; criteria: String);
  published
      Property EmployeeID : Integer Index 0 Read GetInteger Write SetInteger;
      Property FirstName  : String  Index 1 Read GetString  Write SetString;
      Property LastName   : String  Index 2 Read GetString  Write SetString;
      Property BirthDate  : Variant Index 3 Read GetVariant Write SetVariant;
  end;

内容概要:本文围绕“基于交流潮流的电力系统多元件N-k故障模型研究”展开,深入探讨了利用Matlab代码实现电力系统在发生多个关键元件同时故障(即N-k故障)情况下的交流潮流计算与故障分析方法。该模型不仅考虑了传统潮流方程的非线性特性,还引入了故障约束条件,能够精确模拟复杂多样的故障场景,如短路、断线等,进而评估电网在极端运行条件下的稳态与动态行为。研究通过构建典型电力系统算例,验证了所提模型在故障筛选、脆弱性识别及系统恢复策略制定方面的有效性,为电力系统安全评估、风险预警和防御体系构建提供了坚实的理论依据和技术支撑。此外,模型具备良好的扩展性,可进一步应用于连锁故障传播分析、恶意攻击模拟等高级安全分析领域。; 适合人群:具备电力系统分析基础理论知识和Matlab编程能力的高校研究生、科研院所研究人员以及电力公司从事电网规划、运行与安全管理的技术人员,特别适用于开展电力系统安全稳定、可靠性评估与应急响应机制研究的专业人士。; 使用场景及目标:①开展电力系统在多重故障条件下的交流潮流仿真,评估系统电压稳定性、线路过载风险及负荷损失程度;②识别电网中的关键薄弱环节与脆弱元件,支撑电网加固改造与防御资源配置;③用于科研项目中的故障场景建模与算法验证,或作为教学案例帮助学生理解复杂故障下的系统响应机制。; 阅读建议:此资源以Matlab代码为核心实现手段,建议读者结合理论推导与代码实现进行对照学习,重点关注故障建模过程中雅可比矩阵的修正方法、故障注入方式及收敛性处理策略,建议在仿真中逐步增加故障数量与复杂度,深入理解N-k故障对系统潮流分布的影响规律,并尝试将其拓展至含新能源接入的现代电力系统场景中进行验证与优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值