第七章——字符串(字符串与集合)

171 阅读7分钟

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

如果查看String结构体的定义就会发现其中有一个类型别名Index,有两个属性startIndexendIndexString类型还定义了下标脚本,可以通过下标获取对应位置上的字符。String实现了generate()函数,返回的生成器可以按顺序遍历所有字符。如果你对CollectionType协议比较熟悉,你会发现String结构体其实具备了实现这个协议的一切条件,不过因为它没有显示的声明实现这个协议,所以我们无法使用for … in语法,String也无法调用定义在CollectionTypeSequenceType协议拓展里的函数。理论上说我们可以自己拓展String结构体:

extension String: RangeReplaceableCollectionType {} // 声明String实现了CollectionType协议,不需要提供任何方法实现。

var greeting: String = "Hello, world"
if let comma = greeting.indexOf(",") {
print(greeting[greeting.startIndex..<comma])
greeting.replaceRange(greeting.indices, with: "How about some original example string?")
}

不过,显然不是Swift语言的开发者忘记了让String类型实现CollectionType协议。所以我们人为的去拓展String结构体是不明智的行为。虽然在CollectionTypeSequenceType协议拓展里定义了很多非常有用的算法,但并非所有算法都适用于字符串处理,因为其中涉及到Unicode字符的拼接问题。虽然Character类型尽可能把多个代码点拼接成一个字符,但有些时候一个字符一个字符地处理字符串还是有可能会产生错误的结果。

因此,从“字符的集合”这一角度看到的字符串,被声明成了字符串的一个属性:characters,其它几个类似的视图有unicodeScalarsutf8utf16。选择了某一个视图后,你会进入集合处理模式,需要考虑你将要调用的算法产生的结果。在所有视图中,CharacterView是一个比较特殊的视图。String的类型别名Index其实就是CharacterView.Index,这说明CharacterView视图中的下标可以直接在字符串类型中使用。

因为一个字符可以由不同数量的代码点组成,所以CharacterView视图并非随机访问的,你无法判定一个给定的字符前有多少代码点。因此,String.Index只实现了BidirectionalIndexType,也就是我们可以从字符串的起点(结尾)向后(前)遍历,Swift会自动判断有没有多个代码点可以拼接并产生一个正确的偏移量。String的下标还实现了Comparable协议,你或许不知道两个下标之间隔了多少字符,但至少可以知道一个字符的前一个和后一个字符是什么。successorpredecessor方法提供了这个功能。

如果想要直接获取到更远距离的下标,可以使用advancedBy方法来代替在for循环中使用successorpredecessor方法:

let s = "abcdef"
let idx = s.startIndex.advancedBy(5, limit: s.endIndex)  // 向后5个位置
print(s[idx]) //输出结果是:f

limit参数可以确保不会越界,如果向后偏移量太大,超过了字符串的结尾,idx的值就是s.endIndex。不过我们认为,advancedBy方法应该返回一个可选类型的值,如果越界就返回nil

有的读者此时可能会恍然大悟:“我可以为String类型重载下标脚本,接收整数类型的参数”,代码如下:

extension String {
subscript(idx: Int) -> Character {
let strIdx = self.startIndex.advancedBy(idx, limit: endIndex)
guard strIdx != endIndex else { fatalError("String index out of bounds") }
return self[strIdx]
}
}

print(s[5]) //输出结果是:f

正如我们不应该把字符串拓展成集合类型,这种拓展最好也应该避免。这是因为,我们可能会写出这样的代码:

for i in 0..<5 {
print(s[i])
}

但这种写法的效率非常低下,我们知道下标是无法随机访问的,所以s[i]的时间复杂度是O(n),整个循环的时间复杂度是O(n ^ 2),随着字符串长度的增加,耗时会以平方级的速度增加。如果之前习惯了处理固定长度的字符,我们现在可能会觉得有些不适应:没有了整数下标,怎么方便快速地获取到这些字符呢?好在Swift把字符放在集合类型中,我们可以像操作数组那样处理String.characters

