2026-08-24 博客:几个让你少踩坑的概念

2026-08-24 博客:几个让你少踩坑的概念

今天聊的东西有点杂,但都指向同一件事:代码/数据在"边界"处最容易出错------整数到上限、循环停不下来、编码对不上、数据库连不上。一个个说。


1. CancellationToken:给"死循环重试"装一根刹车线

概念

你有没有写过这种代码:连数据库失败 → 重试 → 再失败 → 再重试,用 while(true) 转圈。平时没事,可一旦数据库永久坏了 (密码错、表没了),这个循环就永远停不下来 ,调用它的地方跟着永久卡死

问题不在"重试",在于没有出口CancellationToken 就是给这个循环装一根「刹车线」------外面一喊停,循环立刻退。

类比

CancellationToken = 餐厅点餐后的「取消按钮」线路。它本身不做任何事,就是一个信号

  • ThrowIfCancellationRequested() = 厨房每做一步前瞄一眼「取消按钮被按了吗?按了就收工」。
  • Task.Delay(毫秒, token) = 等待出餐时也留意取消,一按就醒。

伪代码(改造前 → 改造后)

csharp 复制代码
// 改造前:死循环,没有出口
void 保存数据()
{
    while (true)
    {
        try { 写入数据库(); return; }
        catch (异常) { Thread.Sleep(5000); }   // 干等 5 秒,谁也打断不了
    }
}

// 改造后:能被打断
void 保存数据(CancellationToken token = default)   // ① 加一根刹车线
{
    while (true)
    {
        token.ThrowIfCancellationRequested();       // ② 每轮先问:要我停吗?
        try { 写入数据库(); return; }
        catch (取消异常) { throw; }                 // ③ 取消要原样抛出去
        catch (异常) { await Task.Delay(5000, token); }  // ④ 等待也能被打断
    }
}

语法详讲

语法点 为什么这么写 / 常见坑
CancellationToken token = default default 表示"不传也能用",不破坏其它调用方
token.ThrowIfCancellationRequested() 被取消就抛 OperationCanceledException,是给循环开的出口
Task.Delay(5000, token) 对比 Task.Delay(5000):前者等待期间可被取消,后者是"干睡"
catch (OperationCanceledException) { throw; } 放在 catch (Exception) 前面 坑:catch(Exception) 会连取消异常一起吞掉。必须先单独 catch 取消异常并 rethrow,否则"取消"会被当成普通失败重试,永远停不了

源 vs 令牌(权限分离)

CancellationToken 其实有「一对」,别混:

能干什么 关键成员
CancellationTokenSource(源 / 扳手) 能「」------主动取消 .Cancel()
CancellationToken(令牌 / 信号线) 只能「 .IsCancellationRequested
  • .Token:从源上接一根信号线出来。
  • 为什么分两个:权限分离------取消权留在源手里(能按扳手的人),知情权(能不能读"是否取消")发出去给干活的人。干活的人只能"看灯有没有亮",不能自己拉闸。
  • 联动CreateLinkedTokenSource(令牌A, 令牌B) = 一个联动开关,同时监听两个令牌,任一取消就取消。常用于把"外部取消 + 内部取消"合并成一个。

2. int 溢出:为什么 2GB 用 int 存不下

概念

int 是 32 位有符号数,范围 -2,147,483,648 ~ 2,147,483,647(约 ±21 亿)。

2GB = 2,147,483,648 字节 = 正好比 int 的最大值大 1

所以「2GB」这个数,int 存不下------差一个字节。

类比

int 像汽车里程表 ,最多显示 999,999,再走 1 公里就「回绕」回 000,000。数值一旦超上限,不是报错,而是悄悄变成负数或一个错的小数

伪代码

csharp 复制代码
// 危险:三个 int 相乘,结果还是 int
int 总字节数 = 长 * 宽 * 高;
// 假设 2000 * 2000 * 600 = 24 亿,超过 int 上限 21.4 亿
// 结果:总字节数 = -18 亿(回绕成负数!)

// 然后校验:
if (总字节数 > 2GB上限) { 拒绝; }
// 负数 > 2GB → false → 没拦住 → 用负数去分配内存 → 崩

