作者:Mike Ash,原文链接,原文日期:2015-11-06
译者:Cee;校对:numbbbbb;定稿:numbbbbb

译者注:可以结合 WWDC 2015 Session 227 - What’s New in Internationalization 一起学习

欢迎来到本期因修改了很多次稿而推迟发布的周五问答。我发现很多人在使用 Swift 时,都会抱怨 String API 很难用。它很难学习并且设计得晦涩难懂,大多数人希望它能采用其他语言的字符串(String) API 设计风格。今天我就要来讲一下为什么 Swift 中的 String API 会被设计成现在这样(最起码要解释清楚我的看法),以及为什么我最终会认为,就其基础设计而言 Swift 中的 String API 是字符串 API 中设计得最好的。

什么是字符串?

在我们讨论这点之前,首先需要建立一个基本的概念。我们总是把字符串想得很肤浅,很少有人能够深入思考它的本质。深思熟虑才能有助于我们理解接下来的内容。

从概念上来说,什么字符串呢?从表面上看,字符串就是一段文本。"Hello, World" 是字符串;"/Users/mikeash""Robert'); DROP TABLE Students;--" 也是字符串。

(顺道讲一下,我认为不应该把这些不同的文本表述概念看作是同样的字符串类型。人类可读的文本、文件路径、SQL 查询语句,以及其他所有在概念上讲并不相同的东西,在语言表示层面上都应该被表示成不同的类型。我觉得这些概念上不同的字符串应当有不同的类型,这也能大幅减少 bug 数量。尽管我并没有发现有哪个语言或者标准库做到了这点。)

那么在底层,这些常见的「文本」概念又是怎么被表示的呢?唔,得看情况。有很多不同的解决方法。

在很多语言中,字符串是用于存放字节(bytes)的数组(array)。程序所要做的就是为这些字节赋值。这种字符串的表示方法在 C++ 中是 std::string 类型,Python 2、Go 和其他语言也是这样。

C 语言对于字符串的表示就比较古怪和特殊。在 C 语言中,字符串是指向一串非零字节序列(sequence of non-zero bytes)的指针,以零字节位表示字符串的结束。基本的使其实和数组一样,但是 C 语言中的字符串不能包含零字节位,并且诸如查询字符串长度这样的操作需要扫描内存。

很多新语言把字符串定义成了一串 UCS-2 或者 UTF-16 码元(code unit)的集合。Java、C# 还有 JavaScript 是其中的代表。同样,在 Objective-C 中也使用了 Cocoa 和 NSString。这可能是一个历史遗留问题。Unicode 在 1991 年被提出时(译者注:1991 年 10 月发布 Unicode 1.0.0),当时的系统都是 16 位。很多流行的编程语言在那个时代被设计出来,并且将 Unicode 作为字符串的构成基础。在 1996 年,Unicode 在 16 位系统上经历了爆发性的增长(译者注:1996 年 7 月发布了 Unicode 2.0,字库从 7161 个字元变成了 38950 个),这些语言再要改变字符串的编码方式已为时已晚。这时,由于 UTF-16 的编码方式能够将更大的数字编码为一组 16 位码元的集合,因此将字符串视为 16 位码元序列的基本想法就这样延续了下来。

这种想法的一个变体就是将字符串定义成 UTF-8 码元序列,其中组成的码元是 8 位的。总体上来说和 UTF-16 的表示方法很接近,但是对于 ASCII 字符串来说,能够有更加紧凑的表示空间,而且避免了在传递字符串进入函数时,由于这些函数只接受 C 语言风格类型(也就是 UTF-8 字符串)而导致的转换。

也有些语言将字符串表示为 Unicode 码位(code point)指向的一段字符序列。Python 3 中就是这么实现的,在很多 C 语言实现中也提供了内置的 wchar_t 类型。

简短概括一下,一个字符串通常情况下会被当做某些特定字符(character)的序列,其中字符通常是一个字节,或者是一个 UTF-16 码元,又或者是一个 Unicode 码位。

