问题 深度对象比较Delphi


在Delphi中寻找一种方法来为我做深度对象比较,最好是2010 RTTI,因为我的对象不会继承 TComponent。我正在开发一个测试框架 DUnit 并且需要一些可以确切指出哪个领域引起问题的东西(序列化比较让它有点模糊)。


5654
2017-11-03 11:41


起源

序列化为文本格式(XML,JSON)并执行TDiff(angusj.com/delphi)会很容易的 - mjn
DFM格式怎么样:-)它就像一个JSON前身......从1995年开始。 - Warren P


答案:


我自己解决这个问题,作为TObject的类助手实现,因此如果人们想要它可以在任何地方使用。 D2010及以上由于RTTI,但您可以将其转换为使用原始RTTI内容。

下面的代码可能是错误的,因为我最初是为了DUnit并且在其中进行了大量检查而不是更改结果并且不支持TCollections或其他特殊情况的加载但是可以通过使用if-elseif-then来适应它在中间切换。

如果您有任何建议和补充,请不要犹豫,我可以添加它们,以便其他人可以使用它。

玩得开心编码

巴里

unit TObjectHelpers;

interface
   uses classes, rtti;

type

TObjectHelpers = class Helper for TObject
  function DeepEquals (const aObject : TObject) : boolean;
end;

implementation

uses sysutils, typinfo;

{ TObjectHelpers }

function TObjectHelpers.DeepEquals(const aObject: TObject): boolean;
var
  c : TRttiContext;
  t : TRttiType;
  p : TRttiProperty;
begin

  result := true;

  if self = aObject then
    exit; // Equal as same pointer

  if (self = nil) and (aObject = nil) then
    exit; // equal as both non instanced

  if (self = nil) and (aObject <> nil) then
  begin
    result := false;
    exit; // one nil other non nil fail
  end;

  if (self <> nil) and (aObject = nil) then
  begin
     result := false;
     exit; // one nil other non nil fail
  end;

  if self.ClassType <> aObject.ClassType then
  begin
     result := false;
     exit;
  end;

  c := TRttiContext.Create;
  try
    t := c.GetType(aObject.ClassType);

    for p in t.GetProperties do
    begin

       if ((p.GetValue(self).IsObject)) then
       begin

          if not TObject(p.GetValue(self).AsObject).DeepEquals(TObject(p.GetValue(aObject).AsObject)) then
          begin
      result := false;
      exit;
    end;

  end
  else if AnsiSameText(p.PropertyType.Name, 'DateTime') or AnsiSameText(p.PropertyType.Name, 'TDateTime') then
  begin

    if p.GetValue(self).AsExtended <> p.GetValue(aObject).AsExtended then
    begin
      result := false;
      exit;
    end;

  end
  else if AnsiSameText(p.PropertyType.Name, 'Boolean') then
  begin

    if p.GetValue(self).AsBoolean <> p.GetValue(aObject).AsBoolean then
    begin
      result := false;
      exit;
    end;

  end
  else if AnsiSameText(p.PropertyType.Name, 'Currency') then
  begin

     if p.GetValue(self).AsExtended <> p.GetValue(aObject).AsExtended then
     begin
        result := false;
        exit;
     end;

  end
  else if p.PropertyType.TypeKind = tkInteger then
  begin

    if p.GetValue(self).AsInteger <> p.GetValue(aObject).AsInteger then
    begin
      result := false;
      exit;
    end;

  end
  else if p.PropertyType.TypeKind = tkInt64 then
  begin

    if p.GetValue(self).AsInt64 <> p.GetValue(aObject).AsInt64  then
    begin
      result := false;
      exit;
    end;

  end
  else if p.PropertyType.TypeKind = tkEnumeration then
  begin

    if p.GetValue(self).AsOrdinal <> p.GetValue(aObject).AsOrdinal then
    begin
      result := false;
      exit;
    end;

  end
  else
  begin

    if p.GetValue(self).AsVariant <> p.GetValue(aObject).AsVariant then
    begin
      result := false;
      exit;
    end;

  end;

end;

 finally
   c.Free;
  end;

 end;

 end.

12
2017-11-03 15:15



不重要与显着差异如何? (例子:你有没有想要忽略窗口句柄值等差异的情况?你能添加一个排除属性,以便深度比较会跳过某些东西吗? - Warren P
当有人为您按名称识别的类型进行类型重新定义时,这会中断。您应该比较这些类型的TypeInfo。 - Stefan Glienke
如果人们已经在为TObject使用他们自己的(或其他人的)类助手,那就使用NOWHERE。为了大声哭泣,人们什么时候会停止使用课堂助手,因为单元级别的功能/程序足够好并且超过合理的!!看起来好像人们非常渴望找到合法使用的助手,他们会把它们扔到每一个问题上 - 其中99%是完全不合适的。 - Deltics
当有人重新定义类型时,肯定会中断,但正如我所说的快速和脏代码。如果您正在重新定义DateTime / Boolean / Currency,我认为您将拥有必要的技能来查看上面给出的代码并根据您的口味对其进行修改 - Barry


考虑使用 OmniXML持久性

对于XML差异,我已经使用OmniXML编写了一个实用XML差异的实用程序,并且有许多XML比较工具。

我使用OmniXML来完成这个目的的XML差分工具,它对我来说很有用。不幸的是,该工具包含许多特定于域的内容,并且是封闭源代码,属于前雇主,因此我无法发布代码。

我的比较工具有一个简单的算法:

  1. 匹配并构建匹配的XML节点之间的Object1-> Object2节点链接的映射。
  2. 对主键上的每个节点(特定于域的知识)进行排序,使XML顺序不重要。由于您不仅要将TComponents与Names进行比较,还需要找到一种方法来建立每个对象标识,如果您希望能够比较它。
  3. 报告xml doc 1中不在xml doc 2中的项目。
  4. 报告xml doc 2中不在xml doc 1中的项目。
  5. 使用不同于xml doc2的子项或属性在xml doc 1中报告项目。
  6. 可视化工具使用了两个虚拟树视图控件,并且像KDIFF3一样工作但是作为树视图。

4
2017-11-03 13:26



这是一个很好的解决方案,但仍然有点太多的开销和复杂的我喜欢,特别是在DUnit测试中使用。我想我刚刚写了一些可以完成工作的东西,我会稍微发布一下 - Barry
这正是我使用它的方式; DUNIT。它在磁盘上创建了许多单元测试输出文件,这正是我需要查找,理解和修复正在发生的回归的内容。数据就是力量。 - Warren P
啊公平,真的是不同风格。测试套件必须由少数人使用,所以宁愿让前面正在处理它的编码器显而易见,而不是通过文件和检查。 - Barry