学 Swift 时,很多同学都会下意识写出这样的代码:
swift
let name = "Taylor"
print(name[3])
结果直接报错:「下标访问无效」。为什么数组可以 arr[3]
,而字符串就不行?这其实隐藏着一个很有意思、也很「人性化」的设计哲学。今天我们来彻底拆解一下。
🌟 数组为什么可以随便下标访问?
在 Swift(以及其他绝大多数语言)中,数组是由 大小相同、连续排列 的元素组成的。比如:
swift
let numbers = [10, 20, 30, 40]
print(numbers[2]) // 30
这个操作非常快,时间复杂度是 O(1)。为什么?
- 数组在内存中是连续的。
- 每个元素大小一样,比如 Int 都是 8 字节。
- 如果知道起始地址,访问第 n 个元素只需要简单计算:起始地址 + n × 元素大小,就能直接跳到目标位置。
这种访问方式被称为 随机访问(Random Access) ,对 CPU 来说非常高效。
🌈 那字符串呢?
字符串看起来好像也是「一堆字符」组成,为什么不能 str[3]
呢?
这里面有个巨大的坑:字符串中的「字符」并不都是一样大的!
✅ 字符和「扩展字形簇」
在 Swift 中,字符串遵循 Unicode 标准,强调「人类可见的字符」,也就是 扩展字形簇(Grapheme Cluster) 。
举几个例子:
🇺🇸
(美国国旗 emoji)并不是一个「单一字符」,它是由「区域指示符号字母 U」+「区域指示符号字母 S」组合而成。👨👩👧👦
(家庭 emoji)可能由 7 个左右的 Unicode 标量(包括多个 emoji 和零宽连接符)拼在一起。
从人类视角看,它们只是一个符号,但在底层,它们由多个小的「碎片」拼接。
🟠 方格纸思维实验
假设你在一张方格纸上写字符串,每个格子只放一个字母,数组的情况就是这样:
| H | e | l | l | o |
这时候找第 4 个字母非常简单:直接数格子,或者直接按「每页 50 个格子」算偏移。
但如果每个字母占的格子数不一样,比如 emoji 需要 4 个格子拼起来组成,你就无法直接跳到第 n 个「人类字符」了,你需要从头开始,逐个数每个完整字符有多少格,直到找到你要的第 n 个。
这就是 Swift 字符串的本质。
💥 为什么不让写 myString[3]
?
Swift 团队很「严谨」,不想给你提供一个看似简单但暗藏性能陷阱的写法。
如果允许 myString[3]
,你会以为它和数组一样是 O(1),但实际上它需要从开头扫描到第 3 个「可见字符」,时间复杂度是 O(n)。这会导致很多性能 Bug 和错误预期。
✅ 正确写法
在 Swift 中,应该使用 String.Index
:
swift
let greeting = "👨👩👧👦Hello🇺🇸"
let index = greeting.index(greeting.startIndex, offsetBy: 3)
print(greeting[index])
这里,index(_:offsetBy:)
就是一步一步数「人类可见字符」的工具,明确告诉你这个操作是线性扫描。
⚖️ 数组 vs 字符串访问方式对比
数组 | 字符串(Swift) | |
---|---|---|
内存布局 | 元素大小固定 | 字符大小可变 |
下标访问 | O(1) 随机访问 | O(n) 顺序扫描 |
写法 | arr[3] | index + offset |
💡 关于 .isEmpty
和 .count
小知识点补充一下:
arduino
if myString.isEmpty {
// 推荐写法,只检查有没有第一个字符,性能好
}
if myString.count == 0 {
// 不推荐写法,会遍历所有字符,性能差
}
.isEmpty
只需要判断是否有第一个字符,而 .count
会统计完整个字符串里的所有字符(包括组合字符),耗时更高。
Swift 字符串的设计,不是「不能」,而是「不让你误用」。
⚡ 要支持所有人类可见字符(emoji、组合字符),就必须安全、正确地逐步数;要快速随机访问,就用数组。