说实话,字符串这章是我见过最容易被低估的内容。不少开发者觉得字符串函数就是查查手册背背 API,可真到了项目里,绕来绕去翻车的全是字符串。前几天我刷到一个热搜问题:博途里字符串的值已经置成 0 了,触摸屏上却还显示原来的字符。评论区一堆人猜来猜去,但真正的原因其实藏在字符串的内存结构里。再翻热搜里的“C 语言字符串逆序”“SQLServer 字符串转数字”“Java 字符串转 JSON”这些提问,你会发现大家对字符串的理解是碎片化的,只知道某个函数怎么调用,却不知道这个函数背后做了什么。
这篇文章我打算换个讲法,不讲教科书上的函数列表,而是从“字符串到底是什么”出发,把热搜里那些高频问题拆开揉碎。你写代码时会踩的坑,面试时会被问的原理,调接口时碰到的编码乱码,基本都逃不出这几个主题。适合刚入门的初学者,也适合写了好几年业务代码、想回头补基础的朋友。
1. 字符串的本质:先把“字符”和“字节”这两件事掰扯清楚
1.1 为什么几乎所有字符串问题都出在“编码”上
字符串在高层的抽象里是一串字符,但在计算机内存里,一切都只是字节。字符和字节之间靠编码规则互相转换。最常见的编码是 ASCII、UTF-8、UTF-16、GBK 这几种,它们把“字”翻译成“字节”的方式完全不同。
这里有一个非常经典的认知盲区:一个英文字母在几乎所有编码里都占 1 个字节,但一个汉字在 UTF-8 下占 3 个字节,在 GBK 下占 2 个字节,在 UTF-16 下可能占 2 或 4 个字节。所以“字符串长度”这个看似最简单的概念,在不同语言、不同编码下会有不同答案。
Python 里的 len("你好") 返回 2,因为 Python 的字符串对象存储的是 Unicode 字符;Java 里 "你好".length() 返回 2;但如果你取 "你好".getBytes("UTF-8").length,得到的是 6。C 语言的 strlen("你好") 的结果取决于源文件编码和运行环境,如果是 UTF-8 编码且终端按 UTF-8 输出,得到的很可能是 6。不是任何一个函数算错了,而是它们衡量的对象本身就不一样。
热搜词里有一个非常典型的问题:python十六进制和字符串互转。很多人第一次接触这个概念时一头雾水:字符串就是字符串,十六进制不就是数字吗?其实这里面隐藏的同样是“字符到字节”的映射问题。一个字符串“Hello”如果按 UTF-8 编码成字节,就是 48 65 6C 6C 6F,这串十六进制就是它内存中的真正形态。反过来,拿到十六进制字节流,按指定编码解码,才能还原成可读的字符串。
python复制# 字符串转十六进制
"Hello".encode("utf-8").hex()
# 输出:48656c6c6f
# 十六进制转字符串
bytes.fromhex("48656c6c6f").decode("utf-8")
# 输出:Hello
我见过太多项目里的乱码问题,最后定位下来都是编码不一致。上游接口用 GBK 编码发过来,下游消费者按 UTF-8 解析,中文就变成一堆问号或乱码。处理这类问题的第一原则不是到处转码,而是先明确数据链路每一环的编码,再在系统边界处统一转码。在 2024 年的今天,新项目一律 UTF-8,旧系统对接才需要额外处理 GBK 这类兼容问题。
还有热搜里的 urldecoder.decode处理有%的字符串,这属于 URL 编码范畴。URL 里的 %20 表示空格、%E4%B8%AD 是“中”字的 UTF-8 编码。URLDecoder.decode 做的事就是把这种百分号编码还原成普通字符串。很多人在处理带 % 的原始数据时踩坑,是因为没搞清楚:数据里的 % 到底是 URL 编码产生的,还是原本就有的字符。如果你只是单纯想把一个已经编码的 URL 还原,直接 decode 没问题;如果数据里本身就有字面意义上的 %,就要先转义成 %25,否则解出来的内容和你期望的会差很远。
1.2 不可变与可变:一道所有语言都绕不开的分岔口
字符串在编程语言里有两种截然不同的设计:不可变与可变。
Java、Python、C#、JavaScript、Lua 这些语言的字符串默认不可变。你看起来“修改”了一个字符串变量,实际上是在内存里创建了一个新的字符串对象,然后把变量指向了新的对象。而 C、C++、Go 的字符串底层是字节数组,可以在原地址上改内容。
不可变设计带来几个实际好处:线程安全,多个线程共享同一个字符串实例不会互相干扰;哈希值可以缓存,同一个字符串只在第一次计算哈希,后面直接用;安全性更好,不会因为一个引用的修改污染其他引用。但代价也很明显——频繁拼接字符串时会产生大量中间对象,影响性能。
可变字符串适合需要就地修改的场景,但会引入别名问题。两个变量指向同一块内存,你在一个变量上改了内容,另一个变量看到的内容也跟着变了。C 语言里传指针给函数,函数里 strcpy 一下,外部变量的内容就悄无声息地被覆盖,这是无数 C 语言新手崩溃的根源。
热搜里有两个问题正好对应这个话题。一个是 lua 字符串如何改变其中某个字符的值,Lua 的字符串是不可变对象,你不能像 C 语言那样直接 s[2] = 'x'。惯用做法是用 gsub 或手工构造新字符串:
lua复制local s = "hello"
-- 把第 2 个字符(从 1 开始计数)改成 'a'
s = s:sub(1, 1) .. "a" .. s:sub(3)
另一个是 c语言字符串拼接。C 语言里字符串本质是字符数组,拼接要自己保证目标缓冲区足够大,否则就是缓冲区溢出。我早年写 C 的时候,因为 strcat 目标数组开小了,程序运行时直接崩溃,排查了很久才发现是字符串越界覆盖了其他变量。这些都是“字符串底层到底是什么”没搞清楚的代价。
理解不可变与可变的区别,看代码的时候会通透很多。看到 Java 里 String s = s + "x",你脑子里应该出现一幅图:原来的字符串还躺在内存的某个角落,一个新的字符串被创建出来,引用被重新指向。看到 C 代码里 char buf[100]; strcpy(buf, src);,你脑子里应该出现一幅图:一块固定大小的内存区域,拷贝进去的字节数一旦超过 99,就会越过边界。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 字符串核心操作的底层逻辑:查找、截取、拼接、转换到底在干什么
2.1 查找与比较:为什么“相等”没那么简单
字符串比较是热搜高发区,字符串比较是否相等、js判断字符串是否包含、sqlserver 字符串包含判断方法,这些问题的共同根源是没有搞清楚“引用的相等”和“内容的相等”是两回事。
Java 里用 == 比较两个 String 变量,比较的是引用地址,不是内容。两个字符串内容完全相同,但它们是不同对象时,== 返回 false,必须用 equals。Python 的 == 比较的是内容,is 才比较对象身份。C 语言没有字符串类型,strcmp 返回 0 表示相等,很多人第一次写 C 时直接用 if (s == "abc"),结果永远不成立,因为那是在比较指针地址。JavaScript 的 == 在比较字符串和数字时还会做隐式类型转换,又是一层坑。
字符串排序也是同样的道理。不同语言默认排序规则不一样,C 语言的 strcmp 按字符的二进制值逐字节比较,大写字母排在前面,小写字母排在后面,数字排得更靠前。而带语言环境的排序,比如很多业务里的中文排序,规则完全不同。字符串排序这个话题看起来简单,但一旦涉及多语言、大小写混合,排序结果就会出乎意料。
查找函数家族同样值得留意。Java 的 indexOf、JavaScript 的 indexOf/includes、C 语言的 strstr、SQL Server 的 CHARINDEX,它们底层做的大多是单模式字符串匹配。最简单的实现是暴力匹配,从头到尾逐个比较;高级一点的会用 KMP、BM 这类算法。但实际工程中,对大多数短字符串来说,暴力匹配就是最快最可靠的方案,因为算法本身的常数开销很小,KMP 的预处理反而显得多余。真正需要上高级算法的场景,是海量文本搜索或需要稳定性能的大数据处理。
正则表达式是另一类查找工具,它能表达复杂的匹配模式,但代价是编译和执行的开销远高于普通查找。热搜里有一个问题问得特别好:java中使用指定字符分隔字符串是用正则效率高还是自己写的效率高。先给结论:纯字符分隔,自己遍历的效率远超正则。Java 的 String.split 内部就是按正则表达式解析的,哪怕你传的参数是 ",",它也会走一遍正则编译的逻辑。一个 10 万行的文件按逗号分割,正则版本比手写的 indexOf + substring 循环慢数倍。但这不意味着正则没用,当规则复杂到需要写判断逻辑时,手写代码量巨大、可读性很差,这时候正则带来的开发效率提升远大于那点性能开销。
2.2 截取与拼接:每一次操作背后都有隐形成本
字符串截取是另一个高频操作。C 语言没有原生的 substring,要么自己做内存拷贝,要么用指针指向原字符串中间位置。Java 的 substring 在 JDK 7 之后是复制一份新字符串,复杂度 O(n);但 JDK 6 之前它共享底层数组,只调整了起始偏移和长度,O(1),不过也因此引发了一些内存泄漏问题。C++17 引入的 string_view 是零拷贝的视图,但它不拥有内存,原始字符串被销毁后视图就悬空了,用不好就是悬垂指针。
Python 的切片 s[1:3] 也是创建新字符串。看起来只是取了一段,但如果对大字符串反复切片且只保留其中一小部分,底层有可能把整个大对象一直引用着,造成内存回收不掉。这是 Python 的一个经典坑,解决方案是用 str(slice_result) 强制生成独立的副本。
拼接操作的性能问题我在第四节展开讲,这里先点一个结论:循环里用 + 拼接,时间复杂度是 O(n²),数据量大时肉眼可见地变慢。正确做法是用 StringBuilder(Java/C#)、join(Python)、strings.Builder(Go)这类容器。
热搜里还有几个典型问题,本质也和截取拼接相关。c#语言怎样截取字符串,C# 里 Substring(startIndex, length) 用起来还算顺手,但要注意第二个参数是长度而不是结束位置,很多人按 Python 的习惯传结束位置,直接踩坑。两个文本框里的字符串连接在一起怎么弄,这种需求的关键不是拼接本身,而是处理两个文本框可能为空、可能包含空格、可能在拼接时需要加分隔符这些边界细节。拼 SQL、拼文件路径、拼展示文案,规则各不同。
字符串逆序和递归法将一个整数n转换成字符串这两个热搜,我放在一起说。字符串逆序的经典解法是双指针原地交换,时间复杂度 O(n),空间复杂度 O(1):
c复制void reverse(char s[]) {
int i = 0;
int j = strlen(s) - 1;
while (i < j) {
char t = s[i];
s[i] = s[j];
s[j] = t;
i++;
j--;
}
}
递归法把整数转成字符串的思路是:每次除以 10,先递归处理商,再追加余数对应的字符。这个解法用来练递归思维很好,但在生产环境里要小心两个问题。一是大整数递归调用会消耗栈空间,虽然整数转字符串的递归深度不会超过 20 层,基本无风险;二是负数的最小值 INT_MIN 取负数会溢出,这是个非常经典的隐藏 bug。-2147483648 对 INT_MIN 做 -n 还是负数,直接导致结果错误,处理负数的逻辑必须单独考虑。
2.3 字符串与数字互转:看似无脑,坑比你想的多
字符串转数字这个话题在热搜里出现了至少三个变体:sqlserver 字符串转数字、python十六进制和字符串互转、java 字符串转json jackson。足以说明它是日常开发的高频痛点。
先看最简单的情况:把 "123" 转成数字 123。几乎所有语言都提供了现成函数,C 的 atoi、Java 的 Integer.parseInt、Python 的 int()、SQL Server 的 CAST/CONVERT。但边界情况才是真正区分水平的地方:
- 空字符串转数字:
int("")直接报错,Integer.parseInt("")抛异常,atoi("")返回 0,SQL Server 的CAST('' AS INT)直接报错。同一个操作,五种表现。 - 带空格的字符串:
int(" 123 ")在 Python 能转,parseInt(" 123 ")在 Java 也能转,但Integer.parseInt("12 3")不行。 - 全角数字、中文数字:直接转基本都会失败,需要先归一化。
- 溢出:
Integer.parseInt("99999999999")抛异常,int("99999999999")在 Python 里没问题(Python 的整数不限长度),但在 C 语言的atoi里行为是未定义的。 - 进制:
int("ff", 16)是 255,但int("ff")报错。
SQL Server 里的字符串转数字还有一个特殊问题:字符串包含非数字字符时,CAST 和 CONVERT 会直接报错,完全没有容错空间。业务里常见做法是先用 ISNUMERIC 判断,但这个函数有不少坑,比如它会把 '1e3'、'$100' 这类内容也判定为数字。SQL Server 2012 之后推出的 TRY_CAST 和 TRY_CONVERT 更可靠:转换失败时返回 NULL,而不是抛异常。这一点在实际开发中能省不少事。
Java 的字符串转 JSON 是另一类转换。字符串 {"name":"test"} 本质是 JSON 文本,需要反序列化成 Java 对象。用 Jackson 的 ObjectMapper.readValue 是最常见的做法。这里踩坑最多的场景是:JSON 字符串里的字段名和 Java 对象属性名大小写不一致、数字类型精度丢失(JSON 里的长整型到 Java 的 int 会溢出)、嵌套对象解析失败。处理原则是:对象结构要预先定义清楚,字段名用注解对齐,长整型统一用 Long 或字符串接收。
3. 从热搜问题看真实工程场景:几个值得复盘的具体案例
3.1 触摸屏明明读到字符串值是 0,为什么还显示旧字符
开篇提到的问题,这里展开讲。博途(TIA Portal)是西门子 PLC 的组态编程软件,触摸屏和 PLC 之间的字符串数据交互,底层机制和 C 语言里的字符数组很类似。在 PLC 的 DB 数据块里,字符串通常不是只存内容,而是先存一个长度信息,再存字符数据,整个区域是定长预留的。比如定义了一个“STRING[20]”,它占用的实际内存是 22 个字节,第 1 和第 2 字节是最大长度和当前长度,后续 20 个字节才是字符内容。
问题就出在这:当你把触摸屏上显示的字符串“赋值成 0”,这个 0 很可能只是写入了当前长度字节(或者写入了第 1 个字符的位置),后面 20 个字节里的旧字符数据依然原封不动地躺在内存里。触摸屏侧如果按照固定的字符串区域去读取,或者它的显示缓存没有刷新,就会继续展示旧字符。
这就可以解释为什么你“明明把值改成了 0”,屏上显示的却还是原来的内容。解决方案有几种:把整个字符串区域全部置零,再写入新字符串;或者在 PLC 侧写一个专门处理字符串复位的逻辑块,把长度字段和字符数组一并清空;再检查触摸屏侧是否启用了字符串变量缓存,有的话要关闭或设置合理的刷新周期。这类“值变了但界面不变”的问题,背后的通用教训是:字符串是个复合结构,你不能默认给它赋 0 就等于把内容清零了。这一条放到嵌入式开发、串口通信协议解析这些场景里同样适用。
3.2 当字符串成为字典的 Key:哈希、驻留与不可变性
delphi 字符串作字典key这个热搜很有代表性。字符串作为字典(哈希表)的 key,涉及到三件事:哈希计算、相等比较、驻留池。
哈希表靠 key 的哈希值定位存储位置,然后用 key 的相等比较确认命中。所以字符串 key 的类型必须同时保证两件事:哈希值稳定、相等语义清晰。这也正是不可变字符串更适合做 key 的原因——如果字符串可变,哈希值会在存进字典之后变化,那整个哈希表就散架了。
Java 里 String 对象会缓存第一次计算出来的哈希值,同一个字符串对象后续再取哈希是 O(1)。但要注意不同字符串对象内容相同时,哈希值一样,这不影响正确性,只是它们依然可能被当作两个不同的 key 存进字典(取决于 equals 实现)。字符串驻留池则是另一回事,Java 里字面量字符串会被放进常量池,相同内容的字面量指向同一个对象,因此 "abc" == "abc" 在大多数情况下是 true。但通过 new String("abc") 创建的对象就不在池子里,就会打破这个相等关系。这就是为什么我一直强调:业务代码里比较字符串内容,永远用 equals 或语言对应的内容比较方法,不要赌 ==。
Delphi 的字符串是引用计数和写时复制的设计,字符串变量赋值并不复制内容,而是增加引用计数。一旦通过某个引用修改字符串内容,系统才会真的复制一份再改。这种设计在很多老旧的 Delphi 代码里埋过一个坑:你把字符串作参数传进 DLL,如果 DLL 那侧没有用 Delphi 的字符串类型来接收,而是当成普通指针数组处理,那么修改内容时可能没走写时复制机制,导致主程序的字符串被意外修改。这本质上是跨模块传递字符串时,必须保持同一个运行时对字符串内存布局的理解。
C++ 的 std::string 作为 std::unordered_map 的 key 时,每次查找都会涉及字符串的拷贝和哈希计算。用 std::string_view 作 key 可以避免拷贝,但要注意生命周期。这些细节在 C++ 这种没有垃圾回收的语言里尤其重要,一旦 key 引用的底层内存被释放,哈希表再访问就是未定义行为。
3.3 想要“就地修改”一个字符串?先看你的语言同不同意
热搜词里 lua 字符串如何改变其中某个字符的值 和 c语言字符串拼接 放在一起看特别有意思,它们代表了两种完全相反的字符串观。Lua 的字符串是不可变的,修改任何字符都必须生成新字符串;C 语言的字符串是内存区域的别名,可以就地改,但改之前你必须确认这块区域可写、空间足够、边界不会溢出。
许多从 C 转到高级语言的开发者,最容易犯的错就是把 C 的字符串思维带过来,试图“找到那个字符然后改掉它”。Java、Python、C#、JavaScript 里都没有这种就地修改的通用 API。Java 里有 StringBuilder,本质上是一个可变的字符容器,但对调用方来说它已经不是普通的字符串了。Python 想要修改字符串的第 N 个字符,常规手段是切片重组或者 list(s) 转字符列表改完再 join 回来。JavaScript 的新提案 Array.prototype.toSpliced 也不是为字符串设计的,常用做法同样是组合 slice。
这个问题的背后是“值语义”和“引用语义”的区别。不可变字符串是值语义,你拿到的是一个固定的值,所有操作都是产生新值;C 语言字符数组是引用语义,你拿到的是对一块内存的访问权,改不改、怎么改,由你负责。理解了这个区别,你在不同语言间切换写字符串代码时就不会总犯“为什么这语言没有那个函数”的困惑。
4. 字符串性能与安全:正则、拼接和注入,这些坑我替你踩过了
4.1 正则表达式与手写解析的效率之争
前面提到过热搜里的正则效率问题,这里完整展开。需要先建立两个概念:正则表达式的编译开销和执行开销。
编译开销:正则字符串要先被解析成内部结构,才能执行匹配。如果一段正则只用一次,编译开销完全摊在实际执行里;如果在一个循环里反复用同一段正则,每次调用都重新编译,性能就很差。很多语言为正则提供了“预编译”机制,Java 里是 Pattern.compile 得到 Pattern 对象再复用它,Python 里是 re.compile。
执行开销:正则在执行匹配时,引擎会在字符序列上做状态机遍历。复杂的回溯型正则(比如嵌套量词)在最坏情况下甚至可能出现灾难性回溯,导致匹配耗时暴涨。(a+)+$ 这类正则在匹配长字符串时是经典的反例。
那么“指定字符分割”到底该用正则还是手写?我的经验是:
- 固定分隔符、规则简单:手写。Java 用
indexOf+substring,Python 用str.split(它本身走的是 C 层实现,性能很好),SQL Server 可以用CHARINDEX+SUBSTRING组合。 - 分隔符是多个可能字符,或者需要忽略大小写、限定长度、分组捕获:正则更省事。
- 数据量在百万行以上且分割逻辑固定:先用
split,性能不够再用流式手写。
我还想特别提醒一点:Java 的 String.split 接受的是正则表达式,这意味着字符串里的 |、.、*、+、? 等字符都有特殊含义。用 "a.b".split(".") 得到的不是两个元素,而是空数组,因为 . 匹配任意字符。要么写成 split("\\."),要么用 Pattern.quote(".") 把字面量转义。这类问题是生产环境里出现频率极高的隐性 bug,排查起来很费神。
4.2 拼接的 O(n²) 陷阱:什么时候真的需要 StringBuilder
来看一段 Java 代码:
java复制String s = "";
for (int i = 0; i < 100000; i++) {
s += i;
}
这段代码的时间复杂度是 O(n²)。为什么?因为字符串不可变,每次 += 都会创建新的字符串对象,把旧内容完整复制一遍再追加新内容。第一次拼接复制 1 个字符,第二次复制 2 个字符……第一百万次复制 100 万个字符,累计就是约 5000 亿次字符复制。
换成 StringBuilder 后,底层的 char[] 可以动态扩容,追加操作在数组尾部进行,均摊时间复杂度是 O(1) 的,整个循环降到 O(n)。
但优化要有度。如果你的字符串只有 3 个片段,比如把姓、名、后缀拼在一起,用 + 和 StringBuilder 的性能差异完全可以忽略,此时代码可读性更重要。真正要用 StringBuilder 的是循环拼接、大文本组装、批量拼接 SQL 条件这些场景。
Python 里没有 StringBuilder,推荐用 "".join(parts),它在底层一次性分配所需空间并完成拷贝,比循环 += 高效得多。这里有个使用误区:很多人喜欢写 s += str(i) 然后再 s = s + "|",结果每次都生成新对象。更高效的做法是把所有片段收集到列表,最后一次性 join。C# 里的 StringBuilder 和 Java 的几乎同构。Go 则用 strings.Builder。规则总结成一句话:大量拼接用容器类,少量拼接怎么方便怎么写。
顺带一提,热搜里的 字符串加密压缩体积 也是字符串性能相关的话题。常见做法是用 gzip 等算法压缩字符串字节流,再转成 Base64 编码以便文本传输。但要注意两个坑:一是压缩算法对短字符串几乎无效,甚至可能因为压缩格式头导致体积变大;二是压缩后的二进制再转 Base64,体积会比原二进制增加约 33%。如果原字符串本身是文本且有大量重复,压缩效果才明显。JSON 这类结构化的数据往往有很高的重复度,压缩效果通常还不错。
4.3 从字符串拼接看安全边界:转义、注入与外部输入
字符串拼接最危险的地方,不是性能,是安全。当你要把用户输入的内容拼接到某个会被解释的上下文里——SQL 语句、shell 命令、HTML 页面、日志——就会引入注入风险。
最典型的例子是 SQL 注入:SELECT * FROM users WHERE name = ' + 用户输入 + '。如果用户输入是 ' OR '1'='1,拼接出来的 SQL 就变成了 WHERE name = '' OR '1'='1',条件恒真,整表被查出来。防御方式不是“过滤输入”,而是用参数化查询,让数据库把用户输入当作数据而不是 SQL 代码来处理。这也是所有安全专家反复强调的一点:不要把外部输入直接拼进 SQL。
HTML 拼接也有类似的 XSS 风险。用户提交 <script>alert(1)</script>,如果直接拼进页面,浏览器会把它当作代码执行。正确做法是 HTML 转义:把 < 转成 <、> 转成 >、& 转成 &。现在的现代前端框架默认会做转义,但用 v-html、dangerouslySetInnerHTML 之类的接口时,就绕过了框架的保护,需要自己承担转义责任。
日志注入相对隐蔽。日志系统通常按行分割,如果用户输入里包含换行符,攻击者可以伪造日志条目、干扰排查。处理方式是限制日志字段里不能出现换行,或不打印原始用户输入。
还有一类隐藏字符的安全问题。Unicode 里有一种零宽字符,肉眼看不到,但会在字符串中存在。如果一段文本里被插入了零宽字符,展示时根本看不出来,但字符串的实际内容和预期的并不一样。这种技术经常被用于隐藏恶意内容或绕过文本审核。在开发中,对用户输入做校验时,最好明确“哪些字符允许”,而不是“哪些字符禁止”,白名单永远比黑名单安全。
字符串转义和编码是这一节的核心:在哪个上下文里使用数据,就用哪个上下文的规则对特殊字符进行转义。写 SQL 有 SQL 的转义规则,写 HTML 有 HTML 的转义规则,拼 JSON 有 JSON 的转义规则。通用原则是“内容与代码分离”,不要把内容当成代码指令执行。
5. 动手实践:一个字符串处理自查清单
写到这里,我把多年的实际经验浓缩成一张自查清单。每次你处理字符串时,从高到低过一遍这张表,能少踩很多坑。
| 检查项 | 常见误区 | 正确做法 |
|---|---|---|
| 编码是否明确 | 拿到字节就直接转字符串 | 先确认来源编码,再在系统入口统一转成 UTF-8 |
| 比较的是内容还是引用 | Java 用 == 比较 String |
一律用 equals / 语言对应的内容比较方法 |
| 拼接是否在循环里 | 循环用 + 拼接 |
大数据量改用 StringBuilder / join / strings.Builder |
| 截取是否产生隐藏的大对象 | Python 切片保留原大对象 | 对小片段做一次显式副本 |
| 外部输入是否直接拼接 | 用户输入直接进 SQL / HTML / shell | 参数化查询 + 上下文转义 |
| 字符串转数字是否处理边界 | 不处理空串、溢出、非法字符 | 用 TRY_CONVERT / tryParse 等带容错函数 |
| 分割时是否考虑正则特殊字符 | Java split(".") 得到空数组 |
字面量用 Pattern.quote 转义 |
| 字典 key 是否可哈希且不可变 | 用可变对象作 key | 优先用不可变字符串,注意大小写规范统一 |
| PLC / 串口字符串是否启动了长度字段 | 只改字符串内容,忘了长度 | 同步更新长度字段和数据区,必要时整体清零 |
列完清单,再分享一个我踩过多次后的个人习惯。我要求自己写字符串相关代码时,先在注释里写清楚这段字符串会流经哪些环节、每个环节的编码是什么、在哪里被解析。不是为了写注释而写注释,而是逼自己在动手前想清楚数据流。很多看似莫名其妙的字符串 bug,最后定位下来都是缺少这一层全局视角。
还有一个小技巧:调试字符串问题时,别盯着函数调来调去,先把字符串对象用十六进制 dump 出来看一眼。你会发现好多“看起来一样”的字符串根本不一样,一个带 BOM 头,一个带零宽字符,一个末尾多一个不可见空格,人类的眼睛是看不出来的,但程序按字节处理,差一个字节就差之千里。这就是字符串这门课的核心:它看起来是一串字符,实际上是一堆严格的字节。
