概念速览:同一件事,两种表达
在大型测控软件中,模块之间的通信是永恒的话题。一个模块采集到了数据,需要通知另一个模块去处理;用户点击了界面上的按钮,背后的业务逻辑需要响应。这种"发生了一件事,通知感兴趣的人"的模式,LabVIEW 和 C# 各自提供了一套完整的机制。
“一句话概括:LabVIEW 用 User Event(用户事件)实现发布-订阅模式,C# 用 Event(事件)实现同样的模式。两者的核心目标一致——解耦事件的发送者和接收者——但语法和运行时模型截然不同。”
核心概念映射
| 概念 | LabVIEW User Event | C# Event |
|---|---|---|
| 事件的定义 | Create User Event VI(创建事件引用) | event EventHandler<T> 关键字声明 |
| 事件引用 / 类型 | Event Refnum | event 字段本身(强类型) |
| 订阅者注册 | Register For Events 节点 | += 运算符 |
| 订阅者注销 | Unregister For Events 节点 | -= 运算符 |
| 事件的触发 | Generate User Event VI | ?.Invoke(this, args) |
| 事件数据的载体 | Event Data 端子(可以是任意类型组成的 Cluster) | EventArgs 子类或泛型参数 |
| 处理事件的代码 | Event Structure 中的事件分支 | Lambda 表达式 / 命名方法 |
| 多播能力 | 多个 Event Structure 可注册同一事件 | 多个方法可 += 到同一事件 |
这张表清晰表达了同一个抽象的不同语法实现。掌握了这张映射表,就能在两种语言之间自由切换思维。
完整生命周期:五个阶段的逐一对比
让我们沿着一个事件从诞生到消亡的完整时间线,逐一对比两边的实现方式。假设场景:一个按钮被按下时,通知两个不同的模块。
阶段 1:声明事件
LabVIEW — Create User Event
在程序启动时,调用 Create User Event VI,指定事件携带的数据类型(可以是一个 Cluster,内含温度值、时间戳等)。输出一个 Event Refnum——这是整个事件系统的"身份证"。
类比理解:就像你办了一张"事件广播许可证",许可证上写明了广播内容是什么类型的数据。
类比理解:就像你办了一张"事件广播许可证",许可证上写明了广播内容是什么类型的数据。
C# — event 关键字
在类中声明一个 event 字段:
public event EventHandler<string>? Clicked;EventHandler<string> 是 .NET 内置的泛型委托,string 是事件携带的数据类型。这行代码就相当于 LabVIEW 中创建了一个可以携带 String 数据的 User Event。
阶段 2:注册订阅者
LabVIEW — Register For Events
在需要接收事件的 VI 中,使用 Register For Events 节点,将 Event Refnum 和 Event Structure 连接起来。Event Structure 内部创建对应的事件分支,里面放置处理逻辑。
每个需要监听事件的模块都要独立注册。多个模块注册同一个 Event Refnum 是合法的
每个需要监听事件的模块都要独立注册。多个模块注册同一个 Event Refnum 是合法的
C# — += 订阅
用 += 运算符把处理方法绑定到事件上:
btn.Clicked += (sender, name) =>
{
Console.WriteLine($"收到: {name}");
};+= 一次就是增加一个订阅者。可以多次 += 绑定多个处理方法,这就是委托变量的多播委托,也可以把同一个处理方法从不同地方注册上去。
阶段 3:触发事件
LabVIEW — Generate User Event
在事件发生的时机(比如按钮按下 Value Change),调用 Generate User Event VI,传入 Event Refnum 和事件数据。该 VI 会将数据广播给所有注册了该事件的 Event Structure。
注意:Generate User Event 是同步的——它会依次通知所有订阅者,等所有订阅者处理完毕后才返回。
注意:Generate User Event 是同步的——它会依次通知所有订阅者,等所有订阅者处理完毕后才返回。
C# — ?.Invoke()
在类内部调用:
Clicked?.Invoke(this, buttonName);?.Invoke 是空条件运算符——如果没有任何订阅者(事件为 null),就静默跳过,不抛异常。触发也是同步的,所有订阅者依次执行。
阶段 4:处理事件
LabVIEW — Event Structure
在 Event Structure 的事件分支中,通过事件数据端子获取传递过来的数据(Cluster),然后在图形化代码中处理。
Event Structure 可以同时监听多种事件(控件事件、User Event、动态事件),在一个循环中统一处理。
Event Structure 可以同时监听多种事件(控件事件、User Event、动态事件),在一个循环中统一处理。
C# — Lambda / 方法
Lambda 表达式中的代码就是事件处理器:
// sender = 触发事件的对象
// args = 事件携带的数据
(sender, args) =>
{
string name = args; // 获取数据
// ... 处理逻辑
};也可以把处理逻辑写成一个独立方法,用 btn.Clicked += MyHandler; 注册。两种方式功能完全等价。
阶段 5:注销订阅者
LabVIEW — Unregister For Events
当订阅者不再需要接收事件时(比如 VI 即将关闭),调用 Unregister For Events 节点取消注册。
如果不注销,事件引用会一直持有订阅者 VI 的引用,可能导致 VI 无法被正常关闭或内存泄漏。
如果不注销,事件引用会一直持有订阅者 VI 的引用,可能导致 VI 无法被正常关闭或内存泄漏。
C# — -= 取消订阅
用 -= 运算符取消注册:v
btn.Clicked -= handler;同 LabVIEW 一样,不取消订阅会导致事件发布者持有订阅者引用,阻止垃圾回收。对于长生命周期的对象尤其要注意。
完整流程图对比如下:

