【问题标题】:Delphi function return class objectDelphi函数返回类对象
【发布时间】:2017-08-25 12:39:26
【问题描述】:

除了this的问题,我在docwiki上做了一些测试和研究。我的结论是这种代码应该可以在没有内存泄漏的情况下工作:

function testResultObject: TClassA;
begin
 Result := TClassA.Create;
 Result.DoSomething;
end;

然后在某个地方我可以用这种方式调用上面的代码:

var k: TClassA;
begin

 k := testResultObject;
 try
  //code code code
 finally
  k.Free;
 end;

end;

正如雷米在答案中建议的那样,最好避免这种做事方式,而是使用testResultObject(x: TClassA): boolean 之类的东西。在这种情况下,返回 true/false 可以告诉我是否一切正常并且我正在传递一个已经创建的对象。

看这段代码:

function testResultObject: TClassA;
begin

 Result := TClassA.Create;

 try
  Result.DoSomething;
 except
  Result.Free;
 end;

end;

上述函数的第一个版本的问题是DoSomething 可能会引发异常,如果是这样我会泄漏内存。 try-except 的第二个实现可以解决吗?以后肯定会检查结果是赋值还是 nil。

我同意(如上所述)testResultObject(x: TClassA): boolean 会更好。我只是想知道 return-a-class 函数方式是否可以像我写的那样修复。

【问题讨论】:

  • 就个人而言,我一直使用第二种方法(真/假返回),这种方法更好并且使代码更具可读性。您的解决方案很好,但您还应该在异常块中添加 Result := nil
  • 您误解了我之前的回答。当您修改该对象(在该示例中,将项目添加到列表中)时,将对象作为参数传递是很好的。这是一个完全不同的场景。

标签: delphi


【解决方案1】:

您的代码存在严重问题。如果发生错误,它会吞下异常,并返回无效的对象引用。

这很容易解决。规范的方式如下:

function testResultObject: TClassA;
begin
  Result := TClassA.Create;    
  try
    Result.DoSomething;
  except
    Result.Free;
    raise;
  end;
end;

要么函数成功,要么返回一个新对象。或者它失败,自行清理并引发异常。

换句话说,这个函数的外观和行为就像一个构造函数。你以同样的方式消费它:

obj := testResultObject;
try
  // do things with obj
finally
  obj.Free;
end;

【讨论】:

  • 我认为这是最好的答案。我喜欢以标准方式做事,在这里我可以使用正常的 try-finally。此外,该函数正在正确地进行清理,并且代码很容易阅读。
【解决方案2】:

您的第二种方法有效,但存在两个严重问题。

  • 通过吞下所有异常,(正如 J 指出的那样)您将隐藏出现问题的事实。
  • 没有迹象表明调用者已经创建了调用者负责销毁的对象。这使得使用该功能更容易出错;并且更容易导致内存泄漏。

我建议对您的第二种方法进行以下改进:

         {Name has a clue that caller should take ownership of a new object returned}
function CreateObjectA: TClassA;
begin
  {Once object is successfully created, internal resource protection is required:
      - if no error, it is callers responsibility to destroy the returned object
      - if error, caller must assume creation *failed* so must destroy object here
  Also, by assigning Result of successful Create before *try*:
      The object (reference) is returned
          **if-and-only-if**
      This function returns 'normally' (i.e. no exception state)}
  Result := TClassA.Create;    
  try
    Result.DoSomething; {that could fail}
  except
    {Cleanup only if something goes wrong:
      caller should not be responsible for errors *within* this method}
    Result.Free;
    {Re-raise the exception to notify caller:
      exception state means caller does not "receive" Result...
      code jumps to next finally or except block}
    raise;
  end;
end;

上述创建函数最重要的好处是:就任何调用者/客户端代码而言,它的行为与普通的 TObject.Create 完全相同。
所以正确的使用模式是完全一样的。

请注意,我不喜欢 J 的 FreeAndNil 建议,因为如果调用代码不检查结果是否已分配:它很可能是 AV。并且正确检查结果的代码会有点混乱:

