- 更新专题页推荐阅读中的 README 入口为可点击 GitHub 链接 - 修复 source-generators 与 Game/ECS/CQRS 页面中的裸路径导航 - 更新 documentation-full-coverage-governance 跟踪与轨迹以记录第二批治理结果
6.7 KiB
title, description
| title | description |
|---|---|
| ContextAware 生成器 | 说明 [ContextAware] 当前会生成什么、何时使用、与 ContextAwareBase 的边界以及测试场景。 |
ContextAware 生成器
[ContextAware] 是 GFramework.Core.SourceGenerators 中最常用的一类生成器。它的职责很明确:
- 为当前类型自动补齐
IContextAware - 提供可复用的上下文懒加载入口
- 让类型可以直接使用
this.GetSystem<T>()、this.GetModel<T>()、this.GetUtility<T>()等扩展方法
它不负责注册服务,也不会替你决定应该取哪个 System / Model。它解决的是“当前类型如何拿到架构上下文”。
当前包关系
- 特性来源:
GFramework.Core.SourceGenerators.Abstractions - 生成器实现:
GFramework.Core.SourceGenerators - 运行时接口:
GFramework.Core.Abstractions.Rule.IContextAware - 常用扩展方法:
GFramework.Core.Extensions
如果只安装运行时 GFramework.Core 而没有安装 Core.SourceGenerators,[ContextAware] 本身不会生效。
最小用法
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.Extensions;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PlayerController : IController
{
public void Initialize()
{
var playerModel = this.GetModel<IPlayerModel>();
var combatSystem = this.GetSystem<ICombatSystem>();
combatSystem.Bind(playerModel);
}
}
当前最重要的前置条件只有两个:
- 必须是
class - 必须声明为
partial
如果缺少这两个条件,生成器不会补代码。
当前会生成什么
按当前源码,[ContextAware] 会为目标类型生成:
IContextAware的显式接口实现- 受保护的
Context属性 - 类型级静态
SetContextProvider(...) - 类型级静态
ResetContextProvider() - 一个实例级
_context缓存字段 - 一个类型级共享的
_contextProvider - 一个类型级锁
_contextSync
这意味着它不是“每次访问都现查当前架构”。当前行为更接近:
- 先看当前实例是否已经缓存了上下文
- 如果没有,就在同步锁内取共享 provider
- 若 provider 为空,则回退到
GameContextProvider - 把拿到的上下文缓存到当前实例
当前语义里最容易误解的点
provider 是按类型共享,不是按实例共享
SetContextProvider(...) 影响的是“这个生成类型的后续实例或尚未初始化上下文的实例”,不是全仓库所有 [ContextAware]
类型共享同一个 provider。
provider 切换不会自动刷新已缓存实例
一旦某个实例已经把上下文缓存进 _context,后续再调用:
SetContextProvider(...)ResetContextProvider()
都不会自动改写这个实例的已缓存上下文。
如果你确实要覆盖某个现有实例的上下文,应显式调用:
((IContextAware)controller).SetContext(context);
生成路径和 ContextAwareBase 不是一回事
当前源码里两者的默认回退策略不同:
[ContextAware]生成实现- 通过共享 provider 回退,默认 provider 是
GameContextProvider - 带同步锁,支持
SetContextProvider(...)/ResetContextProvider()
- 通过共享 provider 回退,默认 provider 是
ContextAwareBase- 只维护简单的实例级缓存
- 不维护共享 provider
- 默认直接回退到
GameContext.GetFirstArchitectureContext()
因此,旧文档里把两条路径混写成“只是写法不同”已经不准确。
何时使用 [ContextAware]
优先用于这些场景:
- 你的类型不是
AbstractSystem、AbstractModel、AbstractCommand这类已经继承ContextAwareBase的框架基类 - 你希望在测试中显式切换 provider
- 你需要在同一生成类型上统一切换上下文来源
- 你在 Godot 节点、Controller、ViewModel、包装器类型上只想获得上下文访问能力
典型例子:
IController实现- Godot
Node/Control的项目侧包装器 - 不继承框架基类但要访问架构的辅助类型
何时改用 ContextAwareBase
以下场景优先考虑 ContextAwareBase 或已经继承它的框架基类:
- 你本来就继承
AbstractSystem、AbstractModel、AbstractCommand、AbstractQuery - 你不需要类型级共享 provider
- 你只需要简单的实例级上下文缓存
- 调用线程模型已经天然串行,不需要生成实现那套 provider 切换与同步语义
如果一个类型已经通过继承链拿到了 ContextAwareBase,通常没必要再额外标 [ContextAware]。
与 Context Get 注入的关系
[GetModel]、[GetSystem]、[GetUtility]、[GetService] 这类字段注入生成器,并不是独立工作的。
按当前 ContextGetGenerator 的判定规则,目标类型必须满足以下三者之一:
- 标记了
[ContextAware] - 实现了
IContextAware - 继承了
ContextAwareBase
所以更准确的理解是:
[ContextAware]负责“让类型成为 context-aware 类型”- Context Get 系列特性负责“在这个前提下继续减少字段取值样板”
测试场景
如果测试里不想依赖默认全局上下文,推荐显式配置 provider:
PlayerController.SetContextProvider(new TestContextProvider(testArchitecture.Context));
try
{
var controller = new PlayerController();
controller.Initialize();
}
finally
{
PlayerController.ResetContextProvider();
}
需要注意两点:
ResetContextProvider()只会重置共享 provider,不会清除已创建实例上的_context- 如果测试要复用同一实例并切换上下文,应该显式调用
((IContextAware)instance).SetContext(...)
诊断与约束
当前文档里最值得记住的约束只有这些:
- 非
class会触发GF_Rule_001 - 非
partial不会生成实现,并会触发公共 partial 约束诊断 - 嵌套、字段注入等其他错误通常由对应的 Context Get 生成器和其诊断补充报告
与旧写法的边界
下面这些旧说法已经不够准确:
- “
[ContextAware]只是帮你补一个简单的GetContext()” - “切换 provider 后,已有实例会自动跟着切换”
- “
[ContextAware]和ContextAwareBase的默认行为完全一致”
当前更准确的理解是:
- 生成实现带有实例缓存、类型级共享 provider 和同步锁
- provider 切换只影响尚未缓存上下文的实例
ContextAwareBase是更轻量的实例级缓存路径