问题

将字符串表示成一段连续「字符」的序列的确很方便。你可以把字符串看作是数组(array)(通常情况下就个数组),这样就很容易获得字符串的子串、从字符串头部或者尾部取出部分元素、删除字符串的某部分、获取字符总数,等等。

问题是我们身边遍布着 Unicode,而 Unicode 会让事情变得很复杂。简单看一个字符串的例子,看一下它是怎么工作的:

aé∞𝄞

每一个 Unicode 码位都有一串数字(写作 U+nnnn)和一个供我们看得懂的命名(某种原因使用全大写的英文字母表示),这样我们更容易讨论单个字符所表示的内容。对于上面这个特定的字符串,它包括了:

  • U+0061 LATIN SMALL LETTER A
  • U+0065 LATIN SMALL LETTER E
  • U+0301 COMBINING ACUTE ACCENT
  • U+221E INFINITY
  • U+1D11E MUSICAL SYMBOL G CLEF

让我们从字符串的中间移除一个「字符」。对于这个「字符」,我们尝试用 UTF-8、UTF-16 和 Unicode 三种不同的字符编码方式来讲解。

首先将这个「字符」看作是一个 UTF-8 字符单元。这个字符串在 UTF-8 下看上去长这样:

61 65 cc 81 e2 88 9e f0 9d 84 9e
-- -- ----- -------- -----------
a e ´ ∞ 𝄞

我们来移除第 3 个「字符」,即第三个字节(cc)。结果是:

61 65 81 e2 88 9e f0 9d 84 9e

这个字符串已经不再是个合法的 UTF-8 字符串。UTF-8 的字符编码有三类。对于那些 0xxxxxxx 表示的,即由 0 开头的,会被表示为 ASCII 字符,单独归为第一类。那些看上去形如 11xxxxxx 的,表示一个多位序列,长度由第一个 0 的位置决定。第三类表示成 10xxxxxx,说明一个多位序列的剩余部分。cc(译者注:即 11001100,划分在第二类。其中第一个 0 出现在从 0 开始计数的第 2 位,故整个多位序列由两个字节组成)表示了一个多位序列的开始,长度是两个字节,81(译者注:即 10000001,划分在第三类)表示了这个多位序列的尾部。如果移除了 cc,那么剩下的 81 将会被留在字符串中。所有 UTF-8 校验器都会拒绝识别这个字符串(译者注:因为 81 并不是合法的 UTF-8 头部字符,只有第一类和第二类的字符是合法的)。如果我们移除了从第三位之后的任意一个字符,这个问题依旧会发生。

那么如果是第二位呢?如果我们移除了这个字符,我们会得到:

61 cc 81 e2 88 9e f0 9d 84 9e
-- ----- -------- -----------
a ´ ∞ 𝄞

看上去这依然是个合法的 UTF-8 字符串,但是结果并不是我们所期待的那样:

á∞𝄞

对于人类来说,在这个字符串中的「第二个字符」应该是「é」。但是第二位上的字符仅仅是不带语调标记的「e」。这个语调标记被看作是一个「连接字符(combining character)」,被单独添加到前面的字符上。移除第二个字符仅仅是移去了「e」,导致这个语调标记连接到了「a」字符上。

那么如果移去首个字符呢?最终结果是我们所想要的那样:

65 cc 81 e2 88 9e f0 9d 84 9e
-- ----- -------- -----------
e ´ ∞ 𝄞

让我们再把这个字符串当做 UTF-16 编码来看。在 UTF-16 编码下,这个字符串看上去长这个样子:

0061 0065 0301 221e d834 dd1e
---- ---- ---- ---- ---------
a e ´ ∞ 𝄞

我们尝试着移除第二个「字符」:

0061 0301 221e d834 dd1e
---- ---- ---- ---------
a ´ ∞ 𝄞

