一周概念复盘:预热、漂移与门禁

一周概念复盘:预热、漂移与门禁

2026-10-25 · 概念 + 工程方法 + 语法。

这一周碰到的词太杂:有的来自设备,有的来自成像,有的来自"怎么把代码写得不骗自己"。这篇按三层重排:

① 名词解释 (设备与成像的通用概念)② 工程方法 (本周真正学到的判断法)③ 语法 (C# 点逐个讲透)。

全文脱敏:真实类名 / 方法名 / 文件路径 / 产品名 / 厂商名一律泛化;业务数值写成"很长/很多/很大";示意代码用公开常识逻辑演示语法。


零、概念地图

复制代码
                      ┌──────────── 一、名词解释 ────────────┐
  设备侧               │ 射线源:预热 / 出束 / 状态字 / 焦斑     │
                      │ 探测器:开流 / 关流 / 零点漂移 / 位深    │
  成像侧               │ 质量:景深 / 显影 / 对比度 / 卷帘 / 通道序│
                      │ 运动:软限位 / 方向拟合 / 视场 / 路径规划 │
                      └──────────────────────────────────┘
                                    │
                                    ▼
                      ┌──────────── 二、工程方法 ────────────┐
                      │ 1 观察型 vs 操作型(超时怎么定)        │
                      │ 2 事件 + 快照(迟到的订阅者)           │
                      │ 3 刷新颗粒度(由下游代价倒推)          │
                      │ 4 判断子集(界面可用性 ⊆ 门禁)         │
                      │ 5 门禁五件套:fail-closed / 租约 /      │
                      │   命令顺序 / 残留值 / 资源所有权        │
                      └──────────────────────────────────┘
                                    │
                                    ▼
                      ┌──────────── 三、语法 ────────────────┐
                      │ 模式匹配 / record / 源生成器 / 异常块   │
                      │ 纯函数 / 早失败 / 异步 / 线程亲和 /     │
                      │ 事件生命周期 / 冻结对象 / 集合断言      │
                      └──────────────────────────────────┘

第一块 · 名词解释

1.1 射线源:预热、出束、状态字、焦斑

预热(warm-up)

是什么:设备在正式工作前,先低功率"养"一段时间,让关键部件(灯丝、阳极)进入稳定状态。

生活类比 :冬天开车要先热车,烤箱要先预热到温度再放面包。没预热就上,东西不是烤不熟就是烤糊。

工程含义:预热是一个"有代价、有耗时"的过程,中途被打断可能前功尽弃。

出束(emission)

是什么:设备真正开始往外输出能量(辐射/光/激光)。

生活类比:热车 ≠ 上路。热车是在原地烧油,上路才是真正产生位移。

最容易踩的坑 :把"预热"和"出束"当成互斥的两件事。实际上有些设备预热期间就已经在低强度出束了。如果代码里默认"我没开就是没出束",就可能在一个自以为安全的状态下去开防护门------这是本周最重要的一条安全认知。

复制代码
   以为的模型              实际的模型
   ┌──────┐  ┌──────┐     ┌──────────────────┐
   │ 预热 │→ │ 出束 │     │ 预热(内部已在出束)│
   └──────┘  └──────┘     └──────────────────┘
     "没开"                "没开" ≠ "没有能量"

状态字

是什么:设备用一个只读的数值汇报自己当前处于什么状态(等待、准备中、就绪、过载、故障......)。

生活类比 :体温计的读数。你可以一直看它,但它不会因为你看了就退烧------状态是读的,不是写的。

设计含义:状态字是"唯一真值源",界面上的颜色、按钮能不能点,都应该是它的下游推导,而不是各自维护一份。

具体有哪些取值、每个值对应什么,属于设备协议,本文不展开。

焦斑(focal spot)

是什么:能量源"发光"的那个点有多大。点越小,成象越锐利,但能承受的功率越低。

生活类比:手电筒------聚光时又亮又刺眼但只照一小块;散光时柔和但照不清细节。


1.2 平板探测器:开流、关流、零点漂移、位深

开流 / 关流

是什么:探测器平时是"待机"的,要采图得先把它切到"持续出图"的模式,这叫开流;采完切回去,叫关流。

生活类比 :水龙头。开流 = 拧开阀门让水持续流;关流 = 关上。开着的时候水一直在流,关了才不流。

坑:开流是一个"可能卡住"的动作(驱动、总线、设备握手都可能出问题)。如果界面线程傻等,整个程序就冻住了。这类动作必须有自己的兜底。

零点漂移(zero drift)

是什么:设备什么都不拍的时候,读数也不一定是零,而且这个"不为零"的基线会随时间慢慢变。

生活类比:家里的电子秤。放久了空秤可能显示 0.1kg;厨房温度高的夏天和冬天,偏的量还不一样。所以要么定期归零,要么每次测量前先"去皮"。

工程含义:

  • 漂移是趋势,靠单次读数看不出来;

  • 需要周期性的观测(比如在固定位置拍一组基准图)才有意义;

  • 观测必须记录位置------"我上次在某个位置拍过"如果没记下是哪个位置,这次就没法对比。

    复制代码
    基线值
      ↑        ╭──── 漂移前的基线
      │   ╭────╯
      │  ╱        ╭──── 一段时间后整体上移
      │ ╱    ╭───╯
      └────────────────→ 时间
          ↑    ↑    ↑
        观测  观测  观测   ← 两次观测之间发生的事,单次读数看不见

位深(bit depth)

是什么:一个像素用多少位来表示亮度。8 位 = 每个通道 256 级灰度;16 位 = 65536 级。

生活类比:尺子。只有 0/1 两格的尺子只能量"有/没有";有 256 格的尺子能量出层次;有 65536 格的尺子能看到几乎连续的过渡。

为什么重要 :中间处理丢掉的位深,后面再也补不回来。所以原始数据要按无损、高位深保存;有损压缩或降到 8 位,是"看起来差不多,实际把细节抹平了"。

成像质量五件套

概念 一句话 生活类比
景深 能同时清晰的范围(前后方向) 拍合影:光圈越小,前后排越都不糊
显影 目标在图像里"出不出现、清不清楚" 玻璃上的指纹------侧光才看得见
对比度 目标与背景的灰度差 白纸上写白字,再清楚也看不见
卷帘快门 逐行曝光,运动物体被"拉斜" 快速扫过一排栅栏,照片里栅栏是弯的
通道序 红绿蓝三通道的排列顺序 三个人排队报数,队形换了,名字就安错了人

通道序这条本周踩过 :同一个像素缓冲区,一边当作"红绿蓝"读,另一边当作"蓝绿红"读,结果就是红色和蓝色互换------图还在,颜色错了。

复制代码
   RGB 读法            BGR 读法
   (R,G,B) = (255,0,0)  →  读成 (B,G,R) = (0,0,255)
   你以为的"红"              实际显示的"蓝"

1.3 运动与几何:软限位、方向拟合、视场、路径规划、净空

软限位(soft limit)

是什么:在软件里先判断"再往前走就撞了",提前拒绝;与之相对的是硬限位(物理开关,撞到才停)。

生活类比 :楼梯最后一级前面的护栏。护栏是硬限位------撞上才知道;软限位是你走到倒数第二级就有人喊"别走了"。

工程含义:软限位要留余量。余量太小会误拦,余量太大等于没拦。

方向拟合(最小二乘)

是什么:用一堆带噪声的点,拟合出"这条线/这个面大概朝哪"。

生活类比:拿一把不太直的尺子量桌边。量一个点会偏,量十个点取"最贴近所有点的方向",偏差就被平均掉了。

为什么不用两个点:两个点天生长在一条直线上,噪声直接变成方向误差。

视场(FOV)与路径规划

是什么:视场 = "一张图能拍到的范围";路径规划 = "为了拍完整个大目标,先去哪、后去哪、每个点拍几张"。

生活类比 :用手机拍一面墙的壁画------一张拍不全,就要分块拍,还要保证相邻两块有重叠,最后拼起来。先拍哪块、重叠多少、按什么顺序走,就是路径规划。

注意:切分策略、每块怎么算、重叠留多少,属于具体业务实现,本文只说概念。

净空(clearance)

是什么:两个物体在运动过程中,彼此之间最小要留多少距离才不碰。

生活类比 :停车时车门能打开的距离------车停进去了不算完,旁边那辆车要是贴太近,门开不了。


第二块 · 工程方法

2.1 观察型 vs 操作型:超时到底该不该设?

概念

操作型 观察型
你是不是发起者 是:我下的命令 不是:别人/设备自己在做
超时了能不能干预 能:取消、重试、报错 不能:强行干预会破坏正在做的事
超时怎么处理 硬失败,明确报错 只提示,不打断
类比 微波炉定时:到点"叮"一声就停 看邻居烤蛋糕:只能提醒"好像有点久了",不能替人家关火

为什么这条重要

本周的真实教训:一个"长时间的过程"(长到不能傻等,也不该由旁人硬掐)被上层设了"等一会儿就放弃"的逻辑,放弃时顺手做了一次"关闭"------结果把别人正在做的事打断了,而且表面上看是"失败重试",实际是"破坏"。

复制代码
   错误模型:我以为我在操作             真实模型:我只是在观察
   ┌──────┐  发命令   ┌──────┐        ┌──────┐  看状态   ┌──────┐
   │ 上层 │ ───────→ │ 设备 │        │ 上层 │ ───────→ │ 设备 │
   └──────┘  超时关  └──────┘        └──────┘  只提示   └──────┘
              ↑  这一下把别人的活打断了        (不拥有过程,就别替它收尾)

判据

你是不是这个过程的拥有者?

  • 是 → 可以设硬超时、可以取消、可以重试;
  • 不是 → 只做观察和提示,绝不发"关闭/复位"这类命令。

2.2 事件 + 快照:迟到的订阅者听不到过去

场景

设备状态变化时通知界面更新。听起来天经地义,但有一个经典坑:

复制代码
   事件时间线
   ──┬─────────┬─────────┬──────────→
     │ 变化A    │ 变化B    │ 变化C
     │          │          │
     π          π          π   订阅者在这之后才进来
     │          │          │
   └── 订阅者全程听不到 ──┘   因为它只在"变化的那一刻"被通知

事件是"广播",不是"存档"。广播只在发生的那一刻存在,错过就永远错过。

解法:事件 + 当前值快照

csharp 复制代码
// 示意:事件负责"变化时叫醒你",属性负责"你现在看到什么"
public sealed class DeviceMonitor
{
    private int _status;

    public event EventHandler<int>? StatusChanged;   // 广播

    public int CurrentStatus => _status;             // 快照(随时可读)

    public void Update(int status)
    {
        if (status == _status) return;               // 没变化就不喊
        _status = status;
        StatusChanged?.Invoke(this, status);
    }
}

使用方 :订阅事件的同时,立刻读一次 CurrentStatus。

csharp 复制代码
monitor.StatusChanged += OnStatusChanged;
OnStatusChanged(monitor, monitor.CurrentStatus);   // 先补一次当前值

生活类比

小区公告栏:广播喇叭只在事发那刻喊一句,公告栏一直贴着最新状态。晚回家的人听不到喇叭,但看得见公告栏。两者缺一不可。

契约:事件 + 快照,缺一不可;只有事件的发布者,等于要求订阅者"从出生起就一直在场"。


2.3 刷新颗粒度:由下游的代价倒推,而不是"越精确越好"

场景

界面上有一行状态文字要显示"已经进行了多久"。

直觉做法:每秒刷新一次,看起来最精确。

实际结果:整行每秒重建一次,提示框疯狂闪烁。

为什么

两个机制叠加:

  1. 数据模型是值相等 的(record)------值不变就不会触发更新;
  2. 列表同步用的是清空 + 全部重建------哪怕只有一行变了,整行对象也换新的。

于是文本每秒变一次 → 整行每秒重建一次 → 下游的可视控件(提示框、浮动层)被反复销毁重建 → 肉眼看到闪烁。

复制代码
   秒级刷新:   [行1][行2][行3]  → 全部销毁 → [行1'][行2'][行3']
                     ↑ 每秒一次,提示框跟着重建
   分钟级刷新:  [行1][行2][行3]  → 只有内容真的变了才重建

结论

显示粒度由"下游刷新的代价"决定,不是由"数据源能给出多精确"决定。

  • 下游便宜(纯文本标签)→ 可以细;
  • 下游昂贵(重建控件、重排布局、动画)→ 必须粗;
  • 想两者兼得?先把"清空 + 重建"改成"增量更新",再谈精度。

生活类比:挂钟和秒表。你要的是"大概几点了",挂钟够了;秒表每跳一下你都去换一次电池,就是自找麻烦。


2.4 判断子集:界面可用性 ⊆ 门禁判据

原则

界面上"能不能点",必须是"真正拦不拦"的子集。

复制代码
   ┌──────────────────────────┐
   │   门禁判据(真实拦截)      │
   │   ┌──────────────────┐   │
   │   │ 界面可用性判断     │   │  ← 必须完全落在这个圈里
   │   └──────────────────┘   │
   └──────────────────────────┘

反例(本周修过的)

  • 按钮看着能点,点了才弹"不允许" → 界面判断比门禁宽,用户白点一次;
  • 按钮灰着,但真实门禁其实放行 → 界面判断比门禁窄,功能被无谓锁死。

两种偏差都来自同一个根因:同一条规则被抄了两份。

解法

界面的可点性直接调用门禁的同一个判断函数,不要另写一份"如果状态是 A 或 B 就......"。

csharp 复制代码
// 示意:界面只问一次"现在能不能做",不自己写规则
public bool CanStart => _gate.BlockReason(_status) is null;

生活类比:门卫和"请勿打扰"牌。牌子必须由门卫来挂------如果保洁阿姨自己也挂一块,两边迟早对不上。


2.5 门禁五件套

① fail-closed(失败关闭)

是什么:判不清的时候,默认"不允许"。

生活类比:门禁刷卡机坏了,是"锁死等人来修",不是"干脆敞开"。

复制代码
   读到状态 = 就绪      → 放行
   读到状态 = 未就绪    → 拦
   读不到 / 未知 / 异常 → 拦   ← 这一条最容易漏

为什么:未知状态按"允许"处理,等于把最不确定的时刻变成最危险的时刻。

② 校正租约(lease)

是什么:某个稀缺资源(比如"谁可以操作设备")用"发牌子"的方式独占;拿到牌子才能动,交回牌子别人才能动。

生活类比 :一个车位只有一把钥匙。你停进去要拿钥匙,走的时候还回来。没拿钥匙就停,等于占别人的位。

工程要点:

  • 租约要有归属:谁拿的,只有谁能还;
  • 租约和"前置条件"是两件事:有资格操作 ≠ 现在可以操作(还得看设备状态)。

本周最容易混的就是这两个"门":

复制代码
   门1:校正租约     → "现在轮到谁用"(互斥)
   门2:预热门禁     → "设备现在能不能用"(就绪)
   两道门都要过,缺一不可

③ 命令顺序依赖

是什么 :有些命令必须先配置、再启用 ,顺序反了,设备会拒绝,而且可能沿用上一次的设定值。

生活类比:开音响前先调音量。你要是先开功放、后调音量,那一声"砰"就不会缺席。

csharp 复制代码
// 示意:错误顺序 ------ 先启用,后配置
device.Enable();
device.SetLevel(3);      // 设备已经在跑了,这次设置可能被忽略

// 示意:正确顺序 ------ 先配置,后启用
device.SetLevel(3);
device.Enable();

为什么危险 :拒绝可能是静默的,或者返回"成功"但值是旧的。所以顺序错了,症状往往不是报错,而是"数值不对"。

④ 残留设定值

是什么 :你读回来的"当前设定",可能是上一轮留下的,不是这一轮生效的。

生活类比:体重秤上还站着上一个人,你看到的是他的体重。

复制代码
   上一轮:目标值 = 200(但实际只到了 180)
   本轮:  我设 200,读回 200 → "成功"?
            ↑ 读回的是我自己的目标,不是设备的实际输出

判据 :验证必须用独立观测(实际输出、外部读数),不能用"我请求的值"自证。

⑤ 资源所有权

是什么 :谁打开的资源,谁负责关闭;不能替别人关。

生活类比 :下雨天借伞。你借的伞你还;不能因为"伞是我拿在手上的"就把别人的伞还了。

csharp 复制代码
// 示意:只关自己开的
bool owned = false;
try
{
    if (resource.IsOff) { resource.TurnOn(); owned = true; }   // 只有我开的才算我的
    DoWork(resource);
}
finally
{
    if (owned) resource.TurnOff();    // 别人开的,别动
}

为什么:资源可能被上层、被并行流程、被别的模块先打开了。无条件关闭,等于把别人的工作打断------而日志上看起来只是"正常收尾"。


第三块 · 语法

以下示意代码均为公开常识逻辑(计数器、状态机、列表),只用于演示语法;类名、字段名全部泛化。

3.1 is ... or ...:模式匹配

csharp 复制代码
int status = GetStatus();

if (status is 2 or 3)      // 等价于 status == 2 || status == 3
{
    EnableButton();
}

为什么这么写 :C# 9 起支持在 is 后面用 or/and/not 组合常量模式,读起来像自然语言。

什么时候用:判断"属于某几个离散取值之一",特别是状态机里的合法状态集合。

常见坑:

  • is 2 or 3 不是 ||,不能塞表达式 :is a or b 里的 a/b 必须是常量或模式;
  • 优先级:is not 0 and not 1 要按模式语法理解,不要按布尔运算的习惯猜。

3.2 record:值相等

csharp 复制代码
public record StatusItem(string Text, int Level);

var a = new StatusItem("就绪", 1);
var b = new StatusItem("就绪", 1);

Console.WriteLine(a == b);        // True ------ 逐个成员比较

为什么这么写 :普通 class 的 == 比的是"是不是同一个对象";record 编译器自动生成 Equals / GetHashCode / ==,比的是成员值。

什么时候用:数据载体(DTO、状态快照、配置项),尤其是你要靠"值没变就不更新"来省刷新的时候。

常见坑:

  • 成员里有集合 时,record 仍按引用比较,两个内容相同的列表不相等;
  • record struct 与 record class 行为有细微差别(默认值、可空性)。

3.3 [ObservableProperty]:源生成器属性

csharp 复制代码
public partial class PanelViewModel : ObservableObject
{
    [ObservableProperty]
    private int _clickCount;          // 编译期生成 ClickCount 属性 + 通知
}

为什么这么写 :手写 public int ClickCount { get => ...; set { ...; OnPropertyChanged(); } } 太啰嗦,源生成器在编译期替你生成。

什么时候用 :绑定到界面的属性,且类需要是 partial。

常见坑:

  • 忘了 partial → 编译报错;
  • 字段名 _clickCount 生成属性 ClickCount:大小写规则要记牢,别自己再手写同名属性;
  • 生成器只在编译期跑,调试时看不到源文件,别以为"没生成"。

3.4 try / catch / finally + 跨块局部标志

csharp 复制代码
bool owned = false;               // 必须在 try 外面声明
try
{
    Acquire();
    owned = true;
}
catch (Exception ex)
{
    Log(ex);
}
finally
{
    if (owned) Release();         // finally 里能看到 try 里改的值
}

为什么这么写 :finally 保证"无论成功、失败、提前返回"都执行收尾;标志放在外面,块与块之间才共享。

常见坑:

  • catch (Exception) 会把取消异常 也吞掉 → 主动取消要先 catch (OperationCanceledException) 静默处理;
  • 在 finally 里 return 会吞掉原异常,别这么写。

3.5 纯函数:不改入参,返回新对象

csharp 复制代码
static byte[] SwapChannels(byte[] source)
{
    ArgumentNullException.ThrowIfNull(source);

    var result = new byte[source.Length];      // 新数组,不动入参
    for (int i = 0; i + 2 < source.Length; i += 3)
    {
        result[i]     = source[i + 2];
        result[i + 1] = source[i + 1];
        result[i + 2] = source[i];
    }
    return result;
}

为什么这么写:调用方的数据不会被"顺手改掉",函数可重复调用、可并行、好测试。

常见坑:

  • 原地修改入参,调用方以为"我传进去的没变";
  • 返回的是同一个引用(只是换了个名字),等于没做保护。

3.6 ThrowIfNull:早失败

csharp 复制代码
void Render(byte[] pixels)
{
    ArgumentNullException.ThrowIfNull(pixels);
    // ...
}

为什么这么写 :参数不对就在入口处炸,堆栈指向调用方,而不是在下游某个深处空引用。

常见坑 :只防 null,不防空集合------空集合要不要拦,得按业务定。

3.7 async / await + Task.Run

csharp 复制代码
async Task SaveAsync(string path)
{
    await Task.Run(() => CopyLargeFiles(path));   // 重活挪到线程池
    RefreshList();                                // 回到原上下文继续
}

为什么这么写:界面线程被同步重活占住,界面就"假死";把重活丢到线程池,界面线程只负责发状态。

常见坑:

  • async void 只能用于事件处理器,异常没法捕获;
  • 在 UI 上下文里 .Result / .Wait() 会造成死锁;
  • ConfigureAwait(false) 会丢掉回到原线程的能力,界面代码慎用。

3.8 Dispatcher:线程亲和

csharp 复制代码
void ShowConfirm(string text)
{
    var dispatcher = Application.Current?.Dispatcher;
    if (dispatcher is null || dispatcher.CheckAccess())
    {
        Ask(text);                       // 已在 UI 线程,直接来
        return;
    }
    dispatcher.Invoke(() => Ask(text));  // 不在,就切过去
}

为什么这么写 :界面控件有线程亲和性------只能在创建它的线程上访问。后台线程直接弹窗,轻则异常,重则卡死。

常见坑:

  • Invoke 是同步 的:后台线程会等 UI 线程处理完,双方互相等就死锁;能用 InvokeAsync 就用 InvokeAsync;
  • CheckAccess() 是"我是不是 UI 线程",别和"控件是否已加载"搞混。

3.9 事件订阅生命周期

csharp 复制代码
public void OnEnter()
{
    _monitor.StatusChanged -= OnStatusChanged;   // 先退订,防重复
    _monitor.StatusChanged += OnStatusChanged;
    OnStatusChanged(_monitor, _monitor.CurrentStatus);   // 立刻补一次快照
}

public void OnLeave()
{
    _monitor.StatusChanged -= OnStatusChanged;
}

为什么这么写 :订阅是强引用,不退订会让发布者一直握着订阅者 → 内存泄漏;页面复用不先退订 → 同一个事件被处理两次。

常见坑:

  • 只在构造函数里订阅,页面切回来就收不到(视图复用);
  • 只 += 不 -=,页面关了还在跑。

3.10 冻结对象(Freezable)

csharp 复制代码
var brush = new SolidColorBrush(Colors.Orange);
brush.Freeze();          // 冻结后不可修改

为什么这么写 :冻结后的对象可以跨线程共享,而且性能更好(不用做线程校验)。

常见坑:冻结后任何修改都会抛异常------如果要按状态改颜色,就得每次新建一个冻结的,而不是改同一个。

3.11 集合断言

csharp 复制代码
Assert.Equal(new[] { 1, 2, 3 }, actual);          // 顺序敏感
Assert.Equal(new[] { 1, 2, 3 }, actual.OrderBy(x => x));   // 不强求顺序时先排

为什么这么写 :断言失败时能直接看到"哪一位不一样",比 Assert.True(a.SequenceEqual(b)) 的诊断信息好得多。

常见坑 :集合是顺序敏感的,被测代码如果没保证顺序,测试就会偶发失败。


收尾:这一周真正带走的四句话

  1. 状态是读的,不是猜的------读不到就按不可用处理。
  2. 你不是过程的拥有者,就别替它收尾------观察型只提示,不打断。
  3. 同一条规则只写一份------界面判断必须是门禁的子集。
  4. 验证要用独立观测------拿自己的请求值自证成功,是最贵的错觉。
相关推荐
云中飞鸿2 小时前
.Net和C#有什么关系?(从C++视角观察)
开发语言·c#·.net
淡海水5 小时前
07-04-源码-.NET Core GC清扫与压缩
unity·架构·c#·.netcore·gc·1024程序员节
zwm26988881513 小时前
hd 3#负一西送风,能远程启不能远程停
c#
Ai-_Man13 小时前
Meta Muse还是不可以导出左侧所有对话,只能点进去单条内容导出,方法不对吗?
开发语言·人工智能·ai·小程序·c#·1024程序员节
2401_885885041 天前
国际语音C#接口怎么写?.NET环境下实现国际语音验证码批量下发
开发语言·c#·.net
淡海水1 天前
07-03-源码-.NET Core GC标记与扫描
unity·c#·.netcore·gc
波力海苔夹心脆6751 天前
C# string 字符串方法大全:判空、查找、截取、分割、替换一篇讲透
经验分享·c#·.net·字符串·string·入门教程
阿松爱学习2 天前
【Unity开发】Unity长截图功能实现
unity·c#·游戏引擎
_oP_i2 天前
Windows 11 家庭中文版 25H2
c#