var k: TClassA;
begin
  k := testResultObject; {assuming nil result on failed create, next/similar is *required*}
  if Assigned(k) then    {Note how this differs from normal try finally pattern}
  try
    //code using k
  finally
    k.Free;
  end;
end;

注意:重要的是要注意,您不能让调用者简单地忽略内存管理;这将我带到下一部分。


除此之外,如果您的testResultObject 采用您需要调用者根据需要创建和管理其生命周期的输入对象,那么犯粗心错误的可能性就会大大降低。 我不知道你为什么这么反对这种方法?如果不求助于不同的内存模型,你不可能比以下更简单。

var k: TClassA;
begin
  k := TClassA.Create;
  try
    testResultObject(k); {Where this is simply implemented as k.DoSomething;}
    //more code using k
  finally
    k.Free;
  end;
end;

【讨论】:

  • 正如我所说,我更喜欢 testResultObject(x: TClassA): boolean 而不是返回 TClassA。我只是想知道我的解决方案是否正确,仅此而已;)我同意您的最后一个实现是最好的!
  • @Raffaele 为什么你更喜欢布尔值。强制用户对其进行测试,并吞下异常。有一种简单的方法可以解决此问题,并使您的调用代码遵循标准模式。
  • @Craig 您的最终建议并不能解决函数在实例化对象之前可能引发异常的情况。这就是您的第一种方法获胜的地方。
  • @RaffaeleRossi 好奇心总是值得称赞的! :) 顺便说一句,我想对返回布尔结果的函数提供一些警告:1) 确保您始终完全清楚 True 与 False 的含义。有时真的没有足够好的理由来区分;然后它真的应该是一个程序。 2) 记得在调用者中检查结果。这很容易忘记。
  • 是的,这是假设的情况。将其分开可能会很好,但会增加调用者的责任以正确执行此操作。封装在函数中可以消除它。例如,可能会有许可证检查。
【解决方案3】:

唯一的问题:

 function testResultObject: TClassA;
 begin
   Result := TClassA.Create;    
   try
     Result.DoSomething;
   except
     Result.Free;
   end;
 end;

是你没有办法知道函数是否成功。释放对象不会改变引用;该变量仍将指向该对象曾经存在的(现在)无效内存位置。如果您希望消费者能够测试引用是否有效,则必须显式设置对nil 的引用。如果您想使用这种模式(对nil 进行消费者测试),那么您需要这样做:

try
  Result.DoSomething;
except
  FreeAndNil(Result);
end;

这样,调用者可以按照您的意图测试nil(使用Assigned 或其他方式)的结果。但是,这仍然不是一个非常干净的方法,因为您仍在吞咽异常。另一种解决方案可能是简单地引入一个新的构造函数或更改现有的构造函数。例如

TFoo = class
  public
    constructor Create(ADoSomething : boolean = false);
    procedure DoSomething;
end;

constructor TClassA.Create(ADoSomething: Boolean = False);
begin
  inherited Create;
  if ADoSomething then DoSomething;
end;

procedure TClassA.DoSomething;
begin
  //
end;

这样你就可以摆脱所有的异常处理,只需将其称为:

function testResultObject: TClassA;
begin
  Result := TClassA.Create(true);     
end;

既然您现在将DoSomething 执行推入构造函数,任何异常自然会自动调用析构函数,您的内存管理问题就会消失。其他答案也有很好的解决方案。

【讨论】:

  • 我不明白为什么你会建议吞下这样的异常
  • @DavidHeffernan 我不建议这样做,这就是为什么我添加了一个更明智的选择。 OP 提出了这种模式,似乎表明他们很清楚它的缺点,所以我觉得没有必要再强调这一点。
  • 我怀疑 Raffaele 是否欣赏这一点。在专注于泄漏的问题中没有提到这一点。当我阅读它时,您的答案的前半部分祝福了异常吞咽。这是错误的。并且很容易通过再加注来解决。
  • @DavidHeffernan 同意,您已经在回答中提到了这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-23
  • 2020-04-04
  • 2012-02-29
相关资源
最近更新 更多