正确的复制Delphi对象的方法

24

使用构造函数或实例函数复制对象实例有哪些优缺点?

示例A:

type
  TMyObject = class
  strict private
    FField: integer; 
  public
    constructor Create(srcObj: TMyObject); overload; 
    //alternatively:
    //constructor CreateFrom(srcObj: TMyObject);
    property Field: integer read FField;
  end;

constructor TMyObject.Create(srcObj: TMyObject);
begin
  inherited Create;
  FField := srcObj.Field;
end;

示例 B:

type
  TMyObject = class
  strict private
    FField: integer; 
  public
    function Clone: TMyObject;
    property Field: integer read FField;
  end;

function TMyObject.Clone: TMyObject;
begin
  Result := TMyObject.Create;
  Result.FField := FField;
end;

一个重要的区别立即显现出来——在后一种情况下,Create构造函数必须是虚拟的,以便基于TMyObject构建支持克隆的类层次结构。

假设这不是问题——TMyObject及其所有基于它的内容完全由我控制。你怎么做Delphi中的复制构造函数?哪个版本更易读?你什么时候会使用前面或后面的方法?讨论一下 :)

编辑:我对第一个示例的主要担忧是使用与第二个方法相比非常繁重,即

newObj := TMyObject.Create(oldObj)

对比。

newObj := oldObj.Clone;

编辑2或“为什么我想要单行操作”

我同意在大多数情况下使用Assign是合理的方法。甚至可以通过简单地使用assign来内部实现“复制构造函数”。

当我在多线程和通过消息队列传递对象时,通常会创建这样的副本。如果对象创建速度很快,我通常会传递原始对象的副本,因为这真正简化了对象所有权的问题。

换句话说,我更喜欢写以下代码:

Send(TMyObject.Create(obj));
Send(obj.Clone);
to
newObj := TMyObject.Create;
newObj.Assign(obj);
Send(newObj);
4个回答

32

第一个添加关于要创建哪个对象的信息,第二个则没有。这可用于实例化类的后代或祖先。

Delphi的方式(TPersistent)将创建和克隆分开:

dest := TSomeClass.Create; 
dest.Assign(source);  

它具有显式选择要实例化的类的属性。但您不需要两个构造函数,一个用于正常使用,另一个用于克隆。

由于单行要求而进行的编辑 当然可以使用Delphi元类进行混合(未经测试)

type
  TBaseSomeObject = class;
  TBaseObjectClass = class of TBaseSomeObject;

  TBaseSomeObject = class(TPersistent)
    function Clone(t: TBaseObjectClass = nil): TBaseSomeObject; virtual;
  end;

...

  function TBaseSomeObject.Clone(t: TBaseObjectClass = nil): TBaseSomeObject;
  begin
    if Assigned(t) then
      Result := t.Create
    else
      Result := TBaseObjectClass(Self.ClassType).Create;
    Result.Assign(Self);
  end;


 SendObject(obj.Clone); // full clone.
 SendObject(obj.Clone(TDescandantObject)); // Cloned into Descendant object 

对于其余部分,只需实现您的assign()运算符,就可以混合多种方式。

编辑2

我用在D2009中测试过的代码替换了上面的代码。有一些类型的依赖关系可能会让您感到困惑,希望这样更清晰。当然,您还需要研究分配机制。我还测试了metaclass=nil默认参数,它可以工作,所以我添加了它。


2
我会投票支持你的回答,但这会破坏你的5,555分数。;-) 恭喜! - splash
4
好的,我希望有一天能够达到6666 :-) - Marco van de Voort
T被分配的分支是用于“克隆为降序对象”的,换句话说,当要创建的对象与包含数据的对象不同的时候。assign()方法可以使用IS来制作不同的情况,只复制对象之间的共同数据。 - Marco van de Voort
谢谢。在我的实现中,我还添加了一个try/except语句来包围Assign调用。我总是为返回对象的函数在其中释放Result。Result.Free和Raise。如果没有正确实现,Assign可能会失败,并且我喜欢避免内存泄漏。 - Frazz
Result.free是错误的,因为调用函数无法确定发生了什么。正确的方法是使用freeandnil。我通过单元测试来防范编程错误,但这是我当前应用程序的特点(非常关键但紧凑,您希望解决看到的每个问题)。如果您有很多很少使用的业务代码的应用程序,则最好采用您的方式使其更加健壮,并让代码运行并记录错误。 - Marco van de Voort
显示剩余5条评论

7
我认为没有正确的方法,这取决于个人风格。(正如Marco指出的那样,还有更多的方法。)
构造函数方法很短,但它违反了构造函数仅构建对象的原则,这可能不是问题。
克隆方法很简短,尽管你需要为每个类提供一个调用。
赋值方法更像Delphi。它分离了创建和初始化,这是好的,因为我们喜欢一种方法一种函数的概念,使代码更易于维护。
如果你使用流实现分配,你只需要担心哪些字段需要可用。

3

我喜欢克隆风格 - 但仅限于Java(或任何其他GC语言)。 我在Delphi中使用过它几次,但大多数情况下我都使用CreateAssign,因为这样更清楚谁负责销毁对象。


3
总的来说,我不喜欢使用带参数的构造函数。当你开始让你的框架变得更加复杂时,它就会开始咬你的<censored>。 - Marco van de Voort
1
我非常喜欢强制传递所有必要对象的构造函数,比如 Create(AOwner)。依赖注入框架知道两种方式,即基于属性或构造函数的依赖注入 - 两者都有其优点(和缺点)。 - mjn
@mjustin:我也是,但通常我不使用构造函数来创建副本。 - splash

0

我使用第二种方法,即带有克隆函数的方法,它可以完美地工作,即使是复杂的类也可以。我发现这种方法更易读且更加防错。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接