Swift中字符串修改后String.Index有效性的判断规则咨询
你提的这个问题确实戳中了Swift字符串索引的一个容易让人困惑的点——毕竟它和数组索引的行为差异挺大的,咱们一步步来拆解这个问题:
首先明确:String.Index和数组索引的本质差异
数组的索引是基于元素数量的整数偏移,它的有效性只和数组的元素个数强相关——只要数组元素数大于索引值,索引就是有效的,而且只要不删除元素,原有效索引不会失效。
但String.Index完全不同:它是绑定到特定字符串内部编码结构的位置标记,指向的是字符串底层UTF-8/UTF-16/UTF-32编码单元的具体位置,而不是字符的偏移量。因为Swift的字符串是Unicode兼容的,不同字符可能占用不同数量的编码单元,所以字符串的修改哪怕只是追加、替换,都可能彻底改变内部的编码结构,导致原索引指向的位置不再对应有效的字符边界。
关于你遇到的两个反例
1. 字符串拼接后索引失效
var s = "abc" let i = s.index(after: s.startIndex) print(s[i]) // b s = "字" + s print(s) // 字abc print(s[i]) // \255 乱码 print(s.indices.contains(i)) // false
这个情况很好理解:拼接后的字符串底层编码结构完全变了,原索引i指向的是原"abc"的第二个字符位置,但新字符串的起始是占用多字节的"字",原索引已经不在新字符串的有效索引范围内,所以访问会得到乱码,contains也返回false。
2. 替换子范围后indices.contains返回true但索引无效
var s = "abc" let i = s.index(after: s.startIndex) print(s[i]) // b s.replaceSubrange(i...i, with: "字") print(s) // a字c print(s[i]) // \345 乱码 print(s.indices.contains(i)) // true
这个情况更隐蔽:你替换了i位置的单字节字符"b"为多字节的"字",原索引i现在指向的是"字"这个多字节字符的某个中间编码单元。虽然i仍然在新字符串的索引范围内(因为新字符串的索引范围覆盖了所有编码单元的位置),但它并没有指向一个有效的字符边界,所以访问时会得到乱码。这说明s.indices.contains(i)只能判断索引是否在字符串的编码单元范围内,不能保证它指向有效的字符起始位置。
有没有类似数组的不变量?
遗憾的是,几乎没有通用的、可靠的不变量能让你判断原索引在修改后是否有效。哪怕只是在字符串末尾追加字符,都可能因为内部存储重新分配、编码结构变化导致原索引失效——比如追加一个多字节字符,可能会改变整个字符串的编码单元布局。
唯一极端场景下可能相对安全的是:只在字符串末尾追加单字节ASCII字符,且字符串原本全是ASCII字符。但这种情况也不绝对,因为Swift字符串的内部存储实现可能会有优化或变化,所以绝对不要依赖这个假设。
实践中的安全做法
- 绝不复用修改前的String.Index:任何字符串修改(插入、删除、替换、拼接)后,原来的索引都应该被丢弃,重新计算你需要的位置。
- 重新计算索引的方式:如果需要跟踪某个逻辑位置,比如原字符串的第2个字符,可以先记录它相对于起始位置的偏移量:
这种方式虽然需要重新计算,但能保证索引的有效性。let offset = s.distance(from: s.startIndex, to: i) // 修改字符串后 if let newIndex = s.index(s.startIndex, offsetBy: offset, limitedBy: s.endIndex) { // 使用newIndex } else { // 处理越界情况 } - 避免用
indices.contains判断索引有效性:如你所见,这个方法无法确保索引指向有效的字符边界,只能判断是否在编码单元范围内。
内容来源于stack exchange

