ProcessJob 构造函数 — 语法与概念日期2026.08.05一、空合并运算符 ?? — 兜底安全网varresultsourceA??Fallback();语义左边有值取左边左边为 null 才走右边。不是两条对等的分支而是一条主路径加一条保险丝。正常情况左边永远有值。同类运算符a??b;// 等价于if (a null) a b; 只一个参数obj?.Property;// 安全导航obj 为 null 则整个表达式返回 null不抛异常obj?.Method();// 安全调用obj 为 null 就不执行方法二、LINQ 筛选链 — Where Contains 白名单模式varresult[..allItems.Where(itemallowedIds.Contains(item.Id))];拆解逻辑遍历集合allItems的每个元素判断元素的Id是否在allowedIds列表里Contains返回 true → 保留 / 返回 false → 丢弃保留下来的装进新集合核心概念Lambda 表达式item allowedIds.Contains(item.Id)— 左边是输入参数右边是返回布尔值的判断条件Where过滤器保留满足条件的元素Contains集合方法判断某个值是否在集合里白名单模式不是比较相等而是看是否属于某个预定义集合[.. 集合]是 C# 12 集合表达式把筛选结果展开成新集合。和new ListT(集合)等价。三、GUID — 全局唯一标识符varidGuid.NewGuid().ToString();// 输出类似 a1b2c3d4-e5f6-7890-abcd-ef1234567890// 碰撞概率趋近于零相当于永不重复每个对象创建时自动分配一个唯一 ID。用途跨层关联。为什么不直接存对象引用// ❌ 对象引用没法持久化parent.ChildrennewListChild{obj1,obj2};// 序列化到文件会丢// ✅ 字符串 ID 可以parent.ChildIdsnewListstring{obj1.Id,obj2.Id};// 可以写进 XML反查时// 按 ID 从全集里找回对象varchildallItems.First(itemitem.Idparent.ChildIds[0]);设计模式ID 存盘、内存反查。对象引用只管内存里的即时关系ID 管跨层的持久化关联。四、序列化 vs 反序列化// 存盘对象 → 字符串stringxmlSerialize(myObject);// 内存 → 硬盘// 读盘字符串 → 对象MyObjectobjDeserialize(xml);// 硬盘 → 内存这两种操作不是可逆的。细节会丢对象引用 → 变成 ID 字符串事件绑定 → 序列化丢失私有字段 → 取决于序列化器是否处理所以设计数据模型时要区分哪些字段序列化存盘、哪些只在内存里用。五、数据来源的分层一个复杂对象的字段来自不同时刻publicclassContext{// A 层早期算好存文件里publicListItemPreCalcItems;// 从配置加载publicDictionarystring,ParamParams;// 从计算结果加载// B 层执行前一瞬间读硬件publicdoubleVoltage;// 硬件实时值覆盖旧值publicdoubleCurrent;// C 层用户交互时陆续产生publicListGraphicCanvas;// 用户操作结果已就绪}分层的关键A 层值来自离线计算加载即有B 层值必须执行前覆盖旧值可能过期C 层值在用户操作完就已就绪运行时只读执行前的最后一步不是加载全部数据而是用硬件值覆盖那些会变的字段。六、命名约定后缀即语义ListItemitemsToProcess;// ToProcess → 待处理列表DictionaryK,VidMapping;// Mapping → 映射表键值对ParamObjectitemParameter;// Parameter → 参数对象描述某个东西的属性代码名字自带说明书ToProcess— 这堆东西还要过一遍流程不是最终结果Mapping— 这是一个键值对应关系Parameter— 这是一个盒子装了一组相关属性Count— 集合的元素个数Id/Ids— 单数是一个键复数是键的列表读代码时后缀比前缀重要——后缀告诉你它是哪种东西。七、Dictionary — 键值映射表// 定义键类型 → 值类型varmapnewDictionarystring,Item();// 写入map[item_A]objA;// 读取varvaluemap[item_A];// 键不存在会抛异常varvaluemap.GetValueOrDefault(item_A);// 键不存在返回默认值用途用唯一 ID 做索引快速定位对象O(1) 查找。和 List 的区别// List → 遍历查找O(n)varitemlist.First(xx.IdtargetId);// Dictionary → 直接定位O(1)varitemmap[targetId];适用场景知道 ID要拿对象 → Dictionary要遍历所有元素做批量处理 → List既要遍历又要按 ID 查 → 两个都建数据两份指针同一份对象八、嵌套安全访问 — ?. ?? 组合技// 常规写法层层判空intcount;if(obj!null){if(obj.Children!null){countobj.Children.Count;}else{count0;}}else{count0;}// 链式安全访问 兜底intcountobj?.Children?.Count??0;原理obj?.Children— obj 是 null → 返回 null不崩。obj 有值 → 取 Children?.Count— Children 是 null → 返回 null不崩。有值 → 取 Count?? 0— 左边算出 null → 用 0链式写法等价于每一步都检测中间结果是不是 null任何一步 null 就短路。九、! — 空值压制操作符varvaluemaybeNullSource!.Property;// ↑// 我确信它不为 null别报警告作用告诉编译器我比你知道得多这里不会 null。编译器不再报空引用警告。风险如果运行时真的是 null照样崩。!只关警告不管安全。使用原则只有能逻辑证明不为 null 时才用刚赋值完马上用 → 安全从外部传入的参数 → 别用乖乖判空十、字符串插值 — $“…”varmsg$总数:{total}, 有效:{valid}, 来源:{source};花括号里可以是变量、属性、表达式、方法调用$长度{items.Count0?items.Count:0}$日期{DateTime.Now:yyyy-MM-dd}$状态{GetStatus()}// 多层嵌套不会崩空值显示为空字符串$名字{obj?.Name}// obj 是 null → 显示 和拼接的区别拼接每次 都创建新字符串插值一次完成更高效。可读性也更好。十一、ObservableCollection vs List// 普通 List改了 UI 不知道varlistnewListItem();list.Add(item);// UI 不刷新// ObservableCollection改了 UI 自动更新varcollectionnewObservableCollectionItem();collection.Add(item);// UI 立即显示新项ObservableCollection实现了INotifyPropertyChanged接口。集合增删改会自动通知 UI 框架刷新。适用场景数据要实时显示在界面上 → ObservableCollection纯计算用的中间数据 → List 就行十二、延迟执行 vs 立即执行// 延迟声明时不跑用到时才跑IEnumerablevarqueryitems.Where(xx5);// 没执行只是记录了规则// 立即声明时就跑完List、Arrayvarresultitems.Where(xx5).ToList();// 立即执行装进新列表延迟执行的好处链式组合多个 Where/Select 不会重复遍历可取消中途 break 就不浪费计算立即执行的好处结果固定源数据改了它不变可以反复枚举不重复计算什么时候必须立即执行.ToList()要多次遍历同一个筛选结果要在遍历过程中修改源集合要把数据传给后续流程长期持有十三、双重坐标空间 – 物理坐标(μm) vs 像素坐标(px)一张图拍全景一张图拍特写 – 同一颗螺丝在这两张图上的行列号完全不同但螺丝在桌面上的实际位置没变。两套坐标物理坐标 (StartX, StartY) → 从机器台面原点起算单位 μm ↓ InvertMatrix.Transform 像素坐标 (canvaspoint) → 在当前这张图上排第几列第几行单位 px类比螺丝离桌面左上角 5cm – 这是物理坐标。手机拍全景照螺丝在第 600 列 – 这是像素坐标。手机凑近拍特写螺丝在第 3200 列 – 还是像素坐标同一颗螺丝换张图就变了。为什么要分两套// 显示用像素坐标 -- 在当前这张图上的行列号drillData.PixelStartXcanvaspoint.X-radius;// 匹配用物理坐标 -- 都从同一个台面原点起算能做大小比较// component.StartX ≤ drill.StartX ≤ component.EndX物理坐标钻孔和元件都从同一个机器台面原点出发可以直接比大小判断钻孔是不是在这个元件框里像素坐标两张不同的图有两个不同的像素空间数字对不上不能用来比InvertMatrix.Transform 做了什么PointpointnewPoint(drillData.StartX,drillData.StartY);// 物理 μm比如 50000PointcanvaspointInvertMatrix.Transform(point);// 像素 px比如 600Matrix 是一张换算表“50000μm 在这张图上对应第 600 列”。InvertMatrix 是同一张表的反向“第 600 列在这张图上对应 50000μm”。每张图有自己的 Matrix所以同一物理点在不同图上的像素位置不同。那个 Bug// ✗ 错把物理坐标 50000μm 直接当像素值塞进去drillData.PixelStartXpoint.X-radius;// 50000 - 5 49995 px// ✓ 对先过转换门拿到真正的像素位置drillData.PixelStartXcanvaspoint.X-radius;// 600 - 5 595 pxPixelStartX是像素字段应该填 ~600结果填了 ~50000。画布宽度可能才 1280 – 圆画到屏幕外面去了。匹配环节同样遭殃元件像素边界在正常范围500~1000钻孔像素飙到五万永远碰不上。十四、为什么匹配用物理坐标、画图用像素坐标Panel 图 vs FOV 图Panel 图是 CCD 从上方拍的全板鸟瞰图分辨率不高1280×1024但能看全貌。操作员在这张图上画框标出要处理的区域。FOV 是 CCD 凑到 Panel 框附近去拍的高清特写数万像素。一个看整体一个看局部两张完全不同的图。同一颗钻孔在两图里的坐标Panel图全景 FOV图特写 孔1: 像素 (600, 400) 孔1: 像素 (14200, 13500) 孔1: 物理 50000μm 孔1: 物理 50000μm像素位置换张图就变物理坐标换哪张图都一样——都从机器台面统一原点起算。为什么要两套匹配 判断钻孔在不在这块元件的方框里元件框从FOV图上画的 物理边界 50000~55000μm 像素边界 14000~14500px 钻孔在Panel图上 物理 50000μm 像素 600px拿像素比600 在 14000~14500 之间不在。但钻孔确实在元件里。拿物理比50000 在 50000~55000 之间在。物理坐标都从同一个原点出发数字可以直接比大小。像素在两套图上编号体系不一样没法跨图比。显示 在屏幕上画圆圈屏幕只认靠第几列第几行不认微米。你得跟它说画在第 600 列不能说画在 50000μm 处。分工物理坐标 (μm)跨图统一尺子 → 用来做数学判断像素坐标 (px)屏幕单元 → 用来画到画布上同一个点的两种表达方式通过 Matrix/InvertMatrix 互转十五、string.Join Select – 一行日志看全貌调试时需要看一批数据的某个字段常见的写法是循环里一条一条输出十个元素就是十行日志。Selectstring.Join可以压缩成一行用 Select 把每个元素抽出一个字段、格式化成字符串片段再用string.Join按分隔符把它粘成一条直接拼进日志字符串。好处是你不用在屏幕上找不同行来对数据一眼整条看清。代价是数据量太大时单行日志会很长适合几到几十个元素的小集合。十六、CancellationToken – 协作式取消为什么不能直接杀线程批量任务跑到一半用户点取消。直接杀线程不行——线程可能在写文件、释放硬件资源暴力中断会留烂摊子。需要一种让线程自己体面退出的机制。信号链CancellationToken 的核心思路是传信物启动任务时创建一个信物把同一个信物传给所有被调方——“拿着有空看一眼我喊停就收工”每个被调方在自己的循环里隔一阵检查一次——“主人让我停了吗”用户点取消 → 信物被标记为已取消 → 下一次某层被调方检查到 → 抛出异常 → 整条调用链干净退出凭什么比 break 管用break只管当前层的循环。循环里调了另一个方法那个方法内部有更多嵌套break穿透不了。CancellationToken 不怕嵌套同一个信物从最外层一路递到最深层的调用者谁拿到都能检查谁发现都能停。调用链里每一层都不需要知道其他层的存在。为什么叫协作式不是 OS 强制掐断是约定——发出信号的人不变魔术收到信号的人主动检查并退出。如果被调方不检查信物永远不会停。双方必须配合所以叫协作式取消。