Swift的字符串和OC有很大差别,下面详细学习下。
Grapheme Clusters (字形簇)
As you know, a string is made up of a collection of Unicode characters. Until now, you have considered one code point to precisely equal one character, and vice versa. However the term "character" is fairly loose.
就我们目前所知道的,字符串是由一连串的Unicode字符组成。于此同时,我们也是通过一个代码点来直接判断两个字符是否相等,反之亦然。但是,"character"其实概念是相当松散的。
直入主题,在swift中我们表示cafe可以有两种方式:
“ One example is the é in café, 其中é用unicode编码单独表示是233,如果用combining character的形式就是e+声调的形式,e是101,声调是769,而后者这种方式就是Unicode标准中的字形簇。在swift中字形簇用Character来表示。
可能上面的解释还不够清楚解释字形簇在swift的中意义,下面看个例子:
let cafeNormal = "café"
let cafeCombining = "cafe\u{0301}"
cafeNormal.count // 4
cafeCombining.count // 4
\u表示的是Unicode的速记法书写形式,我们可以用Unicode速记法书写任意形式的Unicode编码。这里为啥不直接写呢,因为键盘上敲不出来e加声调的字符。
可以看到代码二者的输出长度一致,为什么呢?因为在Swift中,string是被看做字形簇的一个集合,e加声调就是一个字形簇,所以这里长度输出都是4。
上面的结果意味着我们要花费一个线性的时间去找出字符串的长度,因为我们要逐个遍历字符串去找出有多少个字形簇。所以,我们不能简单的通过内存中长度来判断这个字符串有多大。
挺麻烦的吧,所以最后swift给我们提供了unicodeScalars这种方式来访问string的底层Unicode数据:
cafeNormal.unicodeScalars.count // 4
cafeCombining.unicodeScalars.count // 5
for codePoint in cafeCombining.unicodeScalars {
print(codePoint.value)
}
输出结果:
99
97
102
101
769
Indexing strings(字符串索引)
在Swift中,我们无法通过下标来直接访问字符串的具体字符,为啥呢?因为Swift需要我们关注下string的底层原理,原理是啥?就是我们上面说的字形簇。所以,字符串索引访问的方式就比较蛋疼了,我们只能够使用特殊的字符串索引类型而不是整形的下标类型来访问。
let firstIndex = cafeCombining.startIndex
这里firstIndex的数据类型就是String.Index类型。这里我们只是拿到了索引,具体访问:
let firstChar = cafeCombining[firstIndex]
firstChar结果就是c。
下面,我们尝试访问结尾的字符:
let lastIndex = cafeCombining.endIndex
let lastChar = cafeCombining[lastIndex]
但是,错误出现了
fatal error: Can't form a Character from an empty String
为啥呢?原因就是终结索引实际上是在字符串结尾的1,(let cafeCombining = "cafe\u{0301}")。所以,我们需要这么做:
let lastIndex = cafeCombining.index(before: cafeCombining.endIndex)
let lastChar = cafeCombining[lastIndex]
也可以这么做:
let fourthIndex = cafeCombining.index(cafeCombining.startIndex, offsetBy: 3)
let fourthChar = cafeCombining[fourthIndex]
Equality with combining characters (如何判断组合字符是否相等)
Swift采用一种叫canonicalization(规范化)的技术来处理象形字符之间的比较,和传统的其他语言逐个字符比较,然后判断是否都相等不同。Swift首先对字符串进行规范化操作,让2个字符串变成同一种风格,然后再逐个进行比较,具体的实现方式我们不必关注,了解这个特性即可。
let equal = cafeNormal == cafeCombining
在Swift中,上面代码的执行结构是true.
Strings as bi-directional collections(字符串作为双向集合)
Swift做了一些内存优化,双向集合就是一种,其实概念很简单,看下代码就知道了。
let name = "Matt"
let backwardsName = name.reversed()
backwardsName的类型是string吗?不是,其实在swift中它是 reversed collection 类型,Swift做了内存优化,他还是指向源字符串的内存空间。但是,使用起来和其他字符串并无差别。
let secondCharIndex = backwardsName.index(backwardsName.startIndex, offsetBy: 1)
let secondChar = backwardsName[secondCharIndex] // "t”
如果你确实想要开辟一块新空间,那么这么做:
let backwardsNameString = String(backwardsName)
Substrings (子串)
首先,我们来看一段代码:
let fullName = "Matt Galloway"
let spaceIndex = fullName.index(of: " ")!
let firstName = fullName[fullName.startIndex..<spaceIndex] // "Matt”
- open-ended range (开放范围)
Swift提供了一种新的范围类型:开放范围。我们只需要提供一个索引位置,而另一个索引则被默认假设为集合的开始或结尾,类似于数学中的开闭区间。
应用这个类型,我们可以将上面的代码修改如下:
let firstName = fullName[..<spaceIndex]
let lastName = fullName[fullName.index(after: spaceIndex)...]
细心点,我们可以发现这时firstName的数据类型是Substring而不是string类型,这里其实和reversed string是一个原理,Swift本身是做了内存优化的,如果你想要一个全新string那么就这么做:
let lastNameString = String(lastName)
看到这里,我们不禁会想,为什么Swift的设计者要以这么复杂的方式来处理字符串呢?其实Substring在swift中是一个很巧妙的设计。Substring分享父string的内存,这就意味着我们处理一个子串时不需要额外的内存开销。然后,当我们确实需要一个string类型的字符串时,我们可以显示的创建一个新的字符串类型,然后内存中的数据就会拷贝到这个新字符串当中。
Swift的设计者本可以默认的让这种拷贝行为执行,但是没有这么做,设计者就是想让你持有substring的类型,然后明确的让你明白这底层到底发生了什么。如果你不是在函数返回值或者函数传参的时候明确的遇到了要求数据类型为string时,你可能都不知道你持有的一直是substring类型的字符串,这时,我们就可以用substring类型的字符串来显示的初始化一个string类型的新字符串。
Swift的设计者就是这么固执,但是,设计者这么严谨的使用字符串就是告诉我们,字符串使用很频繁,而且处理起来很复杂,这点在日后编码中是很重要的。