在运行时创建接口实现实例

10

首先,需要解释一下我的情况:

我有一个样例接口,由不同的类实现,这些类可能并不总是有共同的祖先:

  IMyInterface = interface
    ['{1BD8F7E3-2C8B-4138-841B-28686708DA4D}']
    procedure DoSomething;
  end;

  TMyImpl = class(TInterfacedPersistent, IMyInterface)
    procedure DoSomething;
  end;

  TMyImp2 = class(TInterfacedObject, IMyInterface)
    procedure DoSomething;
  end;

我还有一个工厂方法,用于创建实现我的接口的对象实例。我的工厂方法接收类名作为其参数:

  function GetImplementation(const AClassName: string): IMyInterface;

我尝试了两种方法来实现这个工厂方法,第一种方法是使用扩展RTTI:
var
  ctx : TRttiContext;
  t : TRttiInstanceType;
begin
  t := ctx.FindType(AClassName).AsInstance;
  if Assigned(t) then
    Result := t.GetMethod('Create').Invoke(t.MetaclassType, []).AsInterface as IMyInterface;
end;

在这种方法中,我调用了默认构造函数,在我的场景中这是可以的。问题在于,在运行时,我会收到一个错误,告诉我对象不支持IMyInterface。更重要的是,创建的对象没有分配给接口变量;因此,它将被泄漏。我还尝试使用TValue.AsType方法返回值,但它给我带来了访问冲突:
function GetImplementation(const AClassName: string): IMyInterface;
var
  ctx : TRttiContext;
  rt : TRttiInstanceType;
  V : TValue;
begin
  rt := ctx.FindType(AClassName).AsInstance;
  if Assigned(rt) then
  begin
    V := rt.GetMethod('Create').Invoke(rt.MetaclassType, []);
    Result := V.AsType<IMyInterface>;
  end;
end;

我尝试的第二种方法是使用一个通用字典来存储键值对,并提供注册和注销方法:
  TRepository = class
  private
    FDictionary : TDictionary<string, TClass>;
  public
    constructor Create;
    destructor Destroy; override;
    function GetImplementation(const AClassName: string): IMyInterface;
    procedure RegisterClass(AClass: TClass);
    procedure UnregisterClass(AClass: TClass);
  end;

在这里,我实现了GetImplementation方法如下:

function TRepository.GetImplementation(const AClassName: string): IMyInterface;
var
  Obj : TObject;
begin
  if FDictionary.ContainsKey(AClassName) then
  begin
    Obj := FDictionary[AClassName].Create;
    Obj.GetInterface(IMyInterface, Result);
  end;
end;

这个代码可以正常工作,我可以使用GetImplementation返回的值调用DoSomething方法,但它仍然存在内存泄漏问题; 在这里创建的Obj没有分配给任何接口变量; 因此,它没有引用计数,并且会泄漏。

.

现在,我的实际问题是:

所以我的问题是,如何安全地在运行时创建实现我的接口的类的实例? 我看到Delphi Spring Framework,在其Spring.Services单元中提供了这样的功能,但它有自己的反射例程和生命周期管理模型。 我正在寻找一个轻量级的解决方案,而不是整个第三方框架来为我完成这项工作。

谢谢


2
我敢打赌,答案是一旦你拥有了所需的一切,你就拥有了Spring框架,或者一个可以映射到它上面的同态。Lisp程序员经常发现他们“发现”了正确的答案,几乎像科学家揭示物理宇宙的秩序一样。从这个意义上说,我猜想你会重新发现Spring(英格兰)。 - Warren P
2
只有一个注意事项:构造函数并不总是被称为“Create”,因此使用 Rtti 遍历方法并查找无参数构造函数可能更加健壮。 - jpfollenius
3个回答

11

使用RTTI的第一个案例会导致访问冲突,因为TRttiContext.FindType(AClassName)无法找到未在应用程序中显式注册或使用的类的Rtti信息。

因此,您可以更改您的代码:

function GetImplementation(AClass: TClass): IMyInterface;
var
  ctx : TRttiContext;
  t : TRttiInstanceType;
begin
  t := ctx.GetType(AClass).AsInstance;
  if Assigned(t) then
    Result := t.GetMethod('Create').Invoke(t.MetaclassType, []).AsInterface As IMyInterface;
end;

并以这种方式调用

AClass:=GetImplementation(TMyImp2);

