观察者模式 + UI 线程调度”的典型应用


A. 涉及的知识点(抽象)

  1. 观察者模式(Observer Pattern)

    • 发布者DemoDeviceService.cs 内部生成一帧数据 ScopeFrame,通过 OnScopeFrame?.Invoke(frame) 发布事件。
    • 订阅者:UI 在 Form1 里订阅 OnScopeFrame,收到后处理。
    • 作用:发布者和订阅者解耦,谁关心就订阅,不关心就不订阅。
  2. .NET 事件机制

    • event Action<ScopeFrame> OnScopeFrame; 是强类型事件。
    • 发布者只需要 Invoke(frame),订阅方自动收到。
    • 支持多订阅者(UI 可以同时有多个模块监听)。
  3. 线程调度与 UI 安全

    • 事件触发在后台任务线程里,但 WinForms 控件只能在 UI 线程更新。
    • 订阅处理时用 BeginInvoke 切回 UI 线程,保证线程安全。
  4. 数据帧抽象

    • 推送的数据被打包成 ScopeFrame,而不是裸数组,这样一帧数据的元信息(通道数、采样率、时间戳)都随事件传递,方便上层统一处理。
    • 这是“数据契约”的思想:上下游通过一个约定好的对象交互。

B. 可复用的“套路总结”

** 模板**:
👉 “后台产生数据 → 打包成帧/消息对象 → 事件广播 → UI 或其它订阅者收到 → 用 BeginInvoke/Dispatcher.Invoke 切回安全线程更新界面。”

步骤:

  1. 定义一个 数据承载类(类似 ScopeFrame)。
  2. 在后台服务类里定义 public event Action<T>
  3. 产生数据时调用 OnEvent?.Invoke(new T(...))
  4. UI 层 service.OnEvent += e => BeginInvoke(() => Handle(e));

C. 举个复用的例子:心率监控仪 UI

假设我们采集心电信号,每秒推送一次平均心率。

  1. 数据帧定义

    public class HeartRateFrame {public int BPM { get; }public DateTime Timestamp { get; }public HeartRateFrame(int bpm, DateTime ts) { BPM = bpm; Timestamp = ts; }
    }
    
  2. 后台服务

    public class HeartRateService {public event Action<HeartRateFrame> OnHeartRate;public async Task StartAsync(CancellationToken ct) {var rand = new Random();while (!ct.IsCancellationRequested) {int bpm = rand.Next(60, 100);OnHeartRate?.Invoke(new HeartRateFrame(bpm, DateTime.Now));await Task.Delay(1000, ct);}}
    }
    
  3. 前台订阅

    _hrService.OnHeartRate += f => BeginInvoke(new Action(() => lblHeartRate.Text = $"{f.BPM} BPM"));
    

D. 为什么要这样设计?

  • 松耦合:后台服务不依赖 UI,UI 也不用知道后台如何采集。
  • 可扩展:多个 UI 组件(实时曲线、报警灯、日志面板)都可以同时订阅 OnScopeFrameOnHeartRate
  • 安全:通过 BeginInvoke 保证 UI 线程安全。
  • 一致性ScopeFrame/HeartRateFrame 封装上下文信息,接口清晰。

总结
这个“ScopeFrame 订阅机制”本质是 观察者模式 + UI 线程切换 + 数据帧契约
复用时,你只要换掉帧里承载的数据类型(心率帧、温度帧、网络包帧……),其它套路一模一样。


补充

public event Action<T> SomethingHappened;

背后串起了 3 个核心知识点:委托(delegate)事件(event)观察者模式(发布/订阅)。下面用“教学版”把它讲透,并结合你现在的 OnScopeFrame 用法。


1) Action<T> 是什么?—— 委托(delegate)

  • 委托是“方法的类型”。它描述“能被调用的方法长什么样”。
  • Action<T> 是 .NET 内置的泛型委托:表示接受一个 T 参数、没有返回值的方法签名。
    常见的还有 Action(无参)、Action<T1,T2>Func<TIn,TOut>(有返回值)等。
  • 你项目里:Action<ScopeFrame> 就代表“入参是 ScopeFrame、无返回值”的回调。

小练习:任何能写成 void Handle(T x) 的方法/lambda,都能赋给 Action<T>

void HandleFrame(ScopeFrame f) { /* ... */ }
Action<ScopeFrame> handler = HandleFrame;
// 或者
Action<ScopeFrame> handler2 = f => Console.WriteLine(f.Length);

