事件系统 - Snet Docs

事件系统

最后更新: 2026-07-21 命名空间: Snet.Model.@event, Snet.Model.data, Snet.Model.@interface


概述

Snet 事件系统提供了一个线程安全、异步优先的发布/订阅机制,用于驱动程序、核心引擎和外部消费者之间的通信。它同时支持同步(EventHandler<T>)和异步(EventHandlerAsync<T>)事件模式,并内置取消支持和异常隔离。

+------------------+       +------------------+       +-------------------+
|  驱动程序 / 数据源 | ----> |    CoreUnify     | ----> |  订阅者            |
|  (触发事件)       |       |  (分发事件)       |       |  (UI, 服务层)      |
+------------------+       +------------------+       +-------------------+
         |                          |                          |
    OnDataEventHandler        IEvent.OnDataEvent         IEvent.OnDataEvent
    OnInfoEventHandler        IEvent.OnInfoEvent         IEvent.OnInfoEvent
    OnLanguageEventHandler    IEvent.OnLanguageEvent      IEvent.OnLanguageEvent

核心类型

EventArgsAsync -- 异步事件参数基类

所有异步事件参数的基础类,继承自 System.Object

成员 类型 描述
CancellationToken CancellationToken 异步取消令牌(序列化时忽略)
Empty static EventArgsAsync 单例空实例,类似 EventArgs.Empty

工厂方法:

  • CreateOrDefault(CancellationToken) -- 如果令牌不可取消则返回 Empty,否则创建新实例。
// 带取消令牌构造
var args = new EventArgsAsync(cancellationToken);

// 按需创建
var args2 = EventArgsAsync.CreateOrDefault(token);

EventHandlerAsync<TEvent> -- 异步事件委托

public delegate Task EventHandlerAsync<in TEvent>(object? sender, TEvent e)
    where TEvent : EventArgsAsync;

标准 EventHandler<T> 的异步版本。逆变类型参数允许传递派生类型的事件参数。

EventingWrapperAsync<TEvent>(struct 结构体 -- 值类型)

线程安全的结构体,管理 EventHandlerAsync<TEvent> 的订阅、取消订阅和触发。

成员 签名 描述
IsEmpty bool 无订阅者时为 true
构造函数 (string context, Func<Exception, string, CancellationToken, Task> onException) 创建包装器并指定异常回调
AddHandler (EventHandlerAsync<TEvent>?) 订阅处理程序
RemoveHandler (EventHandlerAsync<TEvent>?) 取消订阅
InvokeAsync (object? sender, TEvent) 返回 Task 按顺序触发所有处理程序
Takeover (in EventingWrapperAsync<TEvent>) 从另一个包装器接管所有订阅

关键行为:

  1. 委托缓存: 首次调用 InvokeAsync 时,将调用列表捕获到 _handlers 字段中,避免重复调用 GetInvocationList(),提升性能。
  2. 取消安全: OperationCanceledException 被捕获并静默忽略(任务被取消不算错误)。
  3. 异常隔离: 其他异常通过 onException 回调委托处理。如果没有提供回调,异常会向外传播。
  4. 结构体注意事项: 由于这是值类型,InvokeAsync 在方法入口处将 _handlers 捕获到局部变量中,防止异步状态机快照复制到旧的空值。

事件数据类型

继承层次结构

EventArgsAsync (System.Object)
  |
  +-- BaseModel (Status, Message, Time)
  |     |
  |     +-- EventInfoResult          -- 信息事件结果
  |     |
  |     +-- ResultModel (ResultData) -- 带结果数据
  |           |
  |           +-- EventDataResult    -- 数据事件结果
  |
  +-- EventLanguageResult (Status, Message, Language, Time) -- 语言事件结果

EventDataResult : ResultModel -- 数据事件结果

用于 IEvent.OnDataEvent / OnDataEventAsync 数据采集事件

构造函数 / 工厂方法 描述
EventDataResult() 无参构造
EventDataResult(bool status, string message, object? resultData) 完整构造
EventDataResult(EventDataResult result) 拷贝构造
CreateSuccessResult(string msg) 静态工厂 -- 成功
CreateSuccessResult<T>(string msg, T data) 静态工厂 -- 成功并携带类型化数据
CreateFailureResult(string msg) 静态工厂 -- 失败

ResultModel 继承:object? ResultDataGetSource<T>()、多个 GetDetails(...) 重载。

EventInfoResult : BaseModel -- 信息事件结果

用于 IEvent.OnInfoEvent / OnInfoEventAsync 状态/错误信息事件

构造函数 / 工厂方法 描述
EventInfoResult() 无参构造
EventInfoResult(bool status, string message) 完整构造
EventInfoResult(EventInfoResult result) 拷贝构造
CreateSuccessResult(string msg) 静态工厂 -- 成功
CreateFailureResult(string msg) 静态工厂 -- 失败

BaseModel 继承:bool Statusstring? MessageDateTime Time

EventLanguageResult : EventArgsAsync -- 语言事件结果

用于 IEvent.OnLanguageEvent / OnLanguageEventAsync 语言变更事件不继承BaseModel

