Kook.Net 中的事件
Kook.Net 中的事件采用与标准 .NET 事件模式类似的方式实现, 不同的是,Kook.Net 中的事件类型都为 Task, 事件参数传递不使用 EventArgs,而是直接传递到事件处理程序中。
这使得事件处理程序可以直接在异步上下文中执行,事件返回类型为
Task,而不是 async void。
用法
要从事件中接收数据,只需通过 C# 委托的事件模式进行订阅。
订阅事件支持命名函数,也支持匿名函数(Lambda 表达式)。
线程安全性
所有的事件都被设计为线程安全的,所有的事件都拥有与网关线程相同的上下文, 在网关线程之外的任务上同步运行,
但这样做也会存在副作用,这可能会导致网关现成死锁并终止连接。
经验之谈,任何耗时超过 3 秒的任务都不应该直接在事件上下文中等待,
而是应该包装在 Task.Run 中执行,或是卸载到另外一个任务中。
这意味着您不应该在与事件相同的上下文中通过 KOOK 网关请求数据, 由于网关线程将等待所有被调用的事件处理程序完成,然后才会处理所有来自网关的任何其它数据, 这将导致一个无法恢复的死锁。
Warning
如果您需要在网关线程之外的上下文中(下以事件处理线程指代)访问 Kook.Net 的缓存实体,可能会遇到线程安全性问题。
例如,当您在事件处理线程中访问缓存中的某频道实体时,与此同时,如果网关线程正在处理频道删除事件, 则可能会导致频道实体在缓存中被删除,从而导致事件处理线程中在访问频道实体时可能会抛出异常。
又如,当您在时间处理线程中访问频道消息时,与此同时,如果消息的作者编辑了该消息文本,网关线程可能会更新该消息实体, 从而导致事件处理线程中在访问消息实体时可能会取到不正确的消息文本。
因此建议您在开启不在网关线程中等待的、可能会访问缓存实体中的数据的事件处理线程前,先将必要数据取值为局部变量,再进行后续操作。
相同的问题在队列模式或发布订阅模式下也可能会出现,因此,这些可能会被网关线程更新的数据都应包装为队列或发布订阅事件的参数。
Note
等待任务完成:await Task.Run(() => { /* ... */ }); 或 await Task.Run(async () => { /* ... await ... */ });
此时,异常将会被正确地传播到事件上下文中,但这样做也会导致网关线程等待任务完成。
不等待任务完成:_ = Task.Run(() => { /* ... */ }); 或 _ = Task.Run(async () => { /* ... await ... */ });
此时,异常不会被传播到事件上下文中,您需要在事件处理线程正确地捕获处理异常。
额外要注意的是,如果异常被抛出到 async void 所标记的方法中,如果异常没有被正确地处理,将会导致程序退出。
常见模式
Kook.Net 中的事件签名都是形如 Func<T1, ..., Task> 的模式,没有额外定义名称,
因此,有关方法签名的详细信息,请参考 IntelliSense 智能提示,或直接浏览 API 文档。
不过,Kook.Net 中的事件签名大多遵循类似的模式,还是可以让您从中推断参数定义。
实体变更
具有 Func<Entity, Entity, Task> 签名的事件处理程序通常表示一个实体中的信息发生了变更,
两个实体中,前者为发生变更前实体的副本,后者为变更执行完成后的实体。
此模式通常仅在 EntityUpdated 事件中出现。
缓存实体
具有 Func<Cacheable, Entity, Task> 签名的事件处理程序则通常表示 API
或网关并未提供实体发生变更前的状态,因此它可以从客户端的缓存中提取或从 API 中下载。
有关此对象的更多信息,请参阅 Cacheable 文档。
Note
许多与消息相关的实体(例如:MessageUpdated 和 ReactionAdded)依赖于客户端的消息缓存,
该特性默认不启用,因此,如果您需要使用它,请在 KookSocketConfig
中通过设置 MessageCacheSize 的值来启用该消息缓存。
示例
using Kook;
using Kook.WebSocket;
public class Program
{
private KookSocketClient _client;
public static Task Main(string[] args) => new Program().MainAsync();
public async Task MainAsync()
{
// 如需使用事件中的 Cacheable<IMessage, Guid> 实体,
// 您可能需要在客户端配置中启用消息缓存。
var _config = new KookSocketConfig { MessageCacheSize = 100 };
_client = new KookSocketClient(_config);
await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("KookToken"));
await _client.StartAsync();
_client.MessageUpdated += MessageUpdated;
_client.Ready += () =>
{
Console.WriteLine("Bot is connected!");
return Task.CompletedTask;
}
await Task.Delay(Timeout.Infinite);
}
private async Task MessageUpdated(Cacheable<IMessage, Guid> before, SocketMessage after, ISocketMessageChannel channel)
{
// 如果没有启用消息缓存,消息下载方法可能会获得与 `after` 完全相同的实体
var message = await before.GetOrDownloadAsync();
Console.WriteLine($"{message} -> {after}");
}
}