2) event 是什么?—— 事件(event)

  • event 基于委托,加了一层封装/访问控制

    • 类外部只能 += 订阅、-= 退订;不能直接调用、不能覆盖整个委托字段。
    • 类内部(发布者)才可以触发(Invoke)。
  • 目的:防止外部把你的回调列表清空/替换,或私自触发事件,保证类的不变式与封装性。

对比:

public Action<ScopeFrame> OnScopeFrame;      // 裸委托(不安全)
public event Action<ScopeFrame> OnScopeFrame; // 事件(外部只能 += / -=)

3) 它实现了什么模式?—— 观察者(发布/订阅)

  • 发布者(Subject):在合适时机 OnScopeFrame?.Invoke(frame)
  • 订阅者(Observer)_device.OnScopeFrame += f => BeginInvoke(() => PushFrameToDyn(f));
  • 多播委托:一个事件可以挂多个处理器,触发时会逐个调用(调用链)。

这正是你现在用的套路:设备服务“发布帧”,前台多个模块(示波、记录、告警…)都可以各自订阅


4) 触发与订阅:正确姿势

触发(发布者内部)

// C# 6+ 推荐写法:空条件调用,避免空引用
OnScopeFrame?.Invoke(frame);

订阅(UI/消费者)

_device.OnScopeFrame += f =>BeginInvoke(new Action(() => PushFrameToDyn(f))); // 切回 UI 线程

线程要点:事件经常在后台线程触发;WinForms/WPF 控件只能在UI 线程操作 → 用 BeginInvoke/Dispatcher.Invoke 切回。


5) 进阶要点(面试/实战都常用)

  • 异常隔离:某个订阅者抛异常会中断后续订阅者的调用。稳妥做法:发布者遍历调用列表,逐个 try/catch

    var handlers = OnScopeFrame; // 拷贝引用
    if (handlers != null)foreach (Action<ScopeFrame> h in handlers.GetInvocationList())try { h(frame); } catch (Exception ex) { Log(ex); }
    
  • 内存泄漏:长生命周期发布者(单例/后台服务)↔ 短生命周期订阅者(窗体/控件)——要记得 -= 退订;或采用“弱事件”方案。

  • 命名与约定:.NET 传统是 EventHandler<TEventArgs> 模式:
    event EventHandler<MyEventArgs> Something;,签名固定 (object sender, TEventArgs e)
    你现在用 Action<T> 更简洁,团队内部统一即可。

  • 泛型协变/逆变Action<in T>T逆变的(了解即可)。


6) “对照理解”

  • 现在的定义:

    public event Action<ScopeFrame> OnScopeFrame;
    

    表示“有一件事发生了(新的一帧数据)”,订阅者拿到 ScopeFrame 自己处理。

  • 触发点(设备服务里):

    var frame = new ScopeFrame(ch1, ch2, fs, sw.ElapsedTicks);
    OnScopeFrame?.Invoke(frame);
    

    ——把“一帧数据的契约”一起广播出去(通道数组、采样率、时间戳),订阅方解耦。

  • 订阅点(UI):

    _device.OnScopeFrame += f => BeginInvoke(() => PushFrameToDyn(f));
    

    ——收到就切回 UI 线程,批量刷新 ScottPlot。


7) 可复用的模板

// ① 定义事件(发布者)
public class DataSource<T>
{public event Action<T> OnData;public void Produce(T value) => OnData?.Invoke(value);
}// ② 订阅(消费者,WinForms 中)
_dataSource.OnData += v => BeginInvoke(new Action(() => Handle(v)));// ③ 退订(释放时)
_dataSource.OnData -= Handle; // 如果是具名方法

替换 T 就能复用到:日志行、温度帧、相机图像帧、网络包、心率、GPS……任何“源源不断的数据”。


8) 一个复用示例:温度监控

public sealed class TemperatureFrame
{public double Value { get; }public DateTime Time { get; }public TemperatureFrame(double v, DateTime t) { Value = v; Time = t; }
}public class TempService
{public event Action<TemperatureFrame> OnTemperature;public async Task StartAsync(CancellationToken ct){var rand = new Random();while (!ct.IsCancellationRequested){var f = new TemperatureFrame(20 + rand.NextDouble()*5, DateTime.Now);OnTemperature?.Invoke(f);await Task.Delay(1000, ct);}}
}// UI
_tempService.OnTemperature += f =>BeginInvoke(new Action(() => lblTemp.Text = $"{f.Value:F1} ℃"));

