PAYATHON 2026

如何在 Spring4D 中实现作用域内共享并自动释放的 Context?

数据阿明

对于示例中一次创建 TTest,并让 TTest 与 TSub 共用 IContext 的需求,可以使用 PerResolve。如果同一业务作用域内多次调用工厂都要共享 Context,就需要额外管理作用域状态。释放一个 Scope 引用后,相关对象是否销毁,还取决于是否存在其他接口引用。

一次 Resolve 内共享:使用 PerResolve

PerResolve 的共享范围是一次根解析建立的依赖图。创建 ITest 时,直接注入 TTest 的 IContext 和通过 ISub 注入 TSub 的 IContext 可以是同一个实例。相关行为可参见 Spring4D 对该问题的说明。

假设相关接口、实现类及构造函数已定义,注册方式如下:

Container.RegisterType<IContext, TContext>.PerResolve;
Container.RegisterType<ITest, TTest>.AsTransient;
Container.RegisterType<ISub, TSub>.AsTransient;
Container.Build;

调用时,用一个接口变量持有根对象:

var
  Test: ITest;
begin
  Test := Container.Resolve<ITest>();
  try
    Test.Execute;
  finally
    Test := nil;
  end;
end;

构造函数需要将依赖保存到接口字段中。例如,TSub 保存 FContext: IContext,TTest 保存 FContext: IContext 和 FSub: ISub。根对象释放后,其依赖链持有的接口引用也会随之释放。

这段代码没有引入显式 Scope,适合一次操作对应一个完整对象图的情况。

使用工厂时,区分两种共享边界

假设传入的 Func<ITest> 每次都调用 Container.Resolve<ITest>(),每次调用都会开始一次新的根解析。两次独立解析不会因为 PerResolve 自动共享同一个 Context。

下面两种需求的处理方式不同:

  • 一次工厂调用创建的 TTest 和 TSub 共享 Context:可以使用 PerResolve。
  • 同一次业务操作中,多次调用工厂得到的对象都共享 Context:需要由该操作持有一个 Context,并让工厂使用它。

如果业务代码只需调用一次工厂,可以直接接收并调用它,无须为了使用 PerResolve 再创建 Scope:

procedure DoInScope(TestFactory: Func<ITest>);
var
  Test: ITest;
begin
  Test := TestFactory();
  try
    Test.Execute;
  finally
    Test := nil;
  end;
end;

这里沿用了原问题的过程名称,过程本身没有创建容器作用域。对象是否共享 Context,取决于工厂如何构造它们。

多次工厂调用共享:让工厂持有 Context

需要显式业务作用域时,可以在装配对象的地方创建一个 Context,再通过闭包将它交给工厂。下面的实现沿用题目中的 Func<ITest>,不依赖自定义 Spring4D 生命周期扩展:

function CreateScopedTestFactory: Func<ITest>;
var
  Context: IContext;
begin
  Context := TContext.Create;

  Result :=
    function: ITest
    var
      Sub: ISub;
    begin
      Sub := TSub.Create(Context);
      Result := TTest.Create(Context, Sub);
    end;
end;

procedure RunOperation;
var
  TestFactory: Func<ITest>;
begin
  TestFactory := CreateScopedTestFactory;
  try
    DoInScope(TestFactory);
  finally
    TestFactory := nil;
  end;
end;

在这个示例中,RunOperation 管理作用域边界。同一个工厂捕获同一个 Context。在工厂有效期间,多次调用都会复用该实例,并分别创建新的 TTest 和 TSub。

业务过程 DoInScope 只接收工厂,无须访问容器。直接调用构造函数的代码集中在对象装配处。示例假设 TContext.Create 不需要额外参数;如果实际类还有依赖,应在这里通过相应构造函数或独立的创建工厂传入。

这是应用层实现的作用域,不会为 Container.Resolve 增加透明的作用域支持。示例在创建工厂时就创建 Context,不会等到首次使用时才创建。

为什么原来的 DelegateTo 会递归

在 IContext 的创建委托中再次执行:

Container.Resolve<IContext>

会重新进入同一个注册的创建路径,无法借此获得尚未创建的“原始实例”。

如果继续使用作用域缓存,缓存未命中时应通过独立的创建路径生成 Context,例如调用专门的工厂。这个创建路径不能再次解析同一个 scoped 注册;Context 的其他依赖也不能形成循环依赖。

委托还必须明确处理没有当前作用域的情况,例如抛出异常。原代码只在 TScope.InScope 为真时设置 Result,没有处理其他情况。

作用域结束与对象销毁的注意事项

对于使用引用计数的 TInterfacedObject,对象通常在最后一个接口引用释放时销毁。要使 Context 随业务操作结束而释放,需要同时满足以下条件:

  • ITest、ISub 和 IContext 没有被保存到操作之外。
  • 工厂闭包没有被其他变量或长期存活的对象继续持有。
  • 对象之间没有形成接口强引用环。

原例中的 Scope := nil 只释放该变量持有的引用。如果 test 仍持有 Context,Context 就会继续存活。不要为了立即销毁对象而对仍有接口引用的对象直接调用 Free。

如果必须保留 TScope.New 加任意 Container.Resolve 的调用形式,就需要实现当前作用域查找、实例缓存和退出清理,并接入所用 Spring4D 版本的扩展接口。还应明确嵌套作用域、异常退出以及跨线程调用的规则。题目没有提供版本,不能把示例中的 AsScoped 和 TScope 直接当作已确认可用的内置 API。

备注:内容仅供参考。