Swift:字符串下标访问与Unicode

居然不能用整数下标随机访问?

第一次使用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容