小结

public event Action<T> = 用强类型事件把“某类消息 T”广播出去;外部只能订阅/退订,不能乱触发;配合 UI 线程切换,天然实现“发布/订阅 + 解耦 + 线程安全”。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/96957.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/96957.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/96957.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux应用软件编程---网络编程(TCP:[ 其他机制、头部标志位、应用示例 ]、 HTTP:[ 万维网、概念、格式、报文、应用示例 ]

一、TCP 网络协议补充内容1、TCP 的其他机制1&#xff09;TCP 头部的标志位TCP 头部可用抓包工具 (wireshark) 来查看。头部标志位用途SYN请求建立连接标志位ACK响应报文标志位PSH携带数据标志位&#xff0c;通知接收方该从缓冲区读数据FIN请求断开连接标志位RST复位标志位URG紧…

基于开源飞控pix的无人机装调与测试

文章目录 前言资源下载1、地面站软件独家汉化版QGC地面站&#xff08;推荐&#xff09;原版QGC地面站Mission Planner地面站 2、安装好环境的虚拟机安装虚拟机打开虚拟机文件 3、完整的各版本PX4、QGC源码PX4QGC 一、无人机基本常识/预备知识&#xff08;1&#xff09;无人机飞…

Ubuntu解决makefile交叉编译的问题

问题1&#xff1a;/usr/lib/gcc-cross/aarch64-linux-gnu/11/../../../../aarch64-linux-gnu/bin/ld: cannot find -lwiringpi: No such file or directory 找不到-lwiringpi库路径&#xff0c;其实在3rd/usr/lib/aarch64-linux-gnu下没有libwiringPi.so.2 …

ExcelUtils实现 设置内容 插入行 复制行列格式

ExcelUtils实现&#xff1a;1.实现输入 例如 2 A 的excel格式&#xff0c;自动填充对应excel单元格&#xff1b;2.实现复制并新增下一行&#xff1b;3.实现控制复制上一行相同列的格式&#xff1b;4.实现控制复制同一行上一列的格式&#xff1b;/*** 在指定行下方插入新行并复…

SQLBot 智能问数、数据洞察逻辑拆解

* 基于 SQLBot v1.0.2* 使用 AI Gateway 抓取模型调用记录SQLBot 通过融入 LLM 能力实现了非常优秀的问数体验&#xff0c;这里记录一下产品中如何引入 AI 能力&#xff0c;顺便探究一下调用大模型的数据安全的问题&#xff08;是否会向模型提供真实数据&#xff09;。结论&…

实现统一门户登录跳转免登录

统一门户所有应用页面&#xff0c;点击跳转对应业务系统&#xff0c;实现业务系统免登录//获取所有业务系统项&#xff08;获取并存储到仓库) //用于页面展示 let appSubjectVoList ref<any>([]) appSubjectVoList.value userStore.getAppSubjectVoList || [] //登陆后…

卓伊凡的开源战略与PHP-SG16加密技术深度解析-sg加密技术详解-卓伊凡

卓伊凡的开源战略与PHP-SG16加密技术深度解析-sg加密技术详解-卓伊凡引言&#xff1a;在理想与现实间寻求平衡的开源之路近日&#xff0c;技术创业者卓伊凡先生宣布了一项重大决策&#xff1a;将于明日将其公司旗下的优雅草商城、项目管理系统等众多成熟商业产品正式开源。这一…

回溯 算法常见面试问题

1. 全排列(无重复元素) 核心思想:交换法避免额外空间 def permute(nums):def backtrack(first=0):if first == len(nums):res.append(nums.copy())returnfor i in range(first, len(nums)):nums[first], nums[i] = nums[i], nums[first]backtrack(first + 1)nums[first], …

营销专业人员核心能力构建与发展路径

CDA数据分析师证书含金量高&#xff0c;适应了未来数字化经济和AI发展趋势&#xff0c;难度不高&#xff0c;行业认可度高&#xff0c;对于找工作很有帮助。一、营销人员五维能力模型能力维度核心技能要素工具与方法论产出成果数据驱动决策指标监控、归因分析、效果优化Google …

Android系统学习2——Android.Utils.Log模块讨论

Android系统学习2——Android.Utils.Log模块讨论 ​ 打日志是一个很好的习惯&#xff0c;有的时候我们可以通过这里排查我们的程序的问题。在这里&#xff0c;我们可以从Android的日志机制入手讨论我们的Log模块。 android.util.Log 类的作用 Android 中最常用的日志工具是 and…

使用 YAML 文件,如何优雅地删除 k8s 资源?

在 Kubernetes 中&#xff0c;删除资源是日常运维中不可避免的操作。如果你习惯了使用 kubectl create 和 kubectl apply 来创建和更新资源&#xff0c;那么你可能也会想知道如何用同样基于文件的方式来删除它们。 虽然你总是可以用 kubectl delete deployment <name> 这…

如何将游戏和软件移动到另一个驱动器或外部磁盘中

您的C盘存储空间是否不足&#xff0c;或者您不小心在错误的驱动器中安装了游戏或应用程序。那么使用这个简单的技巧&#xff0c;您可以轻松的将游戏或应用程序移动到另一个分区或磁盘中。1、找到准备移动的软件&#xff0c;选择路径并复制&#xff1a;2、打开记事本&#xff0c…

赋能汽车电子智造:全星QMS打造品质检验、稽核与客诉管理闭环​——全星质量管理软件系统

全星QMS&#xff1a;驱动汽车电子质量卓越与商业成功的核心引擎 在智能汽车时代&#xff0c;汽车电子的质量已成为产品安全、性能与品牌信誉的核心。面对复杂的供应链、严苛的IATF 16949/ISO 26262标准及降本增效的压力&#xff0c;您的企业需要一位数字化战略伙伴。全星质量管…

【数据结构C语言】顺序表

1. 线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串...线性表在逻辑上是线性结构&#xff0c;也就说是连续的一条直线…

AI 学习路径-记录分享

目录推荐学习资源延申阅读推荐学习资源 3Blue1Brown的个人空间-3Blue1Brown个人主页-哔哩哔哩视频 这个简短的课程有助于了解AI的本质&#xff0c;迈入学习AI的第一步。 欢迎加入 &#x1f917; AI Agents 课程 - Hugging Face Agents Course AI Agent&#xff0c;当前火爆…

Windows Server 2019 上安装 Ubuntu 20.04 的几种方式

docker desktop不支持Windows server 2019&#xff0c;所以Windows Server 2019 上安装 Ubuntu 20.04 变成一种可行的途径。记录一下其中可用的几种方式&#xff1a;&#x1f5c2; 常见安装方式对比方式原理难度适用场景优点缺点Hyper‑V 虚拟机&#xff08;推荐&#xff09;利…

当Trae遇上高德MCP:一次国庆武汉之旅的AI技术实践

当Trae遇上高德MCP&#xff1a;一次国庆武汉之旅的AI技术实践 &#x1f31f; Hello&#xff0c;我是摘星&#xff01; &#x1f308; 在彩虹般绚烂的技术栈中&#xff0c;我是那个永不停歇的色彩收集者。 &#x1f98b; 每一个优化都是我培育的花朵&#xff0c;每一个特性都是我…

设计模式:抽象工厂模式

简介 抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,它提供了一种封装一组具有共同主题或相关依赖关系的独立工厂的方式,而无需指定它们的具体类。核心思想是创建一系列相关或相互依赖的对象家族(产品族),可以将客户端与具体产品的创建过程解耦,使得客…

知行——同为科技24周年庆典

在宜人的金秋时节&#xff0c;北京同为科技有限公司于2025年8月23日&#xff0c;天津基地与江西同时隆重举办了以“知行”为主题的周年庆祝活动&#xff0c;回顾企业24年来的奋斗历程&#xff0c;凝聚“同为人”力量&#xff0c;展望更加光明的未来。当天&#xff0c;创始人周慧…

RK android14 定制ES8388音频编解码器双MIC双OUT(1)

文章目录 前言 一、适配内容概述 二、适配步骤 1. HAL层配置修改 1.1 添加声卡名称识别 (`audio_hw.c`) 1.2 注册声卡路由配置 (`config_list.h`) 1.3 定义路由配置表 (`es8388_config.h`) 2. 内核设备树修改 2.1 禁用默认声卡 2.2 配置ES8388声卡节点 2.3 配置I2C和Codec节点 …