Swift 字符串

# String 定义

官方定义 String 就是一组字符的集合。

和 Objective-C 不同, Swift 中的 String 是值类型,并且我们通过下标去访问字符串某个字符的时候,Swift 中下标的类型是 String.Index,而不是 Int..

# 下标为何是 String.Index 类型?

String.Index 的定义是「A position of a character or code unit in a string」。这句话中的 code unit 通常指的是构成字符串的最小单位,即 String.Index 表示字符串中字符的位置。

我们可以通过 String.Index 作为集合下标去获取字符串中某个字符,代码如下

var str:String = "hello world!"
print(str[str.startIndex])   //str.startIndex 表示字符串中首个字符的位置 
/// 终端输出 h

回到标题中,为何是 String.Index 类型作为 Swift 中字符串的下标呢?

这里需要引出 Unicode 的概念。

# Unicode

Unicode,本质是一个标准(The Unicode Standard),它整理、编码了世界上大部分的文字系统,使得电脑能以通用划一的字符集来处理和显示文字,不但减轻在不同编码系统间切换和转换的困扰,更提供了一种跨平台的乱码问题解决方案。

Swift 是 Unicode 兼容的,这意味着 Swift 语言中的字符串可以表示世界上大部分文字系统中的文字符号。

Swift 的原生 String 类型是由 Unicode Scalar(Unicode标量值)构建的。Unicode Scalar 是一个用于字符或修饰符的独特 21 位数字,例如 U+0061 代表 LATIN SMALL LETTER A ("a") 或者 U+1F425 代表 FRONT-FACING BABY CHICK ("🐥")

注意:这里的 Unicode 标量值构建并不表示 Swift 中的字符串的存储方式

关于 Unicode 标量值和码点的区别,可以看看官方术语表 (opens new window)中两者的定义。

我们可以直接遍历字符串中字符的 Unicode 标量值,代码如下

let flowers = "S笑😁"
for v in flowers.unicodeScalars {
    print(v.value)
}
//83 31505 128513 

代码打印的都是十进制,转换为对应的十六进制,分别是U+0053, U+7B11, U+1F601,通过查询 (opens new window)确实是上面三个字符,我们也可以验证一下

let str: String = "\u{53}\u{7B11}\u{1F601}"
print(str) 
//S笑😁

说到这里还是没有解决我们的疑问,在上面的字符中我们似乎仍然可以通过整数下标的方式来获取字符串中某个指定的字符,即单个的 Unicode 标量值。所以我们继续看看什么场景下,通过整型下标没办法获取给定的字符串。

在 Unicode 编码中,一个可阅读字符可能由多个 Unicode 标量值组合而成,比如扩展图素簇(Extended Grapheme Cluster)。

举个实际使用的例子,比如 é,它可以用唯一的 Unicode 标量值 U+00E9 表示;同时它也可以用两个 Unicode 标量值 U+0065, U+0301 表示,而且尽管是两个 Unicode 标量表示的字符,我们获取其长度仍然为 1,也很好理解,因为人们肉眼看到的就是一个字符。

let ch: String = "é"  //双unicode标量值表示
for scalar in ch.unicodeScalars {
    print(scalar.value) 
}
print(ch.count)
//101 769 1

像这样的字符如果出现在我们的字符串里,如果想要整型下标获取字符的话就会比较麻烦,多说一句,数组定位元素的本质方式就是通过数组起始内存地址+下标偏移量,这就要求数组中的元素占用的空间是一致的。

跑题一下,在 Swift 中,我们经常看到不同的数据类型可以遵守相同的协议,然后我们可以创建一个给定协议类型的数组,我们也可以通过下标去定位协议数组中的某个元素。这时候 Swift 语言是如何保证协议类型数组中的元素类型大小一致呢?答案是 Swift 语言为协议单独进行了内存空间布局,保证了协议类型元素的大小是一样的,参考这里 (opens new window)

在当下字符串这种存储方式里,因为不同的字符占据的内存是不同的,所以需要通过新的下标方式去获取给定位置的字符。

这也就是为什么使用 String.Index 去获取下标的原因。

different characters can require different amounts of memory to store, so in order to determine which Character is at a particular position, you must iterate over each Unicode scalar from the start or end of that String. For this reason, Swift strings can’t be indexed by integer value

不过 Swift 中有一个获取 String.Index 的 API 如下,这里的 distance 参数表示的字符的 count,我们上面也分析过就算是双标量值表示的字符 é,它的 count 依旧为 1,所以这里用 Int 类型数值作为 String.Index 偏移量并不冲突。

public func index(_ i: String.Index, offsetBy distance: Int) -> String.Index

# 实际操作

# 遍历字符串

遍历字符串的方法

方法一,直接遍历字符串。

var str = "hello world!"
for (index,character) in str.enumerated() {
    print("\(index)-\(character)")
}

**方法二,将字符串转成字符数组。**代码如下

var str = "hello world!"
var chars:[Character] = Array(str)
for char in chars {
    print("char is \(char)")
}

**方法三,通过 index+while 方式去进行遍历,**不过没办法通过 for index in startIndex…endIndex 这种方式遍历。

var index = str.startIndex
while index < str.endIndex {
    print(str[index])
    index = str.index(after: index)
}

这里其实最值得注意的就是 Swift 字符串中的下标(比如上面的 str.startIndex 变量)并不是 Int 类型,而是 String.Index 类型。

# 删除字符串中的某个字符

参考 LeetCode 27. 移除元素 (opens new window) 这道题,我尝试删除字符串中的某个字符,基本思路还是一致的,但是一些具体的操作细节挺考验对 Swift String 相关的 API 的理解和使用。

总的代码如下

//删除给定字符串中的指定字符
//   str: 给定的字符串
//  char: 指定字符
func deleteCharacters(_ str:inout String, char:Character) {
    var slowIndex = str.startIndex
    var quickIndex = str.startIndex
    while quickIndex < str.endIndex {
        let tchar = str[quickIndex]
        if tchar != char {
            str.replaceSubrange(slowIndex...slowIndex, with: [tchar])
            slowIndex = str.index(after: slowIndex)
        }
        quickIndex = str.index(after: quickIndex)
    }
    let range = str.startIndex..<slowIndex
    str = String(str[range])
}

上面这段代码中需要注意的

  1. 我们已经讨论过的,要通过 String.Index 下标的方式去遍历字符串,而非普通的整型下标。
  2. 修改 String 实例的某个下标字符,不能通过 str[x] = “x” 的方式去修改。
  3. 获取某个下标的前后下标,需要通过专门的 API func index(after i: String.Index) -> String.Index 去获得。
  4. 获取给定区间的子字符串的方式。先拿到 range,通过下标+range的方式去获得子字符串。