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