// 安全:先把第一个数转成 long,整个乘法就在 long 里算
long 总字节数 = (long)长 * 宽 * 高;
// 24 亿装得下 → 校验能正确拦住

语法详讲

  • int 乘法溢出是"静默"的:不会抛异常,直接回绕,所以特别难查。
  • 修法(long)长 * 宽 * 高 ------ 把第一个操作数转成 long,后面整条乘法链自动按 long 算。
  • long 范围:约 ±9.2 × 10¹⁸,装 2GB 绰绰有余。
  • 通用原则:做"乘法 + 上限校验"时,先想清楚"结果最大能到多少",会不会超过参与运算的类型上限。

3. MySQL 认证插件:为什么连不上库

概念

MySQL 8.0 默认用 caching_sha2_password 认证(取代旧的 mysql_native_password)。两者在「非加密连接」下的表现完全不同:

caching_sha2_password mysql_native_password
非加密连接下 必须用服务器的 RSA 公钥加密密码 SHA1 挑战应答,不需要公钥
关键参数 AllowPublicKeyRetrieval=True(同意取公钥)

类比

caching_sha2_password = 寄贵重快递必须上锁 。没有 TLS 押运车时,要用收货方的锁(RSA 公钥)锁密码;而这个锁默认不给你 (防中间人给假锁偷密码),必须显式 AllowPublicKeyRetrieval=True 才给。

常见报错链条

复制代码
连接串 SslMode=Preferred(能加密就加密,不能就裸奔)
  → 现场 TLS 证书坏 → 降级成非加密
    → caching_sha2_password 要公钥 → 默认不给 → 认证失败

语法/配置详讲

  • 两种修法 (殊途同归):
    • 服务器端ALTER USER ... mysql_native_password ------ 只对这一台库生效,每台要手动改。
    • 客户端 :连接串加 AllowPublicKeyRetrieval=True ------ 随软件走,所有机器都生效。
  • 注意mysql_native_password 在 MySQL 8.4+ 已移除,长期看客户端修法更稳。

4. git cherry-pick vs merge:挑单个提交 vs 合整条分支

概念

merge 整分支 cherry-pick
带进什么 对方分支所有提交(含别人的) 只挑指定的提交
结果 保留原提交,可能产生 merge 提交 创建新提交(新 hash),原提交不动
用途 合并整个功能线 只合「某个人」或「某几个」提交

类比

  • merge = 把别人整个文件夹搬过来(包括你不想要的文件)。
  • cherry-pick = 只从别人文件夹里抽几张纸抄过来。

一个容易踩的坑

git merge-base A B 只告诉你「A 和 B 的共同祖先 是谁」,不能 证明「B 是从 A 分出来的」。判断"谁从谁分出来"要看 first-parent 线:某分支第一个提交的父提交在不在另一分支的 first-parent 线上。


5. UTF-8 vs GBK:为什么日志中文变乱码

概念

  • UTF-8(无 BOM):很多程序默认的写文件编码。
  • GBK :中文 Windows 的默认系统编码
  • 关键:记事本/查看器遇到「无 BOM」文件时,只能靠猜 编码。中文 Windows 默认往 GBK 猜 → UTF-8 的中文被按 GBK 读 → 乱码

类比

UTF-8 和 GBK 是两套"密码本"。文件用 UTF-8 写,查看器却拿 GBK 密码本去"翻译",翻出来的中文就是天书。

排查关键

  • 乱码要「日志里有中文」才会显形------如果哪天日志第一次出现中文,乱码就冒出来了,容易误以为是"编码今天变了",其实编码一直没变。
  • 修法:写文件时带 BOMnew UTF8Encoding(true)),查看器看到 BOM 就知道是 UTF-8,不用猜。

6. 代码排查 4 条自检

排查"没报错但结果不对"、"偶发异常"这类问题时,按这 4 条过一遍:

  1. 「没报错但不正常」先查静默失败 ------ catch {} 空吞异常、default else 兜底、漏项、不落日志。
  2. 改一个值先 grep 所有分发点 ------ 枚举/JSON 键/类型 → UI 下拉、序列化、过滤、复制、模板,别只改一处漏一片。
  3. 异步完成标志在 await 成功后再置位 ------ 别在 await 之前就把"完成"标志设了。
  4. catch Exception 会吞掉取消异常 ------ 主动取消要先 catch (OperationCanceledException) 静默复位,否则取消永远不生效。