示例代码片段
C#代码
/* ═══════════════════════════════════════════════
* 事件(Event)最简演示
* 一句话理解:事件 = 发生某件事时,自动通知所有订阅者
*
* LabVIEW 对照:User Event
* Create User Event → event 关键字声明
* Register For Events → += 订阅
* Generate User Event → ?.Invoke() 触发
* Event Structure → lambda 或命名方法
* ═══════════════════════════════════════════════ */
using System;
class Button
{
// 步骤1:声明事件
// event 关键字 + EventHandler 是 .NET 标准写法,内置委托
public event EventHandler<string>? Clicked;
// 步骤2:触发事件的方法
// ?.Invoke 的作用:如果没人订阅(null),就跳过,不报错
public void Click(string buttonName)
{
Console.WriteLine($"[按钮] \"{buttonName}\" 被按下");
Clicked?.Invoke(this, buttonName); // 通知所有订阅者,调用委托
}
}
class Program
{
static void Main()
{
var btn = new Button();
// 步骤3:订阅事件(+=),委托的多播订阅
btn.Clicked += (sender, name) =>
{
Console.WriteLine($" → 订阅者1: 收到! 按钮是 \"{name}\"");
};
//Lambda 表达式(被编译器转为委托的实例)
// 把 lambda 存成变量,后面才能取消订阅,指向一个委托实例的变量
EventHandler<string> sub2 = (sender, name) =>
{
Console.WriteLine($" → 订阅者2: 我也收到了 \"{name}\"");
};
btn.Clicked += sub2; //委托的多播订阅
Console.WriteLine("(当前有 2 个订阅者)");
Console.WriteLine();
// 步骤4:触发(两个订阅者都会收到)
btn.Click("开始采集");
// 步骤5:取消订阅(-=),然后再次触发
btn.Clicked -= sub2; // 订阅者2 不再接收通知
Console.WriteLine();
Console.WriteLine("(订阅者2 已取消,只剩 1 个订阅者)");
Console.WriteLine();
btn.Click("停止采集"); // 只有订阅者1 会收到
}
}
/* ═══════════════════════════════════════════════
* 运行输出:
*
* (当前有 2 个订阅者)
*
* [按钮] "开始采集" 被按下
* → 订阅者1: 收到! 按钮是 "开始采集"
* → 订阅者2: 我也收到了 "开始采集"
*
* (订阅者2 已取消,只剩 1 个订阅者)
*
* [按钮] "停止采集" 被按下
* → 订阅者1: 收到! 按钮是 "停止采集"
* ↑ 订阅者2 不再出现
*
* ──────────── 三步速记 ────────────
* 1. 声明:public event EventHandler<T> xxx;
* 2. 订阅:xxx += (sender, args) => { ... };
* 3. 触发:xxx?.Invoke(this, data);
* 4. 取消:xxx -= handler; // ← 别忘了!
* ═══════════════════════════════════════════════ */