成员 类型 描述
Status bool 成功/失败指示
Message string? 可读描述信息
Language LanguageType? 目标语言(JSON 序列化为字符串)
Time DateTime 事件时间戳(默认为 DateTime.Now

工厂方法:

  • CreateSuccessResult(string msg) -- 成功
  • CreateSuccessResult(string msg, LanguageType? language) -- 成功并指定语言
  • CreateFailureResult(string msg) -- 失败
  • CreateFailureResult(string msg, LanguageType? language) -- 失败并指定语言

解构方法:

  • GetDetails(out string? message) -- 提取消息
  • GetDetails(out LanguageType? language) -- 提取语言类型
  • GetDetails(out string? message, out LanguageType? language) -- 提取消息和语言
  • GetDetails(out EventLanguageResult result) -- 提取完整结果对象

IEvent 接口

定义在 Snet.Model.@interface 中。包含 6 个事件,形成 3 对同步/异步组合:

public interface IEvent
{
    // 数据事件 -- 新数据采集完成时触发
    event EventHandler<EventDataResult>           OnDataEvent;
    event EventHandlerAsync<EventDataResult>      OnDataEventAsync;

    // 信息事件 -- 状态变更、错误、连接状态时触发
    event EventHandler<EventInfoResult>           OnInfoEvent;
    event EventHandlerAsync<EventInfoResult>      OnInfoEventAsync;

    // 语言事件 -- UI 语言切换时触发
    event EventHandler<EventLanguageResult>       OnLanguageEvent;
    event EventHandlerAsync<EventLanguageResult>  OnLanguageEventAsync;
}

设计原则: 每种事件类型都提供了同步和异步两个版本。订阅者根据自身处理模型选择合适的版本。


内部事件触发

在内部,CoreUnify 和驱动实现通过 protected 方法 触发事件:

方法 用途
OnDataEventHandler(EventDataResult) 触发同步数据事件
OnDataEventHandlerAsync(EventDataResult, CancellationToken) 触发异步数据事件
OnInfoEventHandler(EventInfoResult) 触发同步信息事件
OnInfoEventHandlerAsync(EventInfoResult, CancellationToken) 触发异步信息事件
OnLanguageEventHandler(EventLanguageResult) 触发同步语言事件
OnLanguageEventHandlerAsync(EventLanguageResult, CancellationToken) 触发异步语言事件

外部消费者不应直接调用这些方法,而应通过 IEvent 接口订阅。


使用示例

订阅数据事件

using Snet.Model.data;
using Snet.Model.@event;

// 获取实现 IEvent 的驱动实例
var daq = await SomeDaqDriver.InstanceAsync(config);

// 同步订阅(在调用线程上执行)
daq.OnDataEvent += (sender, e) =>
{
    if (e.Status)
    {
        Console.WriteLine($"[数据接收] {e.Message}");
        Console.WriteLine($"载荷: {e.ResultData}");
    }
    else
    {
        Console.WriteLine($"[错误] {e.Message}");
    }
};

// 异步订阅(返回 Task,适合 I/O 密集型操作)
daq.OnDataEventAsync += async (sender, e) =>
{
    if (e.Status)
    {
        await ProcessDataAsync(e.ResultData, e.CancellationToken);
    }
};

订阅信息事件

// 同步:记录连接状态
daq.OnInfoEvent += (sender, e) =>
{
    Console.WriteLine($"[{e.Time:HH:mm:ss}] 状态={e.Status}, {e.Message}");
};

// 异步:写入数据库
daq.OnInfoEventAsync += async (sender, e) =>
{
    await LogToDatabaseAsync(e.Status, e.Message, e.Time, e.CancellationToken);
};

订阅语言变更事件

daq.OnLanguageEvent += (sender, e) =>
{
    if (e.GetDetails(out var lang))
    {
        Console.WriteLine($"语言已切换为: {lang}");
        // 重新加载 UI 字符串资源
    }
};

daq.OnLanguageEventAsync += async (sender, e) =>
{
    if (e.GetDetails(out var msg, out var lang))
    {
        await ApplyLanguageAsync(lang, e.CancellationToken);
    }
};

取消订阅(防止内存泄漏)

// 保存委托引用以便后续取消订阅
EventHandlerAsync<EventDataResult> handler = async (sender, e) =>
{
    await ProcessAsync(e);
};

daq.OnDataEventAsync += handler;

// 不再需要时取消订阅
daq.OnDataEventAsync -= handler;

创建并触发事件(驱动端)

// 创建携带类型化数据的成功结果
var result = EventDataResult.CreateSuccessResult(
    "温度读取完成",
    new { Value = 25.6, Unit = "°C" }
);

// 触发异步事件
await OnDataEventHandlerAsync(result, CancellationToken.None);

// 创建失败结果
var errorResult = EventDataResult.CreateFailureResult("传感器超时");

// 通过 InfoResult 传递错误信息
await OnInfoEventHandlerAsync(
    new EventInfoResult(false, "传感器超时"),
    CancellationToken.None
);

使用 EventLanguageResult

// 宣告语言变更
var langEvent = EventLanguageResult.CreateSuccessResult(
    "语言已切换为中文",
    LanguageType.zh
);

// 通过 GetDetails 检查语言
if (langEvent.GetDetails(out string? msg, out LanguageType? language))
{
    Console.WriteLine($"{msg} -> {language}");
}

线程安全

所有 EventingWrapperAsync<TEvent> 操作在设计上都是线程安全的:

  • AddHandler / RemoveHandler 使用 C# 的 += / -= 操作 event 字段(编译器生成的锁机制)。
  • InvokeAsync 将委托列表缓存到局部变量中,确保快照隔离。
  • 每个处理程序在 try/catch 块中依次调用,单个处理程序失败不会阻止其他处理程序的执行。

最佳实践

  1. I/O 密集型任务优先使用异步订阅(数据库写入、HTTP 调用)。
  2. 轻量级 UI 更新或同步日志使用同步订阅
  3. 处理载荷数据前始终检查 e.Status
  4. 在异步处理程序中尊重 CancellationToken 以支持协作取消。
  5. 不再需要时取消订阅,防止长生命周期订阅者造成的内存泄漏。
  6. 优先使用工厂方法CreateSuccessResult / CreateFailureResult)而非直接使用构造函数以提高可读性。