Delphi函数返回类对象

时间:2017-08-25 12:39:27

标签: delphi

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的第二个实现是否可以成为解决方案?以后我必须检查结果是否已分配或为零。

我同意(如上所述)testResultObject(x: TClassA): boolean会更好。我只是想知道返回班级的功能方式是否可以像我写的那样修复。

3 个答案:

答案 0 :(得分:9)

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

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

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;

答案 1 :(得分:5)

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

  • 通过吞下所有例外,(正如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;

答案 2 :(得分:4)

唯一的问题是:

 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执行推送到构造函数中,因此任何异常都会自动调用析构函数,并且您的内存管理问题也会消失。其他答案也有很好的解决方案。