和上面在 UTF-8 中出现的问题一样,删除了「e」,但是没有删除语调标记,导致这个标记附在了「a」上面。

那么如果删除第五个字符呢?我们得到了如下的序列:

0061 0065 0301 221e dd1e

和不合法的 UTF-8 编码是类似的问题,这个序列也不再是一个合法的 UTF-16 字符串。序列 d834 dd1e 形成了一组代理对(surrogate pair),指两个 16 位的单元用于表示一个超过 16 位的码位(译者注:具体计算参考 Wiki)。而让代理对中的一部分单独出现在字符串中是非法的。在 UTF-8 中通常会出错,而在 UTF-16 中这种状态会被忽略。例如,Cocoa 会将这个字符串渲染成这样:

aé∞�

(译者注:即平时出现的乱码现象。)

那么如果一个字符串被表示成一串 Unicode 码位序列呢?字符串看上去是这样的:

00061 00065 00301 0221E 1D11E
----- ----- ----- ----- -----
a e ´ ∞ 𝄞

对于这种表示方式,我们可以移除任意一个「字符」而不会导致产生一个非法的字符串。但是连接语调标记的问题依然存在。移除第二个字符将会是这样:

00061 00301 0221E 1D11E
----- ----- ----- -----
a ´ ∞ 𝄞

即使使用这种表示方法,我们也无法确保结果的正确。

这些通常不是我们能够简单想到的问题。英语是鲜有的几种仅使用 ASCII 字符就能表示的语言。你肯定不想把求职时的简历(Résumé)改成「Resume」吧!一旦超出 ASCII 字符集,这些荒谬的错误就开始出现了。

字素簇(Grapheme Clusters)

Unicode 中有个概念叫做字素簇(Grapheme Clusters),本质上就是阅读时会被考虑成单个「字符」的最小单元。在大多数表示方法中,一个字素簇就等价于一个单独的码位,但是也有可能会表示成包括语调标记的一部分内容。如果我们将上面的例子表示成字素簇的方式,那么很显然会是这样的:

a é ∞ 𝄞

移除任意一个作为字素簇的单元,留下的内容都会被认为是合情合理的。

注意到在这个例子中,并没有任何的数字等值 (numeric equivalents) 存在。这是因为与 UTF-8、UTF-16 或者普通的 Unicode 码位不同,单个数字无法在一般情况下表示字素簇 (grapheme cluster) 。所谓字素簇,指的是一个或多个码位的序列集合。一个字素簇通常会包含一个或两个码位,但是某些情况下(比如 Zalgo 中)字素簇中也可能会包含大量的码位。例如下面这个字符串:

e⃝⃞⃟⃠⃣⃤⃥⃦⃪⃧꙰꙲꙱

这一团乱七八糟的字符串包括了 14 个不同的码位:

+ U+0065
+ U+20DD
+ U+20DE
+ U+20DF
+ U+20E0
+ U+20E3
+ U+20E4
+ U+20E5
+ U+20E6
+ U+20E7
+ U+20EA
+ U+A670
+ U+A672
+ U+A671

所有的这些码位单元都表示一个单独的字素簇。

下面有个有趣的例子。有这样一个包含瑞士国旗的字符串:

🇨🇭

这个标记实际上包括两个码位:U+1F1E8 U+1F1ED。这两个码位又表示什么意思呢?

+ U+1F1E8 REGIONAL INDICATOR SYMBOL LETTER C
+ U+1F1ED REGIONAL INDICATOR SYMBOL LETTER H

Unicode 包含了 26 个「Regional indicator symbol」而不是将地球上的所有国家的国旗作为单独的码位。将 C 和 H 的两个标识符合起来你就能得到瑞士的国旗。将 M 和 X 合起来会得到墨西哥国旗。每个国旗都是一个单独的字符簇,但是由两个码位组成,即四个 UTF-16 码元或者八个 UTF-8 字节。

字符串 API 实现方式

