2026-09-11 一日综合:树的父链、git 概念、三方死锁、静默失败
今天四个话题,看似互不相关,其实串着同一条暗线:「出错了不吭声」比「报错」可怕得多。一个个讲。
主题一:树的父链,和「按归属起名」
需求长这样
想象一个小区:一栋楼里有几户人家,每家有几件快递。要按「门牌号」给快递贴标签。可快递上没写门牌号------门牌号挂在「楼」上。
所以每件快递得先搞清楚「我是哪栋楼的」,再去楼那里抄门牌号。翻译成程序:
每个「子项」要起名,但名字存在「父级容器」上。子项得沿父链向上找容器,把容器的名字拿来用。
概念:树 + 父指针
小区(根)
├── 1号楼
│ ├── 101 户 ← 有快递
│ ├── 102 户 ← 有快递
│ └── 103 户 ← 有快递
└── 2号楼
└── 201 户 ← 有快递
每个节点只记一件事:「我爸是谁」 (父指针)。顺着「父亲」一级级往上爬,就是父链------就像户口本上「父亲:××」一栏,一路翻到老祖宗。
语法:沿父链递归找祖先
csharp
Node FindGroup(Node node, Dictionary<string, Node> all)
{
if (node.ParentId == "") // 到顶了
return null;
if (!all.TryGetValue(node.ParentId, out var p))// 爸爸不在名册里
return null;
if (p is Group) // 爸爸是「组」,找到
return p;
return FindGroup(p, all); // 不是,继续往上
}
递归------函数自己调自己,每次往上走一层。
坑一:字典 key 重名会「悄悄覆盖」
反查用「楼号」当 key,可同一栋楼 3 件快递的楼号全是「1」:
csharp
map["1"] = "快递A";
map["1"] = "快递B"; // 覆盖 A
map["1"] = "快递C"; // 覆盖 B,只剩 C
字典的 key 是唯一的,第二次写入不是「并存」而是覆盖,且不报错。
类比:快递柜取件码只写「1号楼」,可 1 号楼有 3 户------系统没法区分是谁的,后发短信顶掉前面的。
根因:用了「会重名」的编号当唯一 key。key 只区分到「楼」,区分不到「户」。要区分到户,key 得加一级,或用真正唯一的东西。
坑二:找不到就「静默回退」
csharp
string name = FindGroup(node, all)?.Name ?? node.Id; // 找不到就用自己编号兜底
父链断了、爸爸被删了,程序就默默用错误的名字,日志一片祥和------名字看起来好好的,哪来的错?
类比:孩子找不到爸爸,随手拿学号当门牌号贴上,快递永远送不到,还没人喊一声。
教训:兜底的地方一定要记日志,让人能发现「这是兜底才走到这的」。
语法:文件夹名要净化
csharp
string Clean(string name)
{
var bad = Path.GetInvalidFileNameChars(); // 系统不允许的字符
var ok = new string(name.Select(c => bad.Contains(c) ? '_' : c).ToArray());
return string.IsNullOrWhiteSpace(ok) ? "_" : ok; // 全非法兜底
}
任何外部来的字符串,进文件名/路径前先净化,防止保存失败,也防止塞 ..\ 搞路径穿越。
主题二:git 的几个概念
- author date vs committer date :
cherry-pick会保留「原作者时间」(author date)、只更新「提交时间」(committer date)。判断「新写」还是「搬旧稿」,看 author date。 - cherry-pick vs merge :只挑单个 commit 用 cherry-pick;整个分支用 merge。直接 merge 会带进分支上所有没合的 commit。
- merge vs rebase:merge 保留分叉、多一个 merge commit;rebase 把提交搬到目标之上、历史变线性。
- force push 风险:改写已 push 的远程历史,别人基于旧历史的分支会乱。
- 一个功能可能多个 commit :查「这功能合了没」不能只看一个 commit------原始 commit + cherry-pick 到主线的 copy,两个都要查(
git branch --contains)。 - 归并同类提交:同一功能的 feat + docs + fix 归成一条,历史才清爽。
一句话:git 的坑,大半在「历史看起来对、实际错位」。跟主题一的「静默覆盖」一个味。
主题三:一个「三方死锁」的排查
概念:握手互等 = 三方死锁
三个角色,各等各的:
A 等 B 的「第二次允许」
B 等 C 的「出图」
C 等 B 的「触发」
→ 三方互等,谁都不动
本质是对「某个信号要发几次」的约定不一致------A 以为发一次就够,B 以为要发多次,两边各说各话。
类比:三个人过一扇窄门,都让对方先走,结果卡在门口------这不是谁的错,是「让」的规则没对齐。
概念:虚拟轴
- 真实轴:有电机、有驱动器的物理轴。
- 虚拟轴:程序里「虚拟」出来的轴,不接电机,当指挥棒协调真实轴。
类比:乐队的节拍器------自己不出声,用节奏指挥所有乐手。
排查:怎么判断「突然断电」还是「蓝屏崩溃」
看系统事件日志里 Kernel-Power 41 的 BugcheckCode:
= 0:没有蓝屏错误码 → 突然断电(不是软件崩)。- 非 0:蓝屏,有具体错误码。
排查:怎么判断「空等」还是「忙」
- 空等:CPU 掉到个位数、内存分配骤降、GC 不涨、线程池零排队。
- 忙:CPU 高、分配高、GC 频繁。
一段「空白时间」到底是真忙还是干等,看这几个指标就知道------排查卡顿第一件事,先分清「它是在干活,还是在等人」。
主题四:C# 里几种「静默失败」
这四个陷阱,都是「出错了不吭声」:
- fire-and-forget :异步方法不
await,异常被塞进没人观察的 Task,静默吞掉。 catch(Exception)吞掉 OCE :OperationCanceledException(取消)是Exception的子类,笼统 catch 会把「主动取消」当成普通失败,甚至把取消误报成错误。async void异常逃逸 :async void的异常无法被调用方 try-catch 捕获,会逃到同步上下文。- 空 catch + 标志不复位 :
catch { }吞了异常还不复位状态标志 → 下次没法重启。
这几个和主题一的两个坑是同一类病 :出错了不吭声。防法也统一------凡「可能丢数据」或「用了默认值」的地方,留日志。
一句话总结
| 主题 | 一句话 |
|---|---|
| 树的父链 | 沿父指针往上找祖先,字典 key 重名会静默覆盖 |
| git | 历史「看起来对、实际错位」,搬 commit 时最要小心 |
| 三方死锁 | 三个角色各等各的,根子是「信号发几次」没对齐 |
| 静默失败 | 不 await、笼统 catch、async void,都是异常悄悄溜走 |
今天四件事,贯穿一条线:「静默」是排查的头号敌人。
(本篇为脱敏笔记:不出现真实类名/方法名、产品名、协议消息名与具体业务数字,仅保留通用概念与语法。)