# ProcessJob 构造函数 — 语法与概念

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))];

拆解逻辑:

  1. 遍历集合 allItems 的每个元素
  2. 判断元素的 Id 是否在 allowedIds 列表里
  3. Contains 返回 true → 保留 / 返回 false → 丢弃
  4. 保留下来的装进新集合

核心概念:

  • 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 的核心思路是传信物:

  1. 启动任务时创建一个信物
  2. 把同一个信物传给所有被调方------"拿着,有空看一眼,我喊停就收工"
  3. 每个被调方在自己的循环里隔一阵检查一次------"主人让我停了吗?"
  4. 用户点取消 → 信物被标记为"已取消" → 下一次某层被调方检查到 → 抛出异常 → 整条调用链干净退出

凭什么比 break 管用

break 只管当前层的循环。循环里调了另一个方法,那个方法内部有更多嵌套,break 穿透不了。

CancellationToken 不怕嵌套:同一个信物从最外层一路递到最深层的调用者,谁拿到都能检查,谁发现都能停。调用链里每一层都不需要知道其他层的存在。

为什么叫协作式

不是 OS 强制掐断,是约定------发出信号的人不变魔术,收到信号的人主动检查并退出。如果被调方不检查信物,永远不会停。双方必须配合,所以叫协作式取消。

相关推荐
用户298698530142 小时前
无需安装 Excel,也能轻松将 CSV 转为 XLS 或 XLSX
后端·c#·excel
在世修行3 小时前
从零打造 C# 工业视觉检测系统(五):相机连接、连续取流与双路实时预览
数码相机·c#·视觉检测
JckOLF04Z3 小时前
C#客户端的异步操作
开发语言·c#
曹牧3 小时前
C#:注释嵌套
开发语言·c#
LccKyI3 小时前
C# 零基础学习 Day01|基础概念梳理 + 思维导图总结
开发语言·笔记·学习·c#
雪隐19 小时前
WPF + MVVM 实战系列01-一个老狗回炉重造 WPF 的 12 天血泪史
sqlite·c#·mvvm
在世修行21 小时前
从零打造 C# 工业视觉检测系统(四):海康威视 MVS SDK 入门与相机枚举实战
数码相机·c#·视觉检测
神秘的MT21 小时前
C# WinForm Socket 服务器 + 串口转发
服务器·网络·学习·c#
czhc11400756631 天前
8.4:今日概念与语法总结
c#