我们发现字符串有多种理解方法,也有多种表示「字符」的方式。将「字符」当做一个字素簇可能最接近人们对于「字符」的理解,但是在代码中操作字符串时,要依据语言环境来判断所谓「字符」的含义。当在文本中移动插入光标时,光标经过的字符就是指字素簇。当为了保证文本满足 140 字限制的推文时,这里的字符就是 Unicode 码位。当字符串想要保存在限定长度是 80 个字符的数据库表中时,这里的字符就是个 UTF-8 字节。

那么当你在实现字符串时,如何来平衡性能、内存使用和简洁代码三者呢?

通常的回答是选择一种标准化表示(canonical representation),之后在需要其他表示方法时进行转换。例如,NSString 使用 UTF-16 作为其标准化表示法。整个 API 基于 UTF-16 建立。如果你想要处理 UTF-8 或者 Unicode 码位,你需要将原始字符串转化成 UTF-8 或者 UTF-32 表示然后再对结果进行操作。这种处理方式更多是将字符串视为数据对象,而不是视为字符串本身,所以在转换时并不是很方便。如果你要对字符簇进行操作,还需要使用 rangeOfComposedCharacterSequencesForRange: 方法找到它们和其他字符的分界位置,这是一项非常枯燥的任务。

Swift 的 String 类型则采用了另外一种方法。在这里面没有标准化的表示,而是为字符串的不同表示方式提供了视图(view)。这样无论处理哪种表示方式,你都能够灵活自如地操作。

简述 Swift 中的 String API

在旧版本中的 Swift 中,String 类遵循了 CollectionType 接口,将自己看做是 Character 元素的集合。在 Swift 2 中,这种表示已经不复存在,String 类会根据使用的不同情况,展现出不同的表现方式。

这种表示方式还不是很完善,String 仍然有点倾向于 Character 集合的表示方式,它依旧提供了有点类似集合处理的接口:

public typealias Index = String.CharacterView.Index
public var startIndex: Index { get }
public var endIndex: Index { get }
public subscript (i: Index) -> Character { get }

你可以通过 String 的索引获得单独的 Character。注意,你并不能通过标准的 for in 语法遍历整个字符串。

在 Swift 看来,一个「字符」究竟是什么?正如我们所见,有太多的可能性。Swift 中 String API 的实现基础是将一个字素簇看作一个「字符」。这看上去是一个非常不错的选择,因为正如我们所见,这种方式符合人类在字符串中对于一个「字符」的定义。

不同的视图在 String 类中作为属性展现。例如,characters 属性:

public var characters: String.CharacterView { get }

CharacterViewCharacter 的一个集合:

extension String.CharacterView : CollectionType {
public struct Index ...
public var startIndex: String.CharacterView.Index { get }
public var endIndex: String.CharacterView.Index { get }
public subscript (i: String.CharacterView.Index) -> Character { get }
}

这看上去有点像 String 接口本身,除了它遵循 CollectionType 协议并且拥有所有 CollectionType 提供的方法外,它实现了划分(slice)、遍历(iterate)、映射(map)或者计数(count)方法。所以尽管下面的方法是不被允许的:

for x in "abc" {}

但是这是行得通的:

for x in "abc".characters {}

你可以使用构造函数从 CharacterView 中获得一个字符串:

public init(_ characters: String.CharacterView)

你甚至可以从随机序列中获取 Character 作为一个字符串:

public init<S : SequenceType where S.Generator.Element == Character>(_ characters: S)
// 译者注:现在是 public init(_ characters: String.CharacterView)

继续我们的旅程,下一个是 UTF-32 字符视图。Swift 把 UTF-32 码元叫做「Unicode 标量(unicode scalars)」(译者注:参看 Unicode scalar values),因为 UTF-32 码元与 Unicode 码位是等同的。这个(简化的)接口看上去是这样的:

