
订阅、通知与中转谁通知谁谁又持有谁2026-10-13 · 概念 语法。今天两块① 讲两种「通知」型设计模式——观察者与中介者② 从另一条线里提炼的概念与语法判断依据别被自己擦掉、强引用 / 弱引用、订阅者到底是「对象」还是「方法」。全文脱敏真实类名 / 方法名 / 产品名 / 厂商名一律泛化内部流程的步骤名写成泛化词不含具体耗时 / 次数 / 比例。引子程序里天天在干一件事这边变了通知那边。今天从一个按一下按键就要触发某动作的例子出发把它一路拆开——它串起了两种「通知」的姿势也带出一批天天在用的语法。先把两个词立住后面反复用发布者有事发生时负责喊一声的那个对象。订阅者想被通知、于是提前挂个耳朵上去的那个对象。一、观察者模式Observer订阅者直接挂在发布者身上概念一对多的「订阅—通知」。发布者状态一变就挨个通知所有挂上来的订阅者。发布者 / | \ 通知 / | \ 通知 / | \ 订阅者A 订阅者B 订阅者C关键在方向不对称订阅者认识发布者——是它主动去挂的发布者.某事件 我的处理函数发布者不认识订阅者——它只管喊一声不知道、也不在乎此刻谁在听类比订报纸。你订阅者知道报馆在哪、去订了相当于报馆发布者根本不需要认识你——它每天照印照送谁订谁收没订的一个字收不到。语法C# 里观察者模式的语言级实现就是事件event底层是委托delegate// 发布者这侧声明一个事件——一颗别人可以挂上去的钩子eventAction数据,宽,高,帧号收到一帧;// ↑关键字 ↑ Action...内置委托类型表示一段没有返回值、收这几个参数的函数// 订阅者这侧把我的一个方法挂上去发布者.收到一帧我的处理方法;// ↑ 订阅对应的 - 退订判据看到挂在一个具体对象上某个设备.某事件 …就是观察者模式。二、中介者模式Mediator大家都挂到一个中枢概念一群对象如果彼此都要互相通知逐个会织成一张你扯我、我扯你的网。中介者模式换个思路——谁都不认识谁全都只认识中枢有事发给中枢由中枢转发。对象A ──┐ 对象B ──┼──► 中 枢 ──► 转发给所有关心这类事的对象 对象C ──┘ 对象之间一条线都没有类比机场塔台。每架飞机只跟塔台通话不直接和别的飞机通话谁让谁、谁等谁塔台统一调度。要是几十架飞机两两直接喊话就吵成一锅粥了。语法中介者最漂亮的一点——用「消息的类型」当路由键。发的时候不用告诉中枢发给谁中枢按消息是哪一个类去派发// 收注册时写清楚我只关心『删除消息』这一类事中枢.注册删除消息(this,收到删除时就干这个);// 发丢一个消息进去类型本身就是路由键中枢.发送(new删除消息());// ↑ 谁会收到——所有注册过「删除消息」的人发的人根本不知道都有谁消息往往是个空类——它不装逻辑只是一个信封全部意义就是这是哪一类事class删除消息{}// 空空如也它的类型才是关键类比群公告。你在群里发一句今天放假不用 每个人关心的人自己会看。你发出去的是内容 它属于哪一类通知群中枢负责让对的人看到。判据看到/注册挂在一个中枢对象上而不是具体设备就是中介者。三、两者对照差在「谁持有谁」观察者 Observer中介者 Mediator订阅挂在具体对象上中枢上订阅者与发布者订阅者认识发布者双方互不认识只认识中枢引用的松紧是强引用订阅者中枢对订阅者持弱引用适合场景一对一 / 一对多关系清楚多对多容易乱成一团时一张纸的判据看或注册挂在谁身上——挂着的是具体对象→ 观察者挂着的是中枢→ 中介者。四、概念澄清三连今天被反复追问出来的点这三条是今天真正卡住的地方单独拎出来弱引用弱的是接收方不是消息。订阅时被轻轻拉住的是接收方那个对象而消息是一次性的——发完就丢没人留着它。类比你订阅的是报纸这个长期服务接收方是长期的而每一期报纸本身是消耗品看完就扔消息是一次性的。订阅者是对象不是方法、也不是类。发布者.某事件 处理方法其实等价于 this.处理方法——真正订阅的是拥有这个方法的那个对象。同一个类 new 出两个对象各挂各的会各自收到通知。类比同一款手机类中是你这一台实例下载了某个 App、订阅了推送——不是这款手机订阅的也不是下载这个动作订阅的。强引用 vs 弱引用正是 Observer 与 Mediator 的分水岭。事件强引用——只要你挂在事件上订阅者就不会被回收挂了不退订就是内存泄漏对象本该没人用了却因为还挂在事件上一直活着。中枢注册弱引用——订阅者若没别处引用会被回收中枢自动清掉那条订阅不会留下指向死对象的野回调。一句提醒弱引用是兜底不是许可。它保证对象没了、订阅自清但不保证订阅时机对。若某个对象被复用一直有强引用那它这条订阅就始终有效——此时只注册、不退订反而是刻意的选择退了订复用后再进来就没人接了。五、通用教训别让达成目标的动作擦掉判断目标是否达成的依据概念你想判断某件事到底成没成总要有个依据一个标志、一条记录。坑在于——达成这件事的那个动作往往顺手把这个依据清掉了。于是你的判断永远说不成或永远说成依据形同虚设。实例判断设备是否已进入工作态原先靠一条记账上次设的参数 一个版本号。可让设备进入工作态这个动作自己就会作废这条记账清空它、版本号 1。等下次来查记录早被擦掉 → 判定永远为假→ 别重复劳动的那道优化从未生效。记账 : 上次设的值 版本号 # 想拿它判断是否已就绪 ... 进入工作态() # ◄ 这个动作会顺手 作废 上面的记账 ... 判断是否就绪() ──► 永远读不到记账 ──► 永远判没就绪 ──► 每次都白干一遍正解判断就绪没就绪别问记账直接问事实来源本身——回读设备当前的真实状态状态 : 回读设备() # 直接问设备你现在是什么态 就绪 : (状态 工作态)类比你贴了张便利贴已开过会用来判断这还会用不用再开。可开会这个动作散会时顺手把便利贴撕了。下次一看——没便利贴 → 以为没开过 → 又开一次无限循环。要判断开没开过别信便利贴去查会议记录事实来源。判据写任何要不要补做某事的判断前先问一句——这个判断依赖的状态由谁清除、什么时候清如果清除它的正是你想避免重复做的那件事本身那这个判据一定废。附几个实用语法点详讲1. 委托与事件delegate/event/Action.../EventHandlerT/委托delegate “一种函数的类型”。把一个方法当成值来传、来存靠的就是它。Action.../Func... 框架内置的两个委托省得你每次自己定义。Action收参数、无返回值Func收参数、有返回值。Actioninta;// 收一个 int返回 voidFuncint,stringf;// 收一个 int返回一个 stringevent 在委托外面加个安全壳外面的人只能/-订/退不能直接触发或覆盖它。只有类自己内部能触发。// 类内部触发喊一声if(收到一帧!null)收到一帧(数据,宽,高,帧号);// 现代写法null 条件调用一句顶上面两行收到一帧?.Invoke(数据,宽,高,帧号);常见坑① 触发前要防没人订阅它是 null用上面两种写法②是强引用只订阅不退订会泄漏见 §四。2. 弱引用WeakReference普通引用是强的——“我拿着你你就不能被回收”。弱引用正好相反我指着你但拦不住你被回收你被回收后我这个弱引用就变成空。var强某个对象;// 强引用对象活着var弱newWeakReference(某个对象);// 弱引用不阻止它被回收...if(弱.IsAlive){var真弱.Target;}// 回收了 → IsAlive 变 false为什么要它让长命的注册表 / 缓存 / 中枢能间接指向短命的对象又不把短命对象拖成活不了也死不掉的僵尸。常见坑别把它当自动清理用——你该退订还是要退订弱引用只是最后一道兜底。3. 泛型方法把类型当键注册T(...)void注册T(接受者 r,处理函数 h);// T 是占位类型调用时才定调用注册删除消息(…)时T就被定成删除消息中枢内部拿typeof(T)当Dictionary 的键把订阅者按消息类型分桶存。这就是消息类型当路由键的实现发new 删除消息()→ 按删除消息这个键取桶 → 派给桶里的人。好处编译期就检查类型比用字符串当键安全得多。4. 异步async/await/Task/TaskT/CancellationTokenTask/TaskT “一个将来会完成的操作”T是它完成后的结果类型。async/await 把异步写得像同步await一个Task就在这里等它完成但不占用线程。CancellationToken 一个取消信号一路传给底层操作长任务在该停时检查它、主动退出。asyncTaskbool查状态Async(int设定,CancellationToken 取消default){var状态await读设备Async(取消);// 可被取消return状态.值设定;}常见坑① 一个async Task方法里一个await都没有——合法但多半是疏忽要么忘了await要么这方法根本不该是异步的干脆去掉async②catch (Exception)会吞掉取消异常想区分真出错和被取消要先单独 catch 取消异常。5. 表达式体成员方法/属性只看一眼算出什么的写法把{ return …; }压成一行bool就绪状态工作态;// 只读属性一行int加一(intx)x1;// 方法一行常见坑是映射到别和赋值、相等看混——一个是赋值两个才是比较是把左边的名字定义成右边的表达式。6. 空值处理?./??/ 可空?收到一帧?.Invoke(...);// 左边是 null → 整个表达式短路成 null不报错var名输入的名字??匿名;// 左边 null → 用右边的默认值int?可能没有null;// 值类型也能可空常见坑?.只保护一步a?.B.C里若B也可能为 null链子会断在.C。多级就得多写?.。7. 模式匹配is/is not 变量绑定if(数据来源isnot某类型 vm)return;// 不是某类型 → 直接返回守卫if(数据来源is某类型 vm){vm.干点什么();}// 是 → 顺手把 vm 绑好好处判断 取用一步到位省掉先as再判 null。常见坑绑定出来的vm只在if的为真分支里有效别拿到外面用。8. 特性与源生成[Attribute]与帮你写代码特性[Attribute] 贴在类/方法上的标记给工具/框架看[某标记]privatevoid做点事(){...}// 框架看到这个标记就替它生成配套代码有些标记源生成器会在编译期根据你写的方法自动补出一段代码——比如把一个普通方法自动包成一个符合某接口的对象还是接口的方法。好处少写样板。代价那段代码是生成的你在源码里搜不到——调试时容易明明调了它怎么找不到定义。遇到这种’凭空冒出来’的成员先想到’是不是被什么标记生成出来的’。小结今天的账观察者订阅者直接挂在发布者上订阅者认识发布者发布者不认识订阅者挂在具体对象上。中介者大家都挂到中枢上互不认识用消息类型当路由键/注册挂在中枢上。分水岭 引用松紧事件是强引用不退订泄漏中枢注册是弱引用自清。三连澄清弱引用弱的是接收方不是消息订阅者是对象不是方法/类弱引用是兜底不是许可。一条通用教训判断某状态成没成别依赖会被’达成它的动作’自己擦掉的依据——去问事实来源本身。语法委托/事件Action/Func/?.Invoke、WeakReference、泛型当键、async/await/CancellationToken、、?./??、is/is not、特性与源生成。