类比

这 4 条像开车前的检查清单:油箱(静默失败)、后视镜(分发点)、手刹(异步标志)、刹车灯(取消异常)------少查一项,路上就出事。


7. git 分支合并:把散在多分支的"某人的提交"汇到一起

概念

今天的主线工作其实是这个:一个人的改动散在好几个分支上,要汇到一个测试分支 先测,测过再合主线。核心两件事------只挑这个人的提交别把别人的改动带进来

只挑某人的提交:cherry-pick + 按作者过滤

bash 复制代码
# 1. 先列出「某人」在几个分支上、且还没进主线的提交
git log 分支A 分支B 分支C --author="某人" --not 主线 --no-merges

# 2. 按时间顺序逐个挑出来(会生成新提交、新 hash)
git cherry-pick <commit1> <commit2> <commit3>

为什么不能直接 merge 整个分支 :merge 会把分支上所有人的提交都带进来;只想带"某人"的,就得用 cherry-pick 一个个挑。

merge-base 只能证明"共同祖先",不能证明"谁从谁分出"

一个很容易踩的坑:看到两个分支的 merge-base 是某个提交,就以为"B 是从 A 分出来的"------

bash 复制代码
git merge-base A B   # 只返回"最近共同祖先",仅此而已

判断"B 是不是从 A 分出来的"要看 first-parent 线:B 的第一个提交的父提交,在不在 A 的 first-parent 线上。

bash 复制代码
git rev-list --first-parent A | grep <B的第一个提交的父提交>
# 有 → B 从 A 分出;没有 → 各自从上游分出,只是都带了这个共同祖先

合并时的取舍:跳过文档、处理"编号撞车"

  • 只合代码提交,跳过纯文档提交(尤其两边的文档编号撞车时)。
  • 「代码 + 文档」混在一个提交里:保留代码改动,文档部分取主线版,别让撞车的编号混进来。
  • 这类"文档编号撞车"(两边各自用了同一个编号、内容却不同)交给团队统一重排,不在测试分支上硬拼。

类比

  • 多个分支像不同抽屉,你只想要自己放进去的那几样 → cherry-pick 是"从抽屉里挑出你要的",merge 是"整个抽屉搬走"。
  • merge-base 是"两人共同的祖先照片",只能证明你们有关系,不能证明"谁是谁的爸爸";first-parent 线才是"户口本"。

8. 闭包捕获循环变量:lambda 里循环变量会"迟到"

概念

for 循环里写 lambda / 委托,如果直接捕获循环变量 i,等 lambda 真正执行时,i 已经变成循环结束后的值了,不是你写 lambda 那一刻的值。

类比

像在墙上贴便利贴:"记住第 3 个抽屉"------但你只写了"当前抽屉",没写死"3"。等你回头再看时,"当前抽屉"已经是最后一个了。

伪代码(错误 → 正确)

csharp 复制代码
// 错误:直接捕获 i
for (int i = 0; i < 路径.Count; i++)
{
    Dispatcher.BeginInvoke(() => 处理(路径, i));   // 执行时 i 已经是循环结束的值
}

// 正确:先用局部变量固定当前值
for (int i = 0; i < 路径.Count; i++)
{
    var idx = i;                                   // idx 是"当轮的快照"
    Dispatcher.BeginInvoke(() => 处理(路径, idx)); // 每轮各自捕获自己的 idx
}

语法详讲

  • C# 的 for 循环变量只有一个,每次迭代复用同一个变量 → lambda 捕获的是"那个变量",不是"那一刻的值"。
  • 修法:循环体内 var idx = i; 新建一个局部变量 ,每轮迭代都有独立的 idx,lambda 捕获各自的值。
  • 注意:foreach 循环变量在 C# 5 之后每轮是独立的(没有这个坑),for 才有

9. 执行动作前,先检查前置条件

概念

"没报错但不正常"的一种常见根源:该查的前置条件没查,盲目执行,等底层拒绝才报错

类比

开车前不检查油箱,踩油门车不动了才发现没油。应该"先看仪表盘,再点火"。