public var unicodeScalars: String.UnicodeScalarView
public struct UnicodeScalarView : CollectionType, _Reflectable, CustomStringConvertible, CustomDebugStringConvertible {
public struct Index ...
public var startIndex: String.UnicodeScalarView.Index { get }
public var endIndex: String.UnicodeScalarView.Index { get }
public subscript (position: String.UnicodeScalarView.Index) -> UnicodeScalar { get }
}

类似于 CharacterView,在 UnicodeScalarView 内部也有个 String 的构造函数:

public init(_ unicodeScalars: String.UnicodeScalarView)

不幸的是,UnicodeScalar 序列没有实例化方法,所以在操作时需要做一点额外工作,例如,需要将这些字符转换成数组,然后再将数组转化成字符串。同时,在 UnicodeScalarView 中也没有接受 UnicodeScalar 序列作为参数的实例化方法。然而,Swift 提供了一个在尾部添加元素的函数,所以你可以通过下面三步建立一个 String

var unicodeScalarsView = String.UnicodeScalarView()
unicodeScalarsView.appendContentsOf(unicodeScalarsArray)
let unicodeScalarsString = String(unicodeScalarsView)

接下来是 UTF-16 字符视图,看上去和其他的也很类似:

public var utf16: String.UTF16View { get }
public struct UTF16View : CollectionType {
public struct Index ...
public var startIndex: String.UTF16View.Index { get }
public var endIndex: String.UTF16View.Index { get }
public subscript (i: String.UTF16View.Index) -> CodeUnit { get }
}

在这个视图中,String 的实例化方法又有细微的差别:

public init?(_ utf16: String.UTF16View)

与其他的方法不同,这是一个可能会构造失败的构造方法(译者注:注意 init?)。任何 Character 或者 UnicodeScalar 的序列都是一个合法的 String,但是对于以 UTF-16 作为码元的序列,可能无法将其转化成一个合法的字符串。当内容非法时,构造方法将返回 nil

将任意一个 UTF-16 码元序列转换成一个 String 类型的字符串非常困难。UTF16View 没有公共的构造方法,并且只有很少几个转换方法。这个问题的解决方法就是使用全局 transcode 函数,它已经遵循 UnicodeCodecType 协议。UTF8UTF16UTF32 这三个类中分别实现了这个协议,通过 transcode 函数可以实现三者的互相转化,虽然很不优雅。对于输入,函数接受一个 GeneratorType 类型的参数,中间通过一个用于产生输出结果每一位的函数进行转化。这可将一个 UTF16 字符串一点一点地转化成 UTF32 类型字符串,接着再将每个 UTF-32 码元转化成对应的 UnicodeScalar,拼接到 String 中:

var utf16String = ""
transcode(UTF16.self, UTF32.self, utf16Array.generate(), { utf16String.append(UnicodeScalar($0)) }, stopOnError: true)
// 译者注:transcode 方法的几个参数:
// 1. inputEncoding: InputEncoding.Type
// 2. _ outputEncoding: OutputEncoding.Type
// 3. _ input: Input
// 4. _ output: (OutputEncoding.CodeUnit) -> ()
// 5. stopOnError: Bool
// 这里缺少 utf16Array,可以尝试在第二行代码前加入
// let utf16Array = Array(String(count: 9999, repeatedValue: Character("X")).utf16)
// 来测试结果

最后我们来看一下 UTF-8 字符视图。实现方式和我们之前介绍的一样:

public var utf8: String.UTF8View { get }
public struct UTF8View : CollectionType {
/// A position in a `String.UTF8View`.
public struct Index ...
public var startIndex: String.UTF8View.Index { get }
public var endIndex: String.UTF8View.Index { get }
public subscript (position: String.UTF8View.Index) -> CodeUnit { get }
}

另外在定义中也有一个构造函数。和 UTF16View 一样,这也是一个可能失败的构造方法,因为由 UTF-8 码元组成的序列也有可能是不合法的。

public init?(_ utf8: String.UTF8View)

