最近因为一个字符引发了 Crash,因为实践的业务场景不便描绘,这儿便用一段测试代码作阐明。

话不多说,直接上代码:

let testCharacters: Set<Character> = ["!", "\"", "$", "%", "&", "'", "+", ",", "<", "=", ">", "@", "[", "]", "`", "{", "}"]
let testString = "@`Hello World`!"
var result: UInt8 = 0
for character in testString {
    if testCharacters.contains(character) {
	result += character.asciiValue!
    }
}

上面的代码做的工作是:取出 testString 里特定字符的 ASCII 码,然后相加。

咱们来 Review 下这段代码,有经历的同学应该立马嗅到了代码里的坏滋味:character.asciiValue! 这儿用了强解。

那这儿的强解用得合理吗?因为定义在 testCharacters 里的字符必定都有对应的 ASCII 码,咋一看这儿用强解也不要紧。

可是,如果咱们实践跑一下,就会呈现因为 asciiValue 为 nil 的强解 Crash 了。这是为什么呢?

关键在于 testString 里边包含了 全角字符。testString 里的后一个 是一个全角字符,它是没有 asciiValue 的。

咱们能够在 Swift Playgrounds 里执行下面的代码得到答案:

let halfWidth = "`"
halfWidth.lengthOfBytes(using: .utf8) // 1
halfWidth.first!.isASCII // true
halfWidth.first!.asciiValue // 96
let fullWidth = "`"
fullWidth.lengthOfBytes(using: .utf8) // 3
fullWidth.first!.isASCII // false
fullWidth.first!.asciiValue // nil
// Character 实现 Equatable 协议,判别出两个值是持平的。
halfWidth == fullWidth // true

从上面代码执行结果能够看到,halfWidth 这个半角字符占一个字节长度,对应的 ASCII 码为 96
而全角字符 fullWidth 占三个字节长度,其 asciiValue 为空的。

Swift 数组的 contains 办法运用的是 Equatable 协议 , 从上面代码里 halfWidth == fullWidth 的结果为 true 来看,Character 实现的 Equatable 协议并没有考虑字符全角/半角的情况。

用肉眼看,完全看不出字符有何不同,而 contains 办法结果为 true 也影响了咱们的判别,认为这个强解是 OK 的,稍不留意就导致了 Crash。

最后,从维基百科上整理了关于全角/半角的历史知识:

在前期的计算机中,英语或拉丁字母语言运用的体系,每一个字母或符号,都是运用一字节的空间(一字节由 8 比特组成,共256个编码空间)来贮存;而汉语、日语及韩语文字,因为数量大大超过256个,故惯常运用两字节来贮存一个字符。所以这原本是编码层面的“单字节”“双字节”的问题。

当时的电脑运用等宽字体(如DOS、部分文字编辑器等)时,字体也就适应这种编码形式,将中日韩文字的宽度绘制成拉丁字母和数字的两倍,这样字符的编码存储和显现宽度能够一一对应起来:

  • 单字节文字 显现成 半宽
  • 双字节文字 显现成 全宽
    因此当时的用户就开始习气称中、日、韩等文字为 全角字符,而称拉丁字母或数字为 半角字符

可是,后来计算机的文字编码技术现已产生很大变化,存储一个字符可能用一个、两个、四个或许更多的字节。一个英文字符即便显现为半宽,按照不同的编码方法,并不一定是用一个字节存储。

因此,现在字符编码存储和字符显现宽度的现已没有一一对应联系

可是因为字符编码和字形宽度从前的对应联系,很多用户一向习气性地运用”全角/半角”词汇。

因此现在的 全角字 可能是指:

  • 用两个字节存储的字符
  • ASCII(所谓半角英文和数字)以外所有的字符
  • 显现上字身宽度为一比一正方形的字形。