原则

  • 执行一个动作前,先检查前置条件(状态是否满足、资源是否就绪、输入是否合法)。
  • 满足才执行;不满足就阻断 + 友好提示,而不是盲目执行、等底层报错。
  • 这和"判断两个东西会不会冲突,看有没有全局查找"是同一套思路:先查清楚,再动手

10. 配置项驱动界面显隐:一个开关控制多个控件

概念

想「一个开关,控制界面上某几个按钮显不显示」,别写死注释,而是用一个配置项(bool) + 数据绑定------开关改配置项,界面自动跟着显隐。

类比

像电灯开关:拨一下开关(配置项),灯(按钮)亮/灭;把开关位置记下来(持久化),下次进屋还是上次的状态。

伪代码

csharp 复制代码
// 配置类
bool 显示某按钮 { get; set; } = false;   // 默认隐藏

// 界面:勾选框双向绑定配置项
勾选框.IsChecked = 双向绑定(显示某按钮)

// 界面:按钮显隐绑定配置项(bool → 可见/折叠)
按钮A.可见性 = bool转可见(显示某按钮)
按钮B.可见性 = bool转可见(显示某按钮)

// 视图模型:只读属性暴露给界面
bool 显示某按钮 => 配置.当前值.显示某按钮;
// 关闭设定弹窗后:通知"显示某按钮"变了 → 界面重新读、刷新显隐

语法/模式详讲

  • 双向绑定(TwoWay):勾选框改 → 配置项变;反过来配置项变 → 勾选框跟着变。
  • bool → 可见性 :用「布尔转可见性」转换器,true=可见、false=折叠。
  • 持久化:配置改动要写回配置文件(重启还记得)。常用「深拷贝 → 编辑 → 深拷贝回写 + 保存」。
  • 刷新时机:弹窗关闭、配置保存后,手动发一个「属性变更」通知,让界面重新读配置项------否则界面不会自动知道配置变了。

11. 声明 vs 定义:贴标签 vs 造实物

概念

声明 和 **new(定义/实例化)**是两回事,别以为「写了变量名」就等于「有了对象」。

类比

声明 = 在墙上钉一块「此处放灭火器」的牌子;new = 真的把灭火器挂上去。只有牌子、没有灭火器,出事时伸手就是空。

伪代码

csharp 复制代码
CancellationTokenSource 源;              // 声明:贴了个类型标签,此刻是 null,没有实物
源 = new CancellationTokenSource();       // 定义:真的 new 出对象,塞进这个标签

语法详讲

  • 声明 = 告诉编译器「有这个变量、是某某类型」,但还没造对象 (引用类型此刻是 null)。
  • new = 真正在堆上创建对象,赋给变量。
  • 常见坑:只声明没 new.Cancel(),会直接空引用崩溃。
相关推荐
ACP广源盛139246256732 小时前
Qwen3.8‑2.4T 开源落地@ACP#国产 Serdes 长距离视频传输芯片 GSV5800 在私有化 AI 服务中的价值与应用场景
大数据·数据库·人工智能·嵌入式硬件·矩阵·开源·音视频
oradh2 小时前
Oracle RMAN备份脚本、RMAN还原恢复测试、RMAN常用语句
数据库·oracle·rman备份脚本·rman还原恢复测试·rman常用语句
汽车仪器仪表相关领域2 小时前
ZDT‑I伺服电机测试系统:四象限动态加载
大数据·数据库·分布式·功能测试·汽车·压力测试·可用性测试
云贝教育-郑老师2 小时前
Oracle 块清除(Block Cleanout):commit 之后,数据块里的“战场“谁来打扫?
数据库·学习·oracle
gs801402 小时前
Java 响应式编程详解:从 Flux、Mono 到 Spring WebFlux 技术生态
数据库·oracle
一只旭宝2 小时前
预约系统版本2(pyhton+flask可视化版本)
服务器·数据库·c++·笔记·python·flask
__zRainy__2 小时前
Node系列 · ORM:mysql 驱动程序
数据库·后端·mysql·node.js·orm
程序员夏洛3 小时前
MySQL 中如果发生死锁应该如何解决?
数据库·mysql
肠畔码农3 小时前
Redis 深度内核解析与高性能运维调优指南
运维·数据库·redis