和前者类似,这儿也没有一种简便的方法将任意一个 UTF-8 码元组成的序列转换成 String 类型。仍然可以使用 transcode 方法:

var utf8String = ""
transcode(UTF8.self, UTF32.self, utf8Array.generate(), { utf8String.append(UnicodeScalar($0)) }, stopOnError: true)
// 译者注:自行补充 utf8Array

因为每次调用 transcode 方法实在是太痛苦了,我将它们用在了这一对可能会构造失败的构造函数中:

extension String {
init?<Seq: SequenceType where Seq.Generator.Element == UInt16>(utf16: Seq) {
self.init()
guard transcode(UTF16.self,
UTF32.self,
utf16.generate(),
{ self.append(UnicodeScalar($0)) },
stopOnError: true)
== false else { return nil }
}
init?<Seq: SequenceType where Seq.Generator.Element == UInt8>(utf8: Seq) {
self.init()
guard transcode(UTF8.self,
UTF32.self,
utf8.generate(),
{ self.append(UnicodeScalar($0)) },
stopOnError: true)
== false else { return nil }
}
}

通过这个构造函数,我们能将任意一个 UTF-8 或 UTF-16 码元组成的序列转化成 String 类型的字符串:

String(utf16: utf16Array)
String(utf8: utf8Array)

索引

上面介绍的不同视图都可用于索引 (Indexes)集合,但是它们并是数组。索引类型是一种非常诡异的自定义结构体(struct)。这意味着你不能通过数字来读取不同视图中的内容:

// all errors
string[2]
string.characters[2]
string.unicodeScalars[2]
string.utf16[2]
string.utf8[2]

不过你可以使用集合类型的 startIndex 或者是 endIndex 属性,并且使用 successor() 或者 advancedBy() 方法来移动到合适的位置:

// these work
string[string.startIndex.advancedBy(2)]
string.characters[string.characters.startIndex.advancedBy(2)]
string.unicodeScalars[string.unicodeScalars.startIndex.advancedBy(2)]
string.utf16[string.utf16.startIndex.advancedBy(2)]
string.utf8[string.utf8.startIndex.advancedBy(2)]

这并不是件有趣的事,我们想知道到底发生了什么?

还记得这些以标准化表示保存在字符串对象的视图吗?当你使用了一个不符合标准化表示形式的视图时,存储的数据并不能自动转化成你想要的形式。

回想一下上面所提到的,不同的编码方式有不同的大小和长度。这也意味着无法简单地判断字符在不同视图中对应的位置,因为所映射到的位置是根据保存的数据不同而不同的。考虑下面这个字符串:

AƎ工🄞

这个 String 类型的字符串在 UTF-32 编码下的标准化表示是几个 32 位整型元素的集合:

0x00041 0x0018e 0x05de5 0x1f11e

我们再站在 UTF-8 编码的视角上来看这些数据。理论上说,这些数据就是一组 8 位整型元素的序列:

0x41 0xc6 0x8e 0xe5 0xb7 0xa5 0xf0 0x9f 0x84 0x9e

下面是两者的映射关系:

| 0x00041 | 0x0018e | 0x05de5 | 0x1f11e |
| | | | |
| 0x41 | 0xc6 0x8e | 0xe5 0xb7 0xa5 | 0xf0 0x9f 0x84 0x9e |

如果需要获取在 UTF-8 视图下索引为 6 的值,那么必须去从 UTF-32 的序列中从头开始去扫描,然后获取所在位置所对应的值。

显然,这是可以做到的。Swift 提供了这种底层方法,但是长得并不好看:string.utf8[string.utf8.startIndex.advancedBy(6)]。为什么不能简化这种表示,直接用一个整数来访问索引呢?实际上 Swift 为了加强这种表示牺牲了简洁性。在一个 UTF8View 能提供 subscript(Int)(译者注:即下标索引) 方法的世界里,我们希望下面两段代码是等价的:

for c in string.utf8 {
...
}
for i in 0..<string.utf8.count {
let c = string.utf8[i]
...
}

