忘了String是字符数组

在Swift里,String已经彻底不再是一个集合类型。而是一个提供了从多个维度展现一个Unicode视图的类型。你可以得到它的多个Characters,可以看到它的UTF-8 / UTF-16 / Unicode scalar值等等。

String和NSString处理Unicode时的差异

unicode长度是可变的,我们将看到unicode另外一个可变的特性,即组成同一个字符的code unit组合也是可变的。而区分String和NSString的一个重要方式,就是它们对unicode的这个特性的处理方式,是不同的。为了理解这个事情,我们从unicode grapheme clusters说起。

Unicode grapheme clusters

首先,我们定义一个字符串:

let cafe = "Caf\u{00e9}"

返回

Café

Swift里,我们可以使用\u{}这样的方式使用unicode scalar定义unicode字符。

对于单词Café中的最后一个字符来说,它的unicode scalar是U+00E9,名字是LATIN SMALL LETTER E WITH ACUTE。每一个unicode都有一个scalar值以及一个全大写字母表示的名称。

为了表示这个字符é,除了使用它的unicode scalar外,我们可以用两个其它的unicode字符拼起来:

  • 英文字母e,它的unicode scalar是U0065,name是LATIN SMALL LETTER E;
  • 声调字符’,它的unicode scalar是U0301,name是COMBINING ACUTE ACCENT;

当我们把这两个字符像下面这样组合起来的时候:

let cafee = "Caf\u{0065}\u{0301}"

返回

Café

尽管cafee的定义中貌似有5个字符,但实际显示出来的最后一个字符和之前用unicode scalar定义是一样的。我们管\u{0065}\u{0301}就叫做grapheme cluster。

既然同一个unicode字符可以有多种表现形式,那么由不同code unit构成的字符串相等么?对此,Swift中的String和Objective-C中的NSString处理方式却是有差别的,这种差别,是区分它们最明显的地方之一。

Canonically equivalent

为了能识别上面cafe和cafee的情况,unicode规范中提出了一个概念:canonically equivalent。如何理解它呢?我们先来看Swift是如何识别cafe和cafee的。

Swift String

当我们要读取一个字符串中所有的字符时,可以访问String对象的characters属性,在下个视频中,我们会更多讲到它的用法:

let cafe = "Caf\u{00e9}"
let cafee = "Caf\u{0065}\u{0301}"

cafe.characters.count
cafee.characters.count

返回

4
4

尽管cafee中最后一个字符的定义使用了两个code unit,Swift可以识别的Character中字符的个数也是4。

但是,当我们查看cafe和cafee的UTF-8和UTF-16编码的个数时,就能看到它们的区别了:

cafe.utf8.count
cafee.utf8.count

cafe.utf16.count
cafee.utf16.count

返回

5
6

4
5

为什么会这样呢?我们用UTF-8编码举例:

对于cafe来说,é的UTF-8编码是C3 A9,加上前面Caf的编码是43 61 66,因此cafe的UTF-8编码个数是5;

对于cafee来说,声调字符’的UTF-8编码是CC 81,加上前面Cafe的UTF-8编码是43 61 66 65,因此是6个,它相当于Cafe’;

理解了之后,你可以自己去推算一下UTF-16的情况。

尽管cafe和cafee的编码方式不同,当我们在Swift中,比较cafe和cafee时,结果会是true:

cafe == cafee

返回

true

这就是unicode canonically equivalent的含义,通过这些例子,你也能更好的了解到Swift在unicode表意正确上作出的努力。

NSString

而当我们把这些例子用在NSString上,情况就会有些不同。用同样的code unit定义下面两个NSString对象:

let nsCafe = NSString(characters: [0x43, 0x61, 0x66, 0xe9], length: 4)
nsCafe.length
let nsCafee = NSString(characters: [0x43, 0x61, 0x66, 0x65, 0x0301], length: 5)
nsCafee.length

返回

Café
4
Café
5

可以看到,同样是使用不同的code unit构建字符串”Café”,在NSString看来,它们是长度不同的两个字符串。

因此,当我们比较nsCafe和nsCafee的时候,结果也是没有意外的false:

nsCafe == nsCafee

返回

false

因此,==对NSString来说,并没有执行canonically equivalent的语义。为了在不同的NSString对象之间进行语义比较,我们只能这样:

let result = nsCafe.compare(nsCafee as String)
result == ComparisonResult.orderedSame

返回

NSComparisonResult
true

可以看到,这样就能按照canonically equivalent的方式判断相等了。