现在,如果您想使用类名来调用类,使用列表(例如您的TRepository类)来注册类是一种有效的方法。关于内存泄漏,我非常确定是由于TMyImpl类派生自TInterfacedPersistent类,而后者不像TInterfacedObject类直接实现引用计数导致的。这个TRepository类的实现应该可以正常工作。
constructor TRepository.Create;
begin
  FDictionary:=TDictionary<string,TClass>.Create;
end;

destructor TRepository.Destroy;
begin
  FDictionary.Free;
  inherited;
end;

function TRepository.GetImplementation(const AClassName: string): IMyInterface;
var
  Obj : TObject;
begin
  if FDictionary.ContainsKey(AClassName) then
  begin
    Obj := FDictionary[AClassName].Create;
    Obj.GetInterface(IMyInterface, Result);
  end;
end;

{
or using the RTTI
var
  ctx : TRttiContext;
  t : TRttiInstanceType;
begin
  t := ctx.GetType(FDictionary[AClassName]).AsInstance;
  if Assigned(t) then
    Result := t.GetMethod('Create').Invoke(t.MetaclassType, []).AsInterface As IMyInterface;
end;
}

procedure TRepository.RegisterClass(AClass: TClass);
begin
  FDictionary.Add(AClass.ClassName,AClass);
end;

procedure TRepository.UnregisterClass(AClass: TClass);
begin
  FDictionary.Remove(AClass.ClassName);
end;

1
从现在开始,我将使用StrList := Context.GetType(TStringList).GetMethod('Create').Invoke(Context.GetType(TStringList).MetaclassType, []).AsObject as TStringList; :) - jpfollenius
2
+1 我没有注意到 TInterfacedPersistent。那就是内存泄漏的问题所在。 - David Heffernan
@RRUZ:在你的第一段中,你建议更改函数,使其接受类类型。在我看来这没有意义。或者我理解错了吗? - jpfollenius
啊,现在我明白了,在这种情况下使用RTTI调用构造函数没有意义,但是当你需要调用另一个方法时非常有用。此外,第一段只是整个答案的一部分。 :) - RRUZ
1
很高兴能帮助你 :) 另外,FYI,昵称是RRUZ(Rodrigo Ruz),而不是PRUZ ;) - RRUZ
显示剩余5条评论

4

我认为我会选择第二个选项,主要是因为我喜欢避免使用RTTI,除非它是解决问题的唯一可能方案。

但在你提出的两个选项中,你都说

在这里创建的对象未分配给任何接口变量

这是不正确的。在两种情况下,你都将其分配给类型为IMyInterfaceResult。如果有内存泄漏,则是由其他代码引起的,而不是由此代码引起的。

@RRUZ已经找到了泄漏的原因-即使用不实现引用计数生命周期管理的TInterfacedPersistent。对于TInterfacedObject,你的代码不会泄漏。

值得一提的是,我会直接分配给接口变量,而不是通过对象引用,但那只是一个风格上的偏好。

if FDictionary.TryGetValue(AClassName, MyClass) then
  Result := MyClass.Create as IMyInterface;

泄露的对象与GetImplementation方法中创建的对象类型相同,所以我认为它只是一个未分配给任何接口的悬空对象。正如您所提到的问题,在于使用TInterfacedPersistent。我不知道它本身不实现引用计数,而我只是使用它来注册我的类以便可以使用RTTI访问它们。 - vcldeveloper
关于您提出的直接接口分配建议;当我有特定的类类型时,这是有效的,但是我的字典只存储对TClass的引用;因此,如果我使用您的代码,编译器将抱怨“运算符不适用于此操作数类型”,但是使用对象引用检索接口可以解决这个问题。 - vcldeveloper
问题在于TClass没有实现IInterface,而这是“as”运算符所需的(请参见此处:链接)。定义一个新类型TIntfClass = class of TInterfacedObject会有所帮助。 - sausagequeen

2
你可以使用扩展 RTTI 和 TObject 的 GetInterface 方法来实现:
function GetImplementation(const AClassName: string): IMyInterface;
var
  ctx: TRttiContext;
  t : TRttiInstanceType;
  obj: TObject;
begin
  Result := nil;
  t := ctx.FindType(AClassName).AsInstance;
  if Assigned(t) then begin
    obj := t.GetMethod('Create').Invoke(t.MetaclassType, []).AsObject;
    obj.GetInterface(IMyInterface, Result)
  end;
end;

如果对象重写QueryInterface进行自定义处理,它将无法正常工作,但是TInterfacedPersistentTInterfacedObject都依赖于GetInterface


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