这看上去很相似,但是第二个会意外地更慢一些。第一个循环是一个线性时间的扫描,然而第二个循环需要对每次迭代做一次线性扫描,即需要用二次方项的时间来做迭代遍历。对于一个长度为一百万的字符串,第一个循环只需要 0.1 秒,而第二个循环需要 3 个小时(在我的 2013 年 MacBook Pro 上进行的测试)。

我们再来看另外一个例子,从字符串中获得最后一个字符:

let lastCharacter = string.characters[string.characters.endIndex.predecessor()]
let lastCharacter = string.characters[string.characters.count - 1]

第一个版本会更快一些。因为它直接从字符串的最后开始,从最后一个 Character 开始的地方从后往前搜索,然后获取字符。第二个版本会扫描整个字符串……两次!它首先得扫描整个字符串来获取有多少个 Character,接着再一次扫描特定序号的字符是什么。

类似这样的 API 在 Swift 中只是有点不同、有点难写。这些不同之处让程序员们知道了视图并不是数组,它们也没有数组的行为。当我们使用下标索引时,我们事实上假设了这种操作是一种效率很高的行为。如果 String 的视图提供了这种索引,那其实和我们的主观假设相反,只能写出效率很低的代码。

使用 String 类来写代码吧

在应用层面上使用 String 类写代码意味着什么呢?

你可以使用顶层 API。举个例子,如果你需要判断一个字符串是否是以某个字符开头的,那不需要对字符串索引然后获取第一个字符并做比较。直接使用 hasPrefix 方法,它已经为你准备好了一切。不要害怕导入 Foundation 库和使用 NSString 中的方法。当你想移除 String 开头和结尾多余的空格时,不必手动遍历获取这些字符,可以直接使用 stringByTrimmingCharactersInSet 方法。

如果你需要做一些字符层面的事情,那么就要想象一下,对于特定情况一个「字符」意味着什么。通常,正确答案是指一个字素簇,这在 Swift 中表示成 Character 类型,展现在 characters 视图中。

无论你需要对文本做些什么事情,都要思考一下对文本从头到尾线性扫描的事情。诸如计算有多少个字符、查找中间的字符这类操作会消耗线性的时间,所以你最好整理一下代码,更加干净利落地做这些线性时间扫描的操作。对于特定的视图,取得开始和结束的下标索引,在必要的时候使用 advancedBy() 或者其他类似的方法来移动索引的位置。

总结

Swift 中的 String 类型采取了一种与众不同的方法来处理字符串。其他很多语言会选择一种标准化表示法,然后将转换等操作留给程序员自己去处理。通常它们在「到底什么才是字符?」这种重要的问题上做出了妥协,它们在处理字符串的时候,直接在编码中加入一些「语法糖」来让代码更加易写,然而这本质上就会导致各种困难的发生。Swift 语法可能没那么「甜」,相反则是在告诉你实际上会发生什么。对于程序员来说,这会比较困难,但其实也就只有这些困难。

String 的 API 中也有一些坑,但是我们可以使用一些其他的方法来让操作稍微简单一些。特别地,从 UTF-8 或 UTF-16 转换成一个 String 类型的数据是一件困难而又烦人的事。如果我们有一些能够将任意一串码元序列转换成字符串的 UTF8ViewUTF16View 构造方法,以及另外一些直接建立在这些视图上的转换方法,那么 Swift 中的 String 类型将变得更加友好。

今天就到这里了。希望下次还能给大家带来更多惊喜。周五问答的主题是根据大家的想法产生的,所以记得给我们写信来提出你想要听的话题

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

文章目录
  1. 1. 什么是字符串?
  2. 2. 问题
  3. 3. 字素簇(Grapheme Clusters)
  4. 4. 字符串 API 实现方式
  5. 5. 简述 Swift 中的 String API
  6. 6. 索引
  7. 7. 使用 String 类来写代码吧
  8. 8. 总结