2026-09-03 博客:配置与密码 · 合并代码 · 超时排查
今天聊了三件事:① 程序的「设置」和「登录密码」为什么是两个抽屉;② 多条分支代码怎么合到一起 + 三个防卡死小套路;③ 一个「超时」报错怎么定位(三板斧)。
第一件:配置文件 和 登录密码,是两个抽屉
1. 两个抽屉:设置 和 密码,不在一处
先想一个生活场景:
你有一台单反相机,它有两样家当要保管:
- 相机的设置(白平衡、对焦微调、镜头标定)------决定"拍出来准不准"。
- 锁屏密码------决定"你能不能开机"。
这两样东西物理上存在完全不同的地方:设置存在相机机身里,锁屏密码存在你脑子里(或密码管理器里)。
程序也一样,它有两样家当:
程序根目录/
│
├── 配置文件夹/ ← 「设置」住这里(标定 / 连接参数)
│ ├── 机器配置.json ← 数据(校准等)
│ └── 连接配置.json ← 连接参数(设备地址、端口、连接串)
│
├── 程序.exe ← 程序本体
├── 账号库.db ← 「密码」住这里(账号 + 密码的哈希)
└── 记住密码.dat ← 勾了「记住密码」后生成的加密文件
关键就一句话:
标定(设置)在「配置文件夹」里,密码在「程序旁边」------两个抽屉,不挨着。
所以你「备份配置文件夹」,只保住了设置(标定),没保住密码。恢复之后,设置回来了,密码还是得重新输。
2. 程序"缺了才补",不会主动删
再想一个生活场景:
仓库管理员清点货架,他的原则是------货架空了才补货,有货就绝不动。
程序启动时对配置和数据库,也是这个原则:
csharp
// 配置文件:文件不存在才生成一份默认的,存在就直接读
if (!File.Exists(配置路径))
生成默认配置();
// 账号数据库:表不存在才建表,有表就原样用(不删、不覆盖)
建表如果不存在(账号库);
为什么这么设计?因为程序不敢动你已有的数据:
- 标定数据是现场辛辛苦苦校准出来的,程序不能一启动就重置它。
- 账号密码是真实存在的,程序不能一启动就清空。
所以「密码没了、标定错了」,不是程序自己干的,是外面的动作(换程序、放错位置)弄丢的。
3. 那「重新编译程序」会改配置吗?
先把两个动作分清:
- 编译 :把代码变成 exe,只写「程序文件」,不碰配置文件夹。
- 运行新程序:这才可能碰配置。
而「运行」时,两种配置文件的态度还不一样------像两种收银员:
- 甲收银员 (连接配置):每次下班都把账本重抄一遍(读完 → 整理 → 写回)。如果新版程序改了字段结构,这一重抄就会加字段、丢字段。
- 乙收银员 (标定配置):账本在就只看不动,账本丢了才补一本新的。
csharp
// 甲:每次启动都回写(可能改内容)
配置 = 读取(路径);
迁移旧字段(配置);
配置.写回(); // ← 每次都写
// 乙:缺了才写,在就只读
if (!File.Exists(标定路径))
生成默认标定();
第二件:合并代码的几种姿势 + 三个防卡死套路
1. git 合并的几种姿势
先想一个生活场景:
你同时维护两本账:一本「主线账」(大家都在用),一本「你的草稿账」(你私下加的功能)。现在要把草稿里的东西并回主线。
merge vs rebase ------ 两种「并账」方式
merge(合并):把草稿账直接「钉」进主线,中间留一张「此处合并了草稿」的纸条。
主线: A → B → C ──┐
├→ M (合并提交,纸条)
草稿: B → D → E ┘
rebase(变基):把草稿里的 D、E 拆下来,重新「抄」到主线最新处,历史成一条直线,不留纸条。
主线: A → B → C
草稿: A → B → C → D' → E' (D、E 重抄成 D'、E')
- 区别一句话:merge 留「合并痕迹」,rebase 让历史「一条线、干净」。
bash
git pull # merge:直接拉,会产生一条「合并 xxx」提交
git pull --rebase # rebase:本地提交重放到远程最新之上,历史线性
squash ------ 多张草稿揉成一张正稿
你为一个小功能改了 5 次(5 个提交)。并入主线时不必把 5 个都带过去,可以揉成一个提交。
bash
git reset --soft <基准> # 把 5 个提交退回到暂存区(改动不丢)
git commit # 一次性重新提交成 1 个
- 类比:5 张草稿纸 → 揉成 1 张干净的正稿交上去。
cherry-pick ------ 从一盆水果里挑几个
不必把整条草稿分支都合过来,只挑某几个提交。
bash
git cherry-pick <提交A> <提交B>
git cherry-pick -n <提交A> # -n:只应用改动、先不提交,攒几个再统一提交
顺序依赖 ------ 先合谁,决定会不会撞
两个提交,若 B 是在 A 基础上改的(B 依赖 A):
-
先合 A、再合 B → 顺理成章,零冲突。
-
单独合 B → B 里带着 A 的改动,会和主线撞车。
-
类比:先铺地板、再放家具(顺序对,很顺);反过来先放家具再铺地板就卡住。
冲突两类型:位置冲突 vs 内容冲突
两边代码改到同一个位置,git 分不清先后 → 冲突。但冲突分两种:
| 类型 | 场景 | 解法 |
|---|---|---|
| 位置冲突 | 两人往同一个空白处写字,内容不矛盾(一个写"登记"、一个写"签名") | A+B 两边都留,排个先后 |
| 内容冲突 | 内容互相矛盾(一个写"开"、一个写"关") | 只能选一个,或重写 |
- 大多数情况是「位置冲突」:两边内容互补、都能要,解法就是都留(A 在前 B 在后)。
2. 三个「防卡死」小套路
套路 1:回环自激 → 闸门标志
现象:点一下 A,B 跟着变;B 一变,又去触发 A......像两面镜子对放,光在里面无限反射。
A 变 → B 变
↑ ↓
└── B 又触发 A ──┘ (无限循环)
通用解法:闸门标志(flag)------区分「人点的」和「程序自己改的」:
csharp
bool 闸门 = false;
void A变了()
{
if (闸门) return; // 闸门关着:程序自己在改,别理
...正常响应...
}
void 程序要回写B()
{
闸门 = true; // 先关门
try { B = ...; } // 改 B
finally { 闸门 = false; } // 改完开门
}
套路 2:磁盘满了自动清 → 水位法
现象:磁盘越写越满,满了程序就崩。想在「快满」时自动删旧文件。
通用解法:两个水位------到「高水位」开始删(从旧到新),删到「低水位」就停:
使用率
高水位 ────────── 触发:开始删旧文件
...
低水位 ────────── 停止:不删了
csharp
if (磁盘使用率 > 高水位)
{
foreach (旧文件 in 从旧到新)
{
删除(旧文件);
if (磁盘使用率 <= 低水位) break; // 降到低水位就停
}
}
- 类比:水桶满到警戒线开始往外倒,倒到一半就停,别倒干。
套路 3:取消异常,别被「大网」接住
现象:用户点了「取消」,程序却在弹「出错了」。因为「取消」被当成普通错误记了下来。
csharp
// 错:大网把「取消」也接住了,取消时误报错
catch (Exception)
{
记错误日志();
}
// 对:先单独放走「取消」,再处理「真错误」
catch (OperationCanceledException) { throw; } // 取消是正常退出,不是错误
catch (Exception)
{
记错误日志();
}
- 类比:渔网捕鱼,先把手里的「放生鱼」(取消)捞出来放掉,再处理「真鱼」(真错误)。
第三件:超时排查的三板斧
背景:现场报错「读取图像超时」。怎么定位?三招。
第一招:先搞清「超时」是怎么判的 ------ 竞速
程序等一张图,不是傻等,而是「读图」和「计时器」赛跑,谁先到终点谁赢:
csharp
await Task.WhenAny(读图任务, Task.Delay(超时时长));
// 计时器先完成 → 判定超时
- 类比:等快递,同时设个闹钟;闹钟先响、快递没到 → 超时。
- 关键:这不是「读了一半」,是「闹钟响了,一张都没来」。
第二招:看「收到计数」区分「掉图」还是「没触发」
程序每收到一张图,计数 +1。日志里「收到 0」= 一张都没来。
- 「收到 0」≠「掉图」(掉图是收到几张后断了),而是「从头到尾一张没来」→ 问题在前端(没触发),不在传输中途。
第三招:按「链路分段」逐段排除
图像从设备到程序的链路,拆成三段:
[段1] 上位机 → 控制器 发采集信号 ✅ 正常
[段2] 控制器 → 设备 发触发脉冲 ❓ 唯一没验的断点
[段3] 设备 → 上位机 传回图像 ✅ 正常
- 段1、段3 都正常,只剩段2「控制器有没有真发触发」------用排除法(掉图❌、卡死❌、断线❌、全程坏❌)逼到唯一断点。
- 类比:破案,逐条排除不在场证明,剩下的那个就是真相。
附:排查前先确认「用的是哪套驱动」------ DI 工厂
同一个接口,底下可能有两套实现(一套自带收图线程、一套纯回调)。排查前必须先确认现场到底实例化了哪套,否则容易分析错对象。
- 类比:打客服电话,先按号码转对「接线员」,别对着 A 接线员分析 B 的问题。
语法附录
文件 / 路径
| 语法 | 作用 |
|---|---|
File.Exists(路径) |
判断文件在不在(返回 bool) |
Path.Combine(目录, 文件名) |
拼路径,自动补斜杠 |
AppDomain.CurrentDomain.BaseDirectory |
拿「程序自己所在的目录」 |
git
| 命令 | 作用 |
|---|---|
git pull --rebase |
变基拉取,历史一条线 |
git reset --soft <基准> |
多提交揉成一个前的准备 |
git cherry-pick <提交> / -n |
挑提交 / 挑但不提交 |
git log --author=X A..B |
查某人在 A 有、B 没有的提交 |
C# 三个骨架
- 闸门标志 :
if (_flag) return;+try { _flag=true; ... } finally { _flag=false; } - 水位法 :
if (使用率 > 高水位)里循环删,if (使用率 <= 低水位) break - 竞速超时 :
await Task.WhenAny(读图任务, Task.Delay(时长)) - 放走取消 :
catch (OperationCanceledException) { throw; }放前面
一句话总结
- 设置和密码是两个抽屉,备份要两个都备份。
- 合并代码:rebase 让历史干净、squash 揉多为一、cherry-pick 挑着拿、顺序对就零冲突。
- 三个套路:闸门标志防「自己触发自己」、水位法防「磁盘写满」、先放走取消再抓真错误。
- 超时排查:先懂「竞速」怎么判、再看「收到计数」分掉图/没触发、最后按链路逐段排除。