Merge pull request #266 from GeWuYou/docs/sdk-update-documentation

docs(core): 收口 Core 事件属性与日志专题页
This commit is contained in:
gewuyou 2026-04-21 14:09:11 +08:00 committed by GitHub
commit 4d306498b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 244 additions and 1351 deletions

View File

@ -7,21 +7,19 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-004`
- 恢复点编号:`DOCUMENTATION-GOVERNANCE-REFRESH-RP-005`
- 当前阶段:`Phase 3`
- 当前焦点:
- 已完成 `docs/zh-CN/core/architecture.md``context.md``lifecycle.md``command.md``query.md`
`cqrs.md` 的专题页重写
- `core` 关键专题页已改回当前 `Architecture``ArchitectureContext`、旧 Command/Query 兼容层与新 CQRS
runtime 的真实入口语义
- 下一轮需要继续推进 `docs/zh-CN/core/*` 余下专题页,以及 `docs/zh-CN/game/*`
`docs/zh-CN/source-generators/*` 的专题页核对
- 已完成 `docs/zh-CN/core/events.md``property.md``logging.md` 的专题页重写
- 已按源码与测试复核 `docs/zh-CN/core/state-management.md``coroutine.md`,当前内容与实现基本一致,无需再做
机械改写
- 下一轮需要把重心转到 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*` 的专题页核对
## 当前状态摘要
- 文档治理规则已收口到仓库规范README、站点入口与采用链路不再依赖旧文档自证
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态
- 当前主题仍是 active topic因为 `core` 其余专题页及 `game``source-generators` 栏目下仍可能包含与实现漂移的旧内容
- 高优先级模块入口与 `core` 关键专题页已回到可作为默认导航入口的状态,本轮计划中的 `core` 剩余高风险页面已完成收口
- 当前主题仍是 active topic因为 `game` 与 `source-generators` 栏目下仍可能包含与实现漂移的旧内容
## 当前活跃事实
@ -36,11 +34,15 @@
- `documentation-governance-and-refresh` active trace 已把重复的 `### 下一步` 标题改成带恢复点标识的唯一标题,消除
`MD024/no-duplicate-heading` 告警
- `gframework-pr-review` 脚本已修复“空 `APPROVED` review 覆盖非空 CodeRabbit review body”的解析路径当前分支可重新提取 Nitpick comments
- `docs/zh-CN/core/events.md``property.md``logging.md` 已改成“当前角色、最常用入口、边界和迁移建议”的结构,
不再复刻旧版大而全 API 列表
- `docs/zh-CN/core/property.md` 已明确记录 `BindableProperty<T>.Comparer` 的闭合泛型级共享语义,避免文档继续误导读者把
`WithComparer(...)` 当成实例级配置
- `docs/zh-CN/core/state-management.md``coroutine.md` 已按当前 runtime / 测试重新核对,当前内容可继续保留
## 当前风险
- 旧专题页示例失真风险:`docs/zh-CN/core/*``game/*``source-generators/*` 中仍可能保留看似合理但与
真实实现不一致的示例
- 旧专题页示例失真风险:`docs/zh-CN/game/*``source-generators/*` 中仍可能保留看似合理但与真实实现不一致的示例
- 缓解措施:继续按源码、测试、`*.csproj``ai-libs/` 下已验证参考实现核对,不把旧文档当事实来源
- 采用路径误导风险:根聚合包与模块边界若再次被写错,会继续误导消费者的包选择
- 缓解措施:保持“源码与包关系优先”的证据顺序,改动采用说明时同步核对包依赖与生成器 wiring
@ -64,7 +66,6 @@
## 下一步
1. 继续核对 `docs/zh-CN/core/*` 余下专题页,优先处理 `events``property``state-management``coroutine`
`logging`
2. 再推进 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*` 的专题页重写,优先处理仍引用旧安装方式或旧 API 的页面
1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面
2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例
3. 若 active trace 再积累新的已完成阶段,按恢复点粒度迁入 `archive/traces/`,避免默认启动入口再次膨胀

View File

@ -102,6 +102,28 @@
2. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节
3. 保持 PR review 跟进时优先验证最新未解决线程、非空 CodeRabbit review body 与 MegaLinter 明确告警
1. 继续处理 `docs/zh-CN/core/events.md``property.md``state-management.md``coroutine.md``logging.md`
2. 保持同样的证据顺序:源码、`*.csproj`、模块 README、`ai-libs/` 参考实现
3. 完成下一批专题页重写后再次执行 `cd docs && bun run build`
### 阶段Core 剩余高风险专题页核对RP-005
- 依据 `documentation-governance-and-refresh` active tracking 的恢复点,继续核对
`docs/zh-CN/core/events.md``property.md``state-management.md``coroutine.md``logging.md`
- 对照 `GFramework.Core/Events/*``Property/*``Logging/*``StateManagement/*``Coroutine/*` 以及对应测试后确认:
- `events.md``property.md``logging.md` 仍带有旧版“大而全 API 列表”写法,与当前公开入口和推荐边界不匹配
- `state-management.md``coroutine.md` 已和当前 runtime / 测试语义基本对齐,本轮无需为了统一文风做额外重写
- 重写 `events.md`,使其回到“上下文入口、`EventBus` / `EnhancedEventBus`、优先级传播、局部事件对象、与 Store / CQRS
的边界”的当前结构
- 重写 `property.md`,使其回到“字段级响应式值、何时继续使用 `BindableProperty<T>`、何时切到 `Store<TState>`”的当前结构,
并补充 `BindableProperty<T>.Comparer` 按闭合泛型共享的兼容注意点
- 重写 `logging.md`,使其回到“`LoggerFactoryResolver` 默认行为、`ArchitectureConfiguration` 日志 provider 配置、
`IStructuredLogger` / `LogContext`、provider 替换边界”的当前结构
- 执行 `cd docs && bun run build` 通过,说明本轮 `core` 专题页收口没有破坏文档站构建
### 当前结论RP-005
- 本轮计划中的 `core` 剩余高风险页面已完成核对;`state-management``coroutine` 经复核后可继续保留
- `core` 栏目下一步不再需要围绕这五页反复停留,后续重心应转到 `docs/zh-CN/game/*``docs/zh-CN/source-generators/*`
### 下一步RP-005
1. 继续核对 `docs/zh-CN/game/*`,优先处理仍引用旧安装方式、旧状态系统或旧 UI / Scene 接法的页面
2. 再推进 `docs/zh-CN/source-generators/*`,重点核对生成器 wiring、包关系与最小接入示例
3. 若 active trace 继续累计多个已完成恢复点,按 `archive/traces/` 粒度归档旧阶段细节

View File

@ -1,602 +1,130 @@
# Events 包使用说明
# Events
## 概述
`GFramework.Core.Events` 是架构内的轻量广播层。它适合表达“某件事已经发生”的运行时信号、模块间松耦合通知,
以及为旧模块保留 `EventBus` 语义;如果你需要请求/响应、pipeline behavior 或 handler registry优先使用
[cqrs](./cqrs.md)。
Events 包提供了一套完整的事件系统实现了观察者模式Observer Pattern。通过事件系统可以实现组件间的松耦合通信支持无参和带参事件、事件注册/注销、以及灵活的事件组合。
## 安装方式
事件系统是 GFramework 架构中组件间通信的核心机制,与命令模式和查询模式共同构成了完整的 CQRS 架构。
## 核心接口
### IEvent
基础事件接口,定义了事件注册的基本功能。
**核心方法:**
```csharp
IUnRegister Register(Action onEvent); // 注册事件处理函数
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
### IUnRegister
事件实现位于 `GFramework.Core`,抽象接口位于 `GFramework.Core.Abstractions`
注销接口,用于取消事件注册。
## 最常用入口
**核心方法:**
如果你已经在 `ArchitectureContext` 或任何 `IContextAware` 对象里,最常见的入口仍然是:
- `SendEvent<TEvent>()`
- `SendEvent(eventData)`
- `RegisterEvent(Action<TEvent>)`
- `UnRegisterEvent(Action<TEvent>)`
示例:
```csharp
void UnRegister(); // 执行注销操作
```
using GFramework.Core.Extensions;
using GFramework.Core.System;
### IUnRegisterList
public sealed record PlayerDiedEvent(int PlayerId);
注销列表接口,用于批量管理注销对象。
**属性:**
```csharp
IList<IUnRegister> UnregisterList { get; } // 获取注销列表
```
### IEventBus
事件总线接口,提供基于类型的事件发送和注册。
**核心方法:**
```csharp
IUnRegister Register<T>(Action<T> onEvent); // 注册类型化事件
void Send<T>(T e); // 发送事件实例
void Send<T>() where T : new(); // 发送事件(自动创建实例)
void UnRegister<T>(Action<T> onEvent); // 注销事件监听器
```
## 核心类
### EasyEvent
无参事件类,支持注册、注销和触发无参事件。
**核心方法:**
```csharp
IUnRegister Register(Action onEvent); // 注册事件监听器
void Trigger(); // 触发事件
```
**使用示例:**
```csharp
// 创建事件
var onClicked = new EasyEvent();
// 注册监听
var unregister = onClicked.Register(() =>
public sealed class CombatSystem : AbstractSystem
{
Console.WriteLine("Button clicked!");
});
protected override void OnInit()
{
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied);
}
// 触发事件
onClicked.Trigger();
private void OnPlayerDied(PlayerDiedEvent @event)
{
Logger.Info("Player died: {0}", @event.PlayerId);
}
// 取消注册
unregister.UnRegister();
public void KillPlayer(int playerId)
{
this.SendEvent(new PlayerDiedEvent(playerId));
}
}
```
### Event`<T>`
如果你在架构外单独使用,也可以直接构造 `EventBus`
单参数泛型事件类,支持一个参数的事件。
## EventBus 与 EnhancedEventBus
**核心方法:**
默认实现是 `EventBus`,提供类型化发送与订阅:
```csharp
IUnRegister Register(Action<T> onEvent); // 注册事件监听器
void Trigger(T eventData); // 触发事件并传递参数
```
using GFramework.Core.Events;
**使用示例:**
```csharp
// 创建带参数的事件
var onScoreChanged = new Event<int>();
// 注册监听
onScoreChanged.Register(newScore =>
{
Console.WriteLine($"Score changed to: {newScore}");
});
// 触发事件并传递参数
onScoreChanged.Trigger(100);
```
### Event<T, TK>
双参数泛型事件类。
**核心方法:**
```csharp
IUnRegister Register(Action<T, TK> onEvent); // 注册事件监听器
void Trigger(T param1, TK param2); // 触发事件并传递两个参数
```
**使用示例:**
```csharp
// 伤害事件:攻击者、伤害值
var onDamageDealt = new Event<string, int>();
onDamageDealt.Register((attacker, damage) =>
{
Console.WriteLine($"{attacker} dealt {damage} damage!");
});
onDamageDealt.Trigger("Player", 50);
```
### EasyEvents
全局事件管理器,提供类型安全的事件注册和获取。
**核心方法:**
```csharp
static void Register<T>() where T : IEvent, new(); // 注册事件类型
static T Get<T>() where T : IEvent, new(); // 获取事件实例
static T GetOrAddEvent<T>() where T : IEvent, new(); // 获取或创建事件实例
```
**使用示例:**
```csharp
// 注册全局事件类型
EasyEvents.Register<GameStartEvent>();
// 获取事件实例
var gameStartEvent = EasyEvents.Get<GameStartEvent>();
// 注册监听
gameStartEvent.Register(() =>
{
Console.WriteLine("Game started!");
});
// 触发事件
gameStartEvent.Trigger();
```
### EventBus
类型化事件系统,支持基于类型的事件发送和注册。这是架构中默认的事件总线实现。
**核心方法:**
```csharp
IUnRegister Register<T>(Action<T> onEvent); // 注册类型化事件
void Send<T>(T e); // 发送事件实例
void Send<T>() where T : new(); // 发送事件(自动创建实例)
void UnRegister<T>(Action<T> onEvent); // 注销事件监听器
```
**使用示例:**
```csharp
// 使用全局事件系统
var eventBus = new EventBus();
// 注册类型化事件
eventBus.Register<PlayerDiedEvent>(e =>
eventBus.Register<PlayerJoinedEvent>(e =>
{
Console.WriteLine($"Player died at position: {e.Position}");
Console.WriteLine(e.Name);
});
// 发送事件(传递实例)
eventBus.Send(new PlayerDiedEvent
{
Position = new Vector3(10, 0, 5)
});
// 发送事件(自动创建实例)
eventBus.Send<PlayerDiedEvent>();
// 注销事件监听器
eventBus.UnRegister<PlayerDiedEvent>(OnPlayerDied);
eventBus.Send(new PlayerJoinedEvent("Alice"));
```
### DefaultUnRegister
如果你还需要统计、过滤或弱引用订阅,可以改用 `EnhancedEventBus`。它在 `EventBus` 基础上额外提供:
默认注销器实现,封装注销回调。
- `Statistics`
- `SendFilterable(...)` / `RegisterFilterable(...)`
- `SendWeak(...)` / `RegisterWeak(...)`
**使用示例:**
这类能力更适合工具层、编辑器层或长生命周期对象,不必默认扩散到每个业务事件。
## 优先级、传播与上下文事件
当事件处理顺序或“是否继续传播”本身就是语义的一部分时,使用优先级入口:
```csharp
Action onUnregister = () => Console.WriteLine("Unregistered");
var unregister = new DefaultUnRegister(onUnregister);
using GFramework.Core.Abstractions.Events;
using GFramework.Core.Events;
// 执行注销
unregister.UnRegister();
```
public sealed record InputCommand(string Name);
### OrEvent
var eventBus = new EventBus();
事件或运算组合器,当任意一个事件触发时触发。
**核心方法:**
```csharp
OrEvent Or(IEvent @event); // 添加要组合的事件
```
**使用示例:**
```csharp
var onAnyInput = new OrEvent()
.Or(onKeyPressed)
.Or(onMouseClicked)
.Or(onTouchDetected);
// 当上述任意事件触发时,执行回调
onAnyInput.Register(() =>
eventBus.RegisterWithContext<InputCommand>(ctx =>
{
Console.WriteLine("Input detected!");
});
```
### UnRegisterList
批量管理注销对象的列表。
**核心方法:**
```csharp
void Add(IUnRegister unRegister); // 添加注销器到列表
void UnRegisterAll(); // 批量注销所有事件
```
**使用示例:**
```csharp
var unregisterList = new UnRegisterList();
// 添加到列表
someEvent.Register(OnEvent).AddToUnregisterList(unregisterList);
// 批量注销
unregisterList.UnRegisterAll();
```
### ArchitectureEvents
定义了架构生命周期相关的事件。
**包含事件:**
- `ArchitectureLifecycleReadyEvent` - 架构生命周期准备就绪
- `ArchitectureDestroyingEvent` - 架构销毁中
- `ArchitectureDestroyedEvent` - 架构已销毁
- `ArchitectureFailedInitializationEvent` - 架构初始化失败
## 在架构中使用事件
### 定义事件类
```csharp
// 简单事件
public struct GameStartedEvent { }
// 带数据的事件
public struct PlayerDiedEvent
{
public Vector3 Position;
public string Cause;
}
// 复杂事件
public struct LevelCompletedEvent
{
public int LevelId;
public float CompletionTime;
public int Score;
public List<string> Achievements;
}
```
### Model 中发送事件
```csharp
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
protected override void OnInit()
if (ctx.Data.Name == "Pause")
{
// 监听生命值变化
Health.Register(newHealth =>
{
if (newHealth <= 0)
{
// 发送玩家死亡事件
this.SendEvent(new PlayerDiedEvent
{
Position = Position,
Cause = "Health depleted"
});
}
});
Console.WriteLine("Pause handled");
ctx.MarkAsHandled();
}
}
}, priority: 10);
eventBus.Send(new InputCommand("Pause"), EventPropagation.UntilHandled);
```
### System 中发送事件
当前公开语义是:
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit() { }
public void DealDamage(Character attacker, Character target, int damage)
{
target.Health -= damage;
// 发送伤害事件
this.SendEvent(new DamageDealtEvent
{
Attacker = attacker.Name,
Target = target.Name,
Damage = damage
});
}
}
```
- `Register<T>(handler, priority)`:按优先级订阅
- `RegisterWithContext<T>(...)`:拿到 `EventContext<T>`
- `EventPropagation.All`:广播给全部监听器
- `EventPropagation.UntilHandled`:直到上下文事件被标记为 handled
- `EventPropagation.Highest`:只执行最高优先级层
### Controller 中注册事件
## 局部事件对象
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
如果事件只在一个对象或一个小模块内部流动,不必一定挂到 `EventBus`。当前仍可直接使用:
[ContextAware]
public partial class GameController : IController
{
private IUnRegisterList _unregisterList = new UnRegisterList();
- `EasyEvent`
- `Event<T>`
- `Event<T1, T2>`
- `OrEvent`
- `EventListenerScope<TEvent>`
public void Initialize()
{
// 注册多个事件
this.RegisterEvent<GameStartedEvent>(OnGameStarted)
.AddToUnregisterList(_unregisterList);
这类类型更适合局部组合和 UI/工具层内聚逻辑,不适合作为全局消息总线的替代品。
this.RegisterEvent<PlayerDiedEvent>(OnPlayerDied)
.AddToUnregisterList(_unregisterList);
## 与 Store / CQRS 的边界
this.RegisterEvent<LevelCompletedEvent>(OnLevelCompleted)
.AddToUnregisterList(_unregisterList);
}
- 轻量运行时广播:`EventBus`
- 聚合状态演进:`Store<TState>`,必要时用 `BridgeToEventBus(...)` 兼容旧事件消费者
- 新业务请求模型:`GFramework.Cqrs`
private void OnGameStarted(GameStartedEvent e)
{
Console.WriteLine("Game started!");
}
private void OnPlayerDied(PlayerDiedEvent e)
{
Console.WriteLine($"Player died at {e.Position}: {e.Cause}");
ShowGameOverScreen();
}
private void OnLevelCompleted(LevelCompletedEvent e)
{
Console.WriteLine($"Level {e.LevelId} completed! Score: {e.Score}");
ShowVictoryScreen(e);
}
public void Cleanup()
{
_unregisterList.UnRegisterAll();
}
}
```
## 高级用法
### 1. 事件链式组合
```csharp
// 使用 Or 组合多个事件
var onAnyDamage = new OrEvent()
.Or(onPhysicalDamage)
.Or(onMagicDamage)
.Or(onPoisonDamage);
onAnyDamage.Register(() =>
{
PlayDamageSound();
});
```
### 2. 事件过滤
```csharp
// 只处理高伤害事件
this.RegisterEvent<DamageDealtEvent>(e =>
{
if (e.Damage >= 50)
{
ShowCriticalHitEffect();
}
});
```
### 3. 事件转发
```csharp
public class EventBridge : AbstractSystem
{
protected override void OnInit()
{
// 将内部事件转发为公共事件
this.RegisterEvent<InternalPlayerDiedEvent>(e =>
{
this.SendEvent(new PublicPlayerDiedEvent
{
PlayerId = e.Id,
Timestamp = DateTime.UtcNow
});
});
}
}
```
### 4. 临时事件监听
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class TutorialController : IController
{
public void Initialize()
{
// 只监听一次
IUnRegister unregister = null;
unregister = this.RegisterEvent<FirstEnemyKilledEvent>(e =>
{
ShowTutorialComplete();
unregister?.UnRegister(); // 立即注销
});
}
}
```
### 5. 条件事件
```csharp
public class AchievementSystem : AbstractSystem
{
private int _killCount = 0;
protected override void OnInit()
{
this.RegisterEvent<EnemyKilledEvent>(e =>
{
_killCount++;
// 条件满足时发送成就事件
if (_killCount >= 100)
{
this.SendEvent(new AchievementUnlockedEvent
{
AchievementId = "kill_100_enemies"
});
}
});
}
}
```
## 生命周期管理
### 使用 UnRegisterList
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class MyController : IController
{
// 统一管理所有注销对象
private IUnRegisterList _unregisterList = new UnRegisterList();
public void Initialize()
{
// 所有注册都添加到列表
this.RegisterEvent<Event1>(OnEvent1)
.AddToUnregisterList(_unregisterList);
this.RegisterEvent<Event2>(OnEvent2)
.AddToUnregisterList(_unregisterList);
}
public void Cleanup()
{
// 一次性注销所有
_unregisterList.UnRegisterAll();
}
}
```
## 最佳实践
1. **事件命名规范**
- 使用过去式:`PlayerDiedEvent``LevelCompletedEvent`
- 使用 `Event` 后缀:便于识别
- 使用结构体:减少内存分配
2. **事件数据设计**
- 只包含必要信息
- 使用值类型struct提高性能
- 避免传递可变引用
3. **避免事件循环**
- 事件处理器中谨慎发送新事件
- 使用命令打破循环依赖
4. **合理使用事件**
- 用于通知状态变化
- 用于跨模块通信
- 不用于返回数据(使用 Query
5. **注销管理**
- 始终注销事件监听
- 使用 `IUnRegisterList` 批量管理
- 在适当的生命周期点调用 `Cleanup()`
6. **性能考虑**
- 避免频繁触发的事件(如每帧)
- 事件处理器保持轻量
- 使用结构体事件减少 GC
7. **事件设计原则**
- 高内聚:事件应该代表一个完整的业务概念
- 低耦合:事件发送者不需要知道接收者
- 可测试:事件应该易于模拟和测试
## 事件 vs 其他通信方式
| 方式 | 适用场景 | 优点 | 缺点 |
|----------------------|--------------|-----------|---------|
| **Event** | 状态变化通知、跨模块通信 | 松耦合、一对多 | 难以追踪调用链 |
| **Command** | 执行操作、修改状态 | 封装逻辑、可撤销 | 单向通信 |
| **Query** | 查询数据 | 职责清晰、有返回值 | 同步调用 |
| **BindableProperty** | UI 数据绑定 | 自动更新、响应式 | 仅限单一属性 |
## 事件系统架构
事件系统在 GFramework 中的架构位置:
```
Architecture (架构核心)
├── EventBus (事件总线)
├── CommandBus (命令总线)
├── QueryBus (查询总线)
└── IocContainer (IoC容器)
Components (组件)
├── Model (发送事件)
├── System (发送/接收事件)
└── Controller (接收事件)
```
## 相关包
- [`architecture`](./architecture.md) - 提供全局事件系统
- [`extensions`](./extensions.md) - 提供事件扩展方法
- [`property`](./property.md) - 可绑定属性基于事件实现
- **Controller** - 控制器监听事件(接口定义在 Core.Abstractions 中)
- [`model`](./model.md) - 模型发送事件
- [`system`](./system.md) - 系统发送和监听事件
- [`command`](./command.md) - 与事件配合实现 CQRS
- [`query`](./query.md) - 与事件配合实现 CQRS
一个简单判断规则是:如果你关心“谁来处理、是否有返回值、是否要挂 pipeline”用 CQRS如果你只是广播
“这件事发生了”,事件系统更直接。

View File

@ -1,364 +1,86 @@
# Logging 包使用说明
# Logging
## 概述
`GFramework.Core.Logging` 是 Core runtime 的默认日志实现。只加载抽象层时,`LoggerFactoryResolver` 会退回
silent provider加载 `GFramework.Core` 或在 `ArchitectureConfiguration` 里显式提供 provider 后,日志才会
真正输出。
Logging 包提供了灵活的日志系统,支持多级别日志记录。默认日志级别为 `Info`,确保框架的关键操作都能被记录下来。
## 核心接口
### ILogger
日志记录器接口,定义了日志记录的基本功能。
**核心方法:**
## 最小用法
```csharp
// 日志级别检查
bool IsTraceEnabled();
bool IsDebugEnabled();
bool IsInfoEnabled();
bool IsWarnEnabled();
bool IsErrorEnabled();
bool IsFatalEnabled();
using GFramework.Core.Abstractions.Logging;
// 记录日志
void Trace(string msg);
void Trace(string format, object arg);
void Trace(string format, object arg1, object arg2);
void Trace(string format, params object[] arguments);
void Trace(string msg, Exception t);
var logger = LoggerFactoryResolver.Provider.CreateLogger("Bootstrap");
void Debug(string msg);
void Debug(string format, object arg);
void Debug(string format, object arg1, object arg2);
void Debug(string format, params object[] arguments);
void Debug(string msg, Exception t);
void Info(string msg);
void Info(string format, object arg);
void Info(string format, object arg1, object arg2);
void Info(string format, params object[] arguments);
void Info(string msg, Exception t);
void Warn(string msg);
void Warn(string format, object arg);
void Warn(string format, object arg1, object arg2);
void Warn(string format, params object[] arguments);
void Warn(string msg, Exception t);
void Error(string msg);
void Error(string format, object arg);
void Error(string format, object arg1, object arg2);
void Error(string format, params object[] arguments);
void Error(string msg, Exception t);
void Fatal(string msg);
void Fatal(string format, object arg);
void Fatal(string format, object arg1, object arg2);
void Fatal(string format, params object[] arguments);
void Fatal(string msg, Exception t);
// 获取日志器名称
string Name();
logger.Info("Application started");
logger.Warn("Config file missing");
```
### ILoggerFactory
默认 `ArchitectureConfiguration` 会把 provider 配成 `ConsoleLoggerFactoryProvider`,最小级别是 `Info`。如果你
直接走标准 `Architecture` 启动路径,这条配置会自动生效。
日志工厂接口,用于创建日志记录器实例。
**核心方法:**
## 在 Architecture 中调整日志级别
```csharp
ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info);
```
using GFramework.Core.Architectures;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Abstractions.Properties;
using GFramework.Core.Logging;
### ILoggerFactoryProvider
日志工厂提供程序接口,用于获取日志工厂。
**核心方法:**
```csharp
ILoggerFactory GetLoggerFactory();
ILogger CreateLogger(string name);
```
### LogLevel
日志级别枚举。
```csharp
public enum LogLevel
var configuration = new ArchitectureConfiguration
{
Trace = 0, // 最详细的跟踪信息
Debug = 1, // 调试信息
Info = 2, // 一般信息(默认级别)
Warning = 3, // 警告信息
Error = 4, // 错误信息
Fatal = 5 // 致命错误
}
```
## 核心类
### AbstractLogger
抽象日志基类,封装了日志级别判断、格式化与异常处理逻辑。平台日志器只需实现 `Write` 方法即可。
**使用示例:**
```csharp
public class CustomLogger : AbstractLogger
{
public CustomLogger(string? name = null, LogLevel minLevel = LogLevel.Info)
: base(name, minLevel)
LoggerProperties = new LoggerProperties
{
}
protected override void Write(LogLevel level, string message, Exception? exception)
{
// 自定义日志输出逻辑
var logMessage = $"[{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}] [{level}] {message}";
if (exception != null)
logMessage += $"\n{exception}";
Console.WriteLine(logMessage);
}
}
```
### ConsoleLogger
控制台日志记录器实现,支持彩色输出。
**使用示例:**
```csharp
// 创建控制台日志记录器
var logger = new ConsoleLogger("MyLogger", LogLevel.Debug);
// 记录不同级别的日志
logger.Info("应用程序启动");
logger.Debug("调试信息");
logger.Warn("警告信息");
logger.Error("错误信息");
logger.Fatal("致命错误");
```
**输出格式:**
```
[2025-01-09 01:40:00.000] INFO [MyLogger] 应用程序启动
[2025-01-09 01:40:01.000] DEBUG [MyLogger] 调试信息
[2025-01-09 01:40:02.000] WARN [MyLogger] 警告信息
```
**日志级别颜色:**
- **Trace**: 深灰色
- **Debug**: 青色
- **Info**: 白色
- **Warning**: 黄色
- **Error**: 红色
- **Fatal**: 洋红色
**构造函数参数:**
- `name`:日志器名称,默认为 "ROOT"
- `minLevel`:最低日志级别,默认为 LogLevel.Info
- `writer`TextWriter 输出流,默认为 Console.Out
- `useColors`:是否使用颜色,默认为 true仅在输出到控制台时生效
### ConsoleLoggerFactory
控制台日志工厂,用于创建控制台日志记录器实例。
**使用示例:**
```csharp
var factory = new ConsoleLoggerFactory();
var logger = factory.GetLogger("MyModule", LogLevel.Debug);
logger.Info("日志记录器创建成功");
```
### ConsoleLoggerFactoryProvider
控制台日志工厂提供程序实现。
**使用示例:**
```csharp
var provider = new ConsoleLoggerFactoryProvider();
provider.MinLevel = LogLevel.Debug; // 设置最低日志级别
var logger = provider.CreateLogger("MyApp");
logger.Info("应用程序启动");
```
### LoggerFactoryResolver
日志工厂提供程序解析器,用于管理和提供日志工厂提供程序实例。
**使用示例:**
```csharp
// 设置日志工厂提供程序
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
// 设置最小日志级别
LoggerFactoryResolver.MinLevel = LogLevel.Debug;
// 获取日志记录器
var logger = LoggerFactoryResolver.Provider.CreateLogger("MyApp");
logger.Info("应用程序启动");
```
## 在架构中使用日志
### 1. 在 Architecture 中使用
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("GameArchitecture");
logger.Info("游戏架构初始化开始");
RegisterModel(new PlayerModel());
RegisterSystem(new GameSystem());
logger.Info("游戏架构初始化完成");
}
}
```
### 2. 在 System 中使用
```csharp
public class CombatSystem : AbstractSystem
{
protected override void OnInit()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
logger.Info("战斗系统初始化完成");
}
protected override void OnDestroy()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("CombatSystem");
logger.Info("战斗系统已销毁");
}
}
```
### 3. 在 Model 中使用
```csharp
public class PlayerModel : AbstractModel
{
protected override void OnInit()
{
var logger = LoggerFactoryResolver.Provider.CreateLogger("PlayerModel");
logger.Info("玩家模型初始化完成");
}
}
```
### 4. 自定义日志级别
```
public class DebugLogger : AbstractLogger
{
public DebugLogger() : base("Debug", LogLevel.Debug)
{
}
protected override void Write(LogLevel level, string message, Exception? exception)
{
// 只输出调试及更高级别的日志
if (level >= LogLevel.Debug)
LoggerFactoryProvider = new ConsoleLoggerFactoryProvider
{
Console.WriteLine($"[{level}] {message}");
if (exception != null)
Console.WriteLine(exception);
MinLevel = LogLevel.Debug
}
}
};
```
如果你只是想减少噪音或临时打开 `Debug`,通常只调 `MinLevel` 就够了。
## 结构化日志与上下文
默认 Core logger 实现支持 `IStructuredLogger``LogContext`。当你需要把 `requestId``sceneName` 之类的
上下文随异步流透传时,优先用上下文属性,而不是把所有信息拼进字符串。
```csharp
using GFramework.Core.Abstractions.Logging;
var logger = LoggerFactoryResolver.Provider.CreateLogger("Matchmaking");
using (LogContext.Push("RequestId", requestId))
{
if (logger is IStructuredLogger structured)
{
structured.Log(
LogLevel.Info,
"Player matched",
("PlayerId", playerId),
("RoomId", roomId));
}
}
```
## 日志级别说明
## 当前仓库内置的常用实现
| 级别 | 说明 | 使用场景 |
|-------------|----------|-------------------|
| **Trace** | 最详细的跟踪信息 | 调试复杂的执行流程,记录函数调用等 |
| **Debug** | 调试信息 | 开发阶段,记录变量值、流程分支等 |
| **Info** | 一般信息 | 记录重要的业务流程和系统状态 |
| **Warning** | 警告信息 | 可能的问题但不中断程序执行 |
| **Error** | 错误信息 | 影响功能但不致命的问题 |
| **Fatal** | 致命错误 | 导致程序无法继续运行的严重错误 |
- `ConsoleLoggerFactoryProvider`
- `ConsoleLoggerFactory`
- `CompositeLogger`
- `LoggingConfigurationLoader`
## 最佳实践
如果你需要文件输出、rolling file、async appender 或 JSON formatter可以先用
`LoggingConfigurationLoader` 读取 `LoggingConfiguration`,再把自定义 `ILoggerFactoryProvider` 挂到
`ArchitectureConfiguration.LoggerProperties.LoggerFactoryProvider``LoggerFactoryResolver.Provider`
1. **使用合适的日志级别**
- 使用 `Info` 记录重要业务流程
- 使用 `Debug` 记录调试信息
- 使用 `Warning` 记录异常情况
- 使用 `Error` 记录错误但不影响程序运行
- 使用 `Fatal` 记录严重错误
## 什么时候该换 provider
2. **提供上下文信息**
```csharp
logger.Info($"用户登录成功: UserId={userId}, UserName={userName}");
```
下面这些场景通常不该只靠改 `MinLevel`
3. **异常日志记录**
```csharp
try
{
// 业务逻辑
}
catch (Exception ex)
{
logger.Error("数据库操作失败", ex);
}
```
- 需要文件输出、rolling file 或 async appender
- 需要按 namespace / level 做过滤
- 需要 JSON 格式日志
- 需要组合多个 appender
4. **分类使用日志**
```csharp
var dbLogger = LoggerFactoryResolver.Provider.CreateLogger("Database");
var netLogger = LoggerFactoryResolver.Provider.CreateLogger("Network");
dbLogger.Info("查询用户数据");
netLogger.Debug("发送HTTP请求");
```
5. **在框架组件中合理使用日志**
```csharp
// 在系统初始化时记录
var logger = LoggerFactoryResolver.Provider.CreateLogger("System");
logger.Info("系统初始化完成");
```
## 注意事项
1. **日志级别检查**
- 每个日志方法都会自动检查日志级别
- 如果当前级别低于最小级别,不会输出日志
2. **格式化参数**
- 支持字符串格式化参数
- 支持异常信息传递
3. **ConsoleLogger 的额外参数**
- ConsoleLogger 现在支持自定义TextWriter输出流
- 支持禁用颜色输出的功能useColors参数
## 相关包
- [architecture](./architecture.md) - 架构核心,使用日志系统记录生命周期事件
- [property](./property.md) - 可绑定属性基于事件系统实现
- [extensions](./extensions.md) - 提供便捷的扩展方法
---
**许可证**: Apache 2.0
这时更合理的做法是保留 `ILogger` 调用面不变,只替换 provider / factory / formatter / appender 组合。

View File

@ -1,477 +1,97 @@
# Property 包使用说明
# Property
## 概述
`GFramework.Core.Property` 负责字段级响应式值。它最适合“一个字段变化就足以驱动视图或局部业务逻辑”的场景;
如果你的状态已经是聚合状态树、需要 reducer / middleware / history再切到
[state-management](./state-management.md)。
Property 包提供了可绑定属性BindableProperty的实现支持属性值的监听和响应式编程。这是实现数据绑定和响应式编程的核心组件。
## 安装方式
BindableProperty 是 GFramework 中 Model 层数据管理的基础,通过事件机制实现属性变化的通知。
> 对于简单字段和局部 UI 绑定,`BindableProperty<T>` 仍然是首选方案。
> 如果你需要统一管理复杂状态树、通过 action / reducer 演进状态,或复用局部状态选择器,
> 请同时参考 [`state-management`](./state-management)。
## 核心接口
### IReadonlyBindableProperty`<T>`
只读可绑定属性接口,提供属性值的读取和变更监听功能。
**核心成员:**
```csharp
// 获取属性值
T Value { get; }
// 注册监听(不立即触发回调)
IUnRegister Register(Action<T> onValueChanged);
// 注册监听并立即触发回调传递当前值
IUnRegister RegisterWithInitValue(Action<T> action);
// 取消监听
void UnRegister(Action<T> onValueChanged);
```bash
dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
### IBindableProperty`<T>`
## 最常用类型
可绑定属性接口,继承自只读接口,增加了修改能力。
当前最常见的公开类型是:
**核心成员:**
- `IReadonlyBindableProperty<T>`
- `IBindableProperty<T>`
- `BindableProperty<T>`
一般做法是:内部持有 `BindableProperty<T>`,对外只暴露 `IReadonlyBindableProperty<T>`
## 最小示例
```csharp
// 可读写的属性值
new T Value { get; set; }
using GFramework.Core.Property;
using GFramework.Core.Abstractions.Property;
using GFramework.Core.Model;
// 设置值但不触发事件
void SetValueWithoutEvent(T newValue);
```
## 核心类
### BindableProperty`<T>`
可绑定属性的完整实现。
**核心方法:**
```csharp
// 构造函数
BindableProperty(T defaultValue = default!);
// 属性值
T Value { get; set; }
// 注册监听
IUnRegister Register(Action<T> onValueChanged);
IUnRegister RegisterWithInitValue(Action<T> action);
// 取消监听
void UnRegister(Action<T> onValueChanged);
// 设置值但不触发事件
void SetValueWithoutEvent(T newValue);
// 设置自定义比较器
BindableProperty<T> WithComparer(Func<T, T, bool> comparer);
```
**使用示例:**
```csharp
// 创建可绑定属性
var health = new BindableProperty<int>(100);
// 监听值变化(不会立即触发)
var unregister = health.Register(newValue =>
public sealed class PlayerModel : AbstractModel
{
Console.WriteLine($"Health changed to: {newValue}");
});
// 设置值(会触发监听器)
health.Value = 50; // 输出: Health changed to: 50
// 取消监听
unregister.UnRegister();
// 设置值但不触发事件
health.SetValueWithoutEvent(75);
```
**高级功能:**
```csharp
// 1. 注册并立即获得当前值
health.RegisterWithInitValue(value =>
{
Console.WriteLine($"Current health: {value}"); // 立即输出当前值
// 后续值变化时也会调用
});
// 2. 自定义比较器(静态方法)
BindableProperty<int>.Comparer = (a, b) => Math.Abs(a - b) < 1;
// 3. 使用实例方法设置比较器
var position = new BindableProperty<Vector3>(Vector3.Zero)
.WithComparer((a, b) => a.DistanceTo(b) < 0.01f); // 距离小于0.01认为相等
// 4. 字符串比较器示例
var name = new BindableProperty<string>("Player")
.WithComparer((a, b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase));
```
### BindablePropertyUnRegister`<T>`
可绑定属性的注销器,负责清理监听。
**使用示例:**
```csharp
var unregister = health.Register(OnHealthChanged);
// 当需要取消监听时
unregister.UnRegister();
```
## BindableProperty 工作原理
BindableProperty 基于事件系统实现属性变化通知:
1. **值设置**:当设置 `Value` 属性时,首先进行值比较
2. **变化检测**:使用 `EqualityComparer<T>.Default` 或自定义比较器检测值变化
3. **事件触发**:如果值发生变化,调用所有注册的回调函数
4. **内存管理**:通过 `IUnRegister` 机制管理监听器的生命周期
## 在 Model 中使用
### 什么时候继续使用 BindableProperty
以下场景仍然优先推荐 `BindableProperty<T>`
- 单个字段变化就能驱动视图更新
- 状态范围局限在单个 Model 内
- 不需要统一的 action / reducer 写入入口
- 不需要从聚合状态树中复用局部选择逻辑
如果你的状态已经演化为“多个字段必须一起更新”或“多个模块共享同一聚合状态”,
可以在 Model 内部组合 `Store<TState>`,而不是把所有字段都继续拆成独立属性。
### 与 Store / StateMachine 的边界
- `BindableProperty<T>`:字段级响应式值
- `Store<TState>`:聚合状态容器,负责统一归约状态变化
- `StateMachine`:流程状态切换,不负责数据状态归约
一个复杂 Model 可以同时持有 Store 和 BindableProperty
```csharp
public class PlayerStateModel : AbstractModel
{
public Store<PlayerState> Store { get; } = new(new PlayerState(100, "Player"));
public BindableProperty<bool> IsDirty { get; } = new(false);
protected override void OnInit()
{
Store.RegisterReducer<DamageAction>((state, action) =>
state with { Health = Math.Max(0, state.Health - action.Amount) });
}
}
public sealed record PlayerState(int Health, string Name);
public sealed record DamageAction(int Amount);
```
### 定义可绑定属性
```csharp
public class PlayerModel : AbstractModel
{
// 可读写属性
public BindableProperty<string> Name { get; } = new("Player");
public BindableProperty<int> Level { get; } = new(1);
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> MaxHealth { get; } = new(100);
public BindableProperty<Vector3> Position { get; } = new(Vector3.Zero);
// 只读属性(外部只能读取和监听)
public IReadonlyBindableProperty<int> ReadonlyHealth => Health;
protected override void OnInit()
public void Damage(int amount)
{
// 内部监听属性变化
Health.Register(hp =>
{
if (hp <= 0)
{
this.SendEvent(new PlayerDiedEvent());
}
else if (hp < MaxHealth.Value * 0.3f)
{
this.SendEvent(new LowHealthWarningEvent());
}
});
// 监听等级变化
Level.Register(newLevel =>
{
this.SendEvent(new PlayerLevelUpEvent { NewLevel = newLevel });
});
}
// 业务方法
public void TakeDamage(int damage)
{
Health.Value = Math.Max(0, Health.Value - damage);
}
public void Heal(int amount)
{
Health.Value = Math.Min(MaxHealth.Value, Health.Value + amount);
}
public float GetHealthPercentage()
{
return (float)Health.Value / MaxHealth.Value;
Health.Value = Math.Max(0, Health.Value - amount);
}
}
```
## 在 Controller 中监听
### UI 数据绑定
监听方式:
```csharp
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class PlayerUI : Control, IController
var unRegister = playerModel.ReadonlyHealth.RegisterWithInitValue(health =>
{
[Export] private Label _healthLabel;
[Export] private Label _nameLabel;
[Export] private ProgressBar _healthBar;
private IUnRegisterList _unregisterList = new UnRegisterList();
public override void _Ready()
{
var playerModel = this.GetModel<PlayerModel>();
// 绑定生命值到UI立即显示当前值
playerModel.Health
.RegisterWithInitValue(health =>
{
_healthLabel.Text = $"HP: {health}/{playerModel.MaxHealth.Value}";
_healthBar.Value = (float)health / playerModel.MaxHealth.Value * 100;
})
.AddToUnregisterList(_unregisterList);
// 绑定最大生命值
playerModel.MaxHealth
.RegisterWithInitValue(maxHealth =>
{
_healthBar.MaxValue = maxHealth;
})
.AddToUnregisterList(_unregisterList);
// 绑定名称
playerModel.Name
.RegisterWithInitValue(name =>
{
_nameLabel.Text = name;
})
.AddToUnregisterList(_unregisterList);
// 绑定位置(仅用于调试显示)
playerModel.Position
.RegisterWithInitValue(pos =>
{
// 仅在调试模式下显示
#if DEBUG
Console.WriteLine($"Player position: {pos}");
#endif
})
.AddToUnregisterList(_unregisterList);
}
public override void _ExitTree()
{
_unregisterList.UnRegisterAll();
}
}
Console.WriteLine($"Current HP: {health}");
});
```
## 常见使用模式
## 当前公开语义
### 1. 双向绑定
- `Value`
- 读写当前值;只有值被判定为“真的变化”时才会触发回调
- `Register(...)`
- 订阅后续变化,不会立即回放当前值
- `RegisterWithInitValue(...)`
- 先回放当前值,再继续订阅
- `SetValueWithoutEvent(...)`
- 更新值但不触发通知
- `UnRegister(...)`
- 显式移除某个处理器
- `WithComparer(...)`
- 改写值变化判定逻辑
```c#
// Model
public class SettingsModel : AbstractModel
{
public BindableProperty<float> MasterVolume { get; } = new(1.0f);
protected override void OnInit() { }
}
## 一个需要注意的兼容点
// UI Controller
[ContextAware]
public partial class VolumeSlider : HSlider, IController
{
private BindableProperty<float> _volumeProperty;
`BindableProperty<T>.Comparer` 是按闭合泛型 `T` 共享的静态比较器,`WithComparer(...)` 本质上会改写这一共享
比较器。也就是说,多个 `BindableProperty<int>` 实例会观察到同一比较规则;只有当你确定整个 `T` 族都要共享同一
判等语义时,再去改它。
public override void _Ready()
{
_volumeProperty = this.GetModel<SettingsModel>().MasterVolume;
## 什么时候继续用 Property
// Model -> UI
_volumeProperty.RegisterWithInitValue(vol => Value = vol)
.UnRegisterWhenNodeExitTree(this);
下面这些场景仍然优先使用 `BindableProperty<T>`
// UI -> Model
ValueChanged += newValue => _volumeProperty.Value = (float)newValue;
}
}
```
- 单个字段变化就能驱动 UI
- 状态范围局限在单个 Model 或单个页面
- 不需要统一的 action / reducer 写入口
- 不需要撤销/重做、历史快照或中间件
### 2. 计算属性
## 什么时候该切到 Store
```c#
public class PlayerModel : AbstractModel
{
public BindableProperty<int> Health { get; } = new(100);
public BindableProperty<int> MaxHealth { get; } = new(100);
public BindableProperty<float> HealthPercent { get; } = new(1.0f);
protected override void OnInit()
{
// 自动计算百分比
Action updatePercent = () =>
{
HealthPercent.Value = (float)Health.Value / MaxHealth.Value;
};
Health.Register(_ => updatePercent());
MaxHealth.Register(_ => updatePercent());
updatePercent(); // 初始计算
}
}
```
如果状态已经演化为下面这些形态,更适合用 `Store<TState>`
### 3. 属性验证
- 多个字段必须作为一个原子状态一起演进
- 多个模块共享同一聚合状态
- 需要 reducer / middleware / 历史回放
- 需要从整棵状态树中复用局部选择逻辑
```c#
public class PlayerModel : AbstractModel
{
private BindableProperty<int> _health = new(100);
public BindableProperty<int> Health
{
get => _health;
set
{
// 限制范围
var clampedValue = Math.Clamp(value.Value, 0, MaxHealth.Value);
_health.Value = clampedValue;
}
}
public BindableProperty<int> MaxHealth { get; } = new(100);
protected override void OnInit() { }
}
```
迁移时不必一次性抛弃旧绑定风格。当前已经提供:
### 4. 条件监听
- `store.Select(...)`
- `store.ToBindableProperty(...)`
```c#
using GFramework.Core.Abstractions.Controller;
using GFramework.Core.SourceGenerators.Abstractions.Rule;
[ContextAware]
public partial class CombatController : Node, IController
{
public override void _Ready()
{
var playerModel = this.GetModel<PlayerModel>();
// 只在生命值低于30%时显示警告
playerModel.Health.Register(hp =>
{
if (hp < playerModel.MaxHealth.Value * 0.3f)
{
ShowLowHealthWarning();
}
else
{
HideLowHealthWarning();
}
}).UnRegisterWhenNodeExitTree(this);
}
}
```
## 性能优化
### 1. 避免频繁触发
```c#
// 使用 SetValueWithoutEvent 批量修改
public void LoadPlayerData(SaveData data)
{
// 临时关闭事件
Health.SetValueWithoutEvent(data.Health);
Mana.SetValueWithoutEvent(data.Mana);
Gold.SetValueWithoutEvent(data.Gold);
// 最后统一触发一次更新事件
this.SendEvent(new PlayerDataLoadedEvent());
}
```
### 2. 自定义比较器
```c#
// 避免浮点数精度问题导致的频繁触发
var position = new BindableProperty<Vector3>()
.WithComparer((a, b) => a.DistanceTo(b) < 0.001f);
```
## 实现原理
### 值变化检测
```c#
// 使用 EqualityComparer<T>.Default 进行比较
if (!EqualityComparer<T>.Default.Equals(value, MValue))
{
MValue = value;
_mOnValueChanged?.Invoke(value);
}
```
### 事件触发机制
```c#
// 当值变化时触发所有注册的回调
_mOnValueChanged?.Invoke(value);
```
## 最佳实践
1. **在 Model 中定义属性** - BindableProperty 主要用于 Model 层
2. **使用只读接口暴露** - 防止外部随意修改
3. **及时注销监听** - 使用 UnRegisterList 或 UnRegisterWhenNodeExitTree
4. **使用 RegisterWithInitValue** - UI 绑定时立即获取初始值
5. **避免循环依赖** - 属性监听器中修改其他属性要小心
6. **使用自定义比较器** - 对于浮点数等需要精度控制的属性
7. **复杂聚合状态使用 Store** - 当多个字段必须统一演进时,使用 Store 管理聚合状态更清晰
## 相关包
- [`model`](./model.md) - Model 中大量使用 BindableProperty
- [`events`](./events.md) - BindableProperty 基于事件系统实现
- [`state-management`](./state-management) - 复杂状态树的集中式管理方案
- [`extensions`](./extensions.md) - 提供便捷的注销扩展方法
---
**许可证**: Apache 2.0
这意味着你可以先把写路径统一到 `Store<TState>`,再渐进迁移现有 UI 或 Controller 的读取方式。