首先,即使没有整数下标,遍历所有的字符也很容易,可以使用for循环:

for (i, c) in "hello".characters.enumerate() {
print("\(i): \(c)")
}

(i, c)是一个元组,i是从0开始的一组连续整数,c表示第i个字符。读者可以运行这段代码,或直接查看enumerate()函数的定义。

接下来,如果想要找到某一个特定的字符,我们可以使用indexOf方法:

var greeting: String = "Hello!"
if let idx = greeting.characters.indexOf("!") {
greeting.insertContentsOf(", world".characters, at: idx)
}
print(greeting)	// 输出结果:Hello, world!

String并不具备MutableCollectionType协议的特点,实现这个协议的集合的下标脚本是可读可写的。这并不说明String不可变,我们只是无法使用下标脚本修改String的某个字符。原因依然是字符的长度是不固定的,把一个由3个代码点组成的字符替换成由2个代码点组成的字符所需要的时间是线性的,因为后续所有的字符的位置都需要移动。因此,即使是替换单个字符,我们也需要使用replaceRange方法。

字符串与可切片

处理数组的切割操作是比较麻烦的,因为返回值的类型不是Array而是ArraySlice。这会让写递归函数变得痛苦,因为输入的参数类型可能是ArraySlice。字符串的集合视图就没有这样的问题,SubSlic被定义成Self类型的一个实例,所以范型函数的参数类型可以是Sliceable,返回值也是Sliceable,举个例子来说明,world变量的类型是String.CharacterView

let world = "Hello, world!".characters.suffix(6).dropLast()    // 值是:"world"

数组的split函数返回的是数组切片,它对于字符串处理也有用,其定义如下:

extension CollectionType {
public func split(maxSplit: Int = default,
allowEmptySlices: Bool = default,
@noescape isSeparator: (Self.Generator.Element) -> Bool) -> [Self.SubSequence]
}

由于前两个参数都有默认值,所以它的使用非常简单:

let seperatedArray = "a,b,c".characters.split{ $0 == "," }.map { String($0) }
print(seperatedArray)	// 输出结果:["a", "b", "c"]

因为split函数的最后一个参数是闭包,所以它能做额不仅仅是比较字符这么简单,我们可以以此实现一个换行器,可以找到非常长的字符串中的空格或换行符,然后自动换行:

extension String {
func wrap(after: Int = 70) -> String {
var i = 0
let lines = self.characters.split{ character -> Bool in
switch character {
case "\n", " " where i >= after:
i = 0
return true
default:
++i
return false
}
}.map(String.init)
return lines.joinWithSeparator("\n")
}
}

因为split函数返回值的类型是[Self.SubSequence],所以对于字符串来说,这个类型是String.CharacterView,因此需要调用map函数把数组中的元素转化成String类型。

在大多数时候,我们需要根据某个字符来分割字符串,所以可以新增mSplit函数以简化这样的操作:

extension CollectionType where Generator.Element: Equatable {
func mSplit(seperator: Generator.Element) -> [SubSequence] {
return split { $0 == seperator}
}
}

之前的代码简化为:

let seperatedArray = "a,b,c".characters.mSplit(",").map { String($0) }
print(seperatedArray)	// 输出结果:["a", "b", "c"]

个人认为可以进一步简化:

extension String {
func split(seperator: Character) -> Array<String> {
return characters.split{ $0 == seperator}.map { String($0) }
}
}

let seperatedArray = "a,b,c".split(",")
print(seperatedArray)	// 输出结果:["a", "b", "c"]

还可以定义一个split函数可以接受多个分隔符:

extension CollectionType where Generator.Element: Equatable {
func split<S: SequenceType where Generator.Element == S.Generator.Element>
(seperators: S) -> [SubSequence] {
return split { seperators.contains($0) }
}
}

print("Hello, world".characters.split(",!".characters).map { String($0) })
// 输出结果:["Hello", " world"]