居然不能用整数下标随机访问?
第一次使用Swift字符串之前,已经习惯了C,C++直接通过下标随机访问字符串数组的用法,乃至于第一次顺手用Swift写下这样的代码却报错时,满脑子只剩黑人问号
let str = "hello world"
// 报错:'subscript' is unavailable: cannot subscript String with an Int, see the documentation comment for discussion
let char = str[1]
跟随着错误提示,打开文档才恍然大悟,String的下标方法,根本不支持Int类型的下标:
subscript(bounds: Range<String.Index>) -> String { get }
Accesses the text in the given range.
subscript(i: String.Index) -> Character { get }
Accesses the character at the given position.
subscript(bounds: ClosedRange<String.Index>) -> String { get }
Accesses the text in the given range.
文档中列出Swift标准库只支持的三种下标访问String字符串的方法,看一下这三种参数。
-
Range<String.Index>
:元素为String.Index类型的Range(开区间) -
String.Index
:String.Index元素 -
ClosedRange<String.Index>
:元素为String.Index类型的CloseRange(闭区间)
所以正确的String下标访问姿势是这样:
let str = "hello world"
/// 下标类型为String.Index
let firstChar = str[str.startIndex] // h
let secondChar = str[str.index(after: str.startIndex)] // e
let thirdChar = str[str.index(str.startIndex, offsetBy: 2)]
let lastChar = str[str.index(before: str.endIndex)] // !
/// 下标类型为Range<String.Index>
let fullStr = str[str.startIndex..<str.endIndex] // hello world!
/// 下标类型为ClosedRange<String.Index>
let fullStr = str[str.startIndex...str.index(before: str.endIndex)] // hello world!
自己拓展下标方法
学习了String的下标方法之后,得知原来的错误写法应该改成:
let str = "hello world"
//let char = str[1]
let char = str[str.index(str.startIndex, offsetBy: 1)]
很显然比起直接用Int访问的方式,写起来麻烦很多,这个时候我们就可以利用拓展添加自己的下标方法了
extension String {
subscript (i: Int) -> Character {
return self[self.index(self.startIndex, offsetBy: i)]
}
subscript (i: Int) -> String {
return String(self[i] as Character)
}
subscript (r: Range<Int>) -> String {
let start = index(startIndex, offsetBy: r.lowerBound)
let end = index(startIndex, offsetBy: r.upperBound)
return self[start..<end]
}
subscript (r: ClosedRange<Int>) -> String {
let start = index(startIndex, offsetBy: r.lowerBound)
let end = index(startIndex, offsetBy: r.upperBound)
return self[start...end]
}
}
用起来妥妥的很方便,追求安全的话也可以添加控制越界的代码返回可选值,比如:
subscript (i: Int) -> Character? {
guard i < self.characters.count else{
return nil
}
return self[self.index(self.startIndex, offsetBy: i)]
}
想在Swift中直接使用整数进行下标访问的时候,之前使用OC的小伙伴可能会想到这样的办法:
let str = "hello world"
//let char = str[1]
let char = (str as NSString).character(at: 1) //101
101是e在UTF-16(NSString对象使用UTF-16编码)下的编码,可见输出正确,但是让我们换一个字符串再看看:
let string = "e\u{301}" // é
let charFromNSString = (str as NSString).character(at: 0) //101
let charFromString = str[str.startIndex] //é
character at:方法与Swift的下标访问方法返回的值并不一样,这是为什么呢?
Returns the character at a given UTF-16 code unit index.
这是Apple文档中对character at:方法的描述,说明此方法的索引对象是字符串对应的UTF-16码元。所以返回了索引为0的码元,即101。对于这种情况OC中有专门的字符串正规化处理办法,也可以判断一个字符的码元长度,可以参考:NSString 与 Unicode中正规形式与随机访问部分。
至于为什么String类型就是直接输出了é呢,这就要了解一下String类型与Unicode的关系了。
String背后的Unicode
Swift的String类型是基于Unicode标量建立的,先来介绍一下Unicode和Unicode标量。
Unicode
人类使用的文字和符号要想被计算机所理解必须要经过编码,Unicode就是其中的一种编码标准。
码点:Unicode标准为世界上几乎所有的书写系统里所使用的每一个字符或符号定义了一个唯一的数字。这个数字叫做码点(code points),以U+xxxx这样的格式写成,格式里的xxxx代表四到六个十六进制的数。例如U+0061表示小写的拉丁字母(LATIN SMALL LETTER A)("a"),U+1F425表示小鸡表情(FRONT-FACING BABY CHICK) ("🐥"),有趣的是,字符的名字比如"FRONT-FACING BABY CHICK"也是Unicode标准的一部分。
编码格式:通过字符到码点之间的映射,人们得以用统一的方式表示符号,但还需要定义另一种编码来确定码点与其存储在内存和硬盘中的值的对应关系。有三种Unicode支持的编码格式:
- UTF-8:表示一个码点需要1~4个八位的码元。利用字符串的utf8属性进行访问。
- UTF-16:用一或两个16位的码元表示一个吗点。利用字符串的utf16属性进行访问。
- 21位的 Unicode 标量值集合,也就是字符串的UTF-32编码格式,用21位的码元表示一个码点。利用字符串的unicodeScalars属性进行访问。
String的可拓展字符群集
每一个String对象都有一个characters: String.CharacterView
属性,代表一个可拓展的字符群。Apple文档中对String.CharacterView的描述:
In Swift, every string provides a view of its contents as characters. In this view, many individual characters—for example, “é”, “김”, and “🇮🇳”—can be made up of multiple Unicode code points. These code points are combined by Unicode’s boundary algorithms into extended grapheme clusters, represented by the Character type. Each element of a CharacterView collection is a Character instance.
大意为每一个Swift字符串提供一个CharacterView包含它的全部内容,在CharacterView中,如“é”, “김”, and “🇮🇳”是作为独立的character存在的,这些独立的character可能是由多个Unicode码点组成的。组成独立character的一个或多个码点会被Unicode组合成一个可拓展字符群,由Character类型来表示。CharacterView集合的每一个元素都是一个Character实例。先来看一下可拓展字形群的意思:
可拓展的字形群:一个或多个可生成人类可读字符的Unicode标量的有序排列。比如é即为一个可拓展字形群,它可以用单一的Unicode标量é(LATIN SMALL LETTER E WITH ACUTE, 或者U+00E9)来表示。然而一个标准的字母e(LATIN SMALL LETTER E或者U+0065)加上一个急促重音(COMBINING ACTUE ACCENT,或者U+0301),这样一对标量就表示了同样的字母é。在第一种情况,这个字形群包含一个单一标量;而在第二种情况,它是包含两个标量的字形群。除此之外,还有几个比较特殊的可拓展字符群集如:
let precomposed: Character = "\u{D55C}" // 한
let decomposed: Character = "\u{1112}\u{1161}\u{11AB}" // ᄒ, ᅡ, ᆫ
// precomposed 是 한, decomposed 是 한
let enclosedEAcute: Character = "\u{E9}\u{20DD}"
// enclosedEAcute 是 é⃝
let regionalIndicatorForUS: Character = "\u{1F1FA}\u{1F1F8}"
// regionalIndicatorForUS 是 🇺🇸
由文档我们可以得知,CharacterView集合中的元素为Character实例,一个Character实例对应一个可拓展字形群。所以Swift字符串无论是在计算长度(通过character的count属性),还是在进行下标访问,都是以Character,也就是可拓展字形群为最小单位的,所以在Swift字符串眼里é("\u{E9}")与e加 ́("\u{65}\u{301}")是一回事。
但NSString对象是以Unicode码点为最小单位索引和计算长度,所以所示结果与Swift字符串不同。
let str = "e\u{301}" // é
let strLengthOfString = str.characters.count // 1
let strLengthOfNSString = (str as NSString).length // 2
参考书目与文章
《The Swift Programming Language:Strings and Characters》
NSString 与 Unicode
网友评论