事件系统 - Snet Docs

事件系统

命名空间: 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(object? sender, EventDataResult e) 触发同步数据事件
OnDataEventHandlerAsync(object? sender, EventDataResult e) 触发异步数据事件
OnInfoEventHandler(object? sender, EventInfoResult e) 触发同步信息事件
OnInfoEventHandlerAsync(object? sender, EventInfoResult e) 触发异步信息事件
OnLanguageEventHandler(object? sender, EventLanguageResult e) 触发同步语言事件
OnLanguageEventHandlerAsync(object? sender, EventLanguageResult e) 触发异步语言事件

外部消费者不应直接调用这些方法,而应通过 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(this, result);

// 创建失败结果
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)而非直接使用构造函数以提高可读性。