文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 如何正确处理逐个字符的问题
  4. 4. Swift中String如何处理逐个字符?
  5. 5. 让Swift字符串也支持整型下标访问
  6. 6. 总结

文档更新说明

  • 最后更新 2019年06月19日
  • 首次更新 2019年06月19日

前言

  之前写过一篇OC中的NSString和Swift中的String , 简单记录了字符串数量的数量问题, 这次仔细说一下, 老的文档就不改了, 直接开一个新的.   

如何正确处理逐个字符的问题

  前面讲到, 在OC里面NSString的length表示存储字符的UFT16的单元数量, 如果我们的字符串全部是字母, 那么一个字母就表示1个单元, 但是字符串夹杂着汉字, emoji的时候就麻烦了, 麻烦的不是说汉字有多个字节存储, 相反汉字如果用UTF16存储的话,只需要1个单元即可(内存占用两个字节), 但是emoji不同, 有的需要2个UTF16单元, 有的甚至需要更多, 先来看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSString *str = @"中国人😂|🥶a";
printf("%lu\n", (unsigned long)str.length);
for(int i = 0; i < str.length; i++){
unichar ch = [str characterAtIndex: i];
printf("%s\n", [[NSString stringWithCharacters:&ch length:1] cStringUsingEncoding:NSUTF8StringEncoding]);
}
}

运行上面代码,结果如下:

1
2
3
4
5
6
7
8
9
10
9
(null)
(null)
|
(null)
(null)
a

可以看到其中emoji表情没有出现, 但是多了几个null这是为什么? 稍微修改一下上面的代码, 用纯OC的形式打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSString *str = @"中国人😂|🥶a";
printf("%lu\n", (unsigned long)str.length);
for(int i = 0; i < str.length; i++) {
// unichar 是 unsigned short的别名, 占用2个字节
unichar ch = [str characterAtIndex: i];
NSLog(@"%@", [NSString stringWithCharacters:&ch length:1]);
}
}

运行上面代码, 结果如下:

1
2
3
4
5
6
7
8
9
10
9
2019-06-19 15:15:09.439632+0800 OCSimpleView[4247:307941] 中
2019-06-19 15:15:09.439819+0800 OCSimpleView[4247:307941] 国
2019-06-19 15:15:09.439909+0800 OCSimpleView[4247:307941] 人
2019-06-19 15:15:09.440028+0800 OCSimpleView[4247:307941] \ud83d
2019-06-19 15:15:09.440106+0800 OCSimpleView[4247:307941] \ude02
2019-06-19 15:15:09.440183+0800 OCSimpleView[4247:307941] |
2019-06-19 15:15:09.440251+0800 OCSimpleView[4247:307941] \ud83e
2019-06-19 15:15:09.440342+0800 OCSimpleView[4247:307941] \udd76
2019-06-19 15:15:09.440342+0800 OCSimpleView[4247:307941] a

现在可以看明白了, 原来符号😂需要2个UFT16单元表示, 🥶也是需要2个单元, 所以我们上面使用方法stringWithCharacters的时候, 😂需要两个单元才能解析出完整的符号,我们却只传了一个, 导致解析失败打印出来的就是null.

  理解了这个之后, 接着说正确处理的方法. OC提供了一个自动识别符号字节大小的方法, 修改一下代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSString *str = @"中国人😂|🥶a";
printf("%lu\n", (unsigned long)str.length);
NSRange range;
for(int i = 0; i < str.length; i += range.length) {
range = [str rangeOfComposedCharacterSequenceAtIndex:i];
NSLog(@"%@, range.length=%@", [str substringWithRange:range], @(range.length));
}
}
/* 打印如下
9
2019-06-19 15:28:02.422055+0800 OCSimpleView[4510:322599] 中, range.length=1
2019-06-19 15:28:02.422182+0800 OCSimpleView[4510:322599] 国, range.length=1
2019-06-19 15:28:02.422276+0800 OCSimpleView[4510:322599] 人, range.length=1
2019-06-19 15:28:02.422394+0800 OCSimpleView[4510:322599] 😂, range.length=2
2019-06-19 15:28:02.422481+0800 OCSimpleView[4510:322599] |, range.length=1
2019-06-19 15:28:02.422579+0800 OCSimpleView[4510:322599] 🥶, range.length=2
2019-06-19 15:28:02.422667+0800 OCSimpleView[4510:322599] a, range.length=1
*/

终于可以正确获取每一个符号了. 看到这里, 我们对OC字符串的处理的理解加深了, 平时产品经理让你限制用户名符号个数之类的处理起来也更明白了. 其实OC在这方面的处理还是不明显的, 下面看看Swift的处理方法, 顺便理解一下Swift为什么要这么设计.

Swift中String如何处理逐个字符?

  第一次接触Swift的时候估计很多人都被其中的字符串API搞晕了, 它不像OC一样有一个- (unichar)characterAtIndex:(NSUInteger)index;方法, index是整形, 传0就是第0个符号. 取而代之的是String.Index类, 下面先看一下遍历字符

1
2
3
4
5
6
let str2 = "中国人😂|🥶a"
print(str2.count)
for c in str2 {
print(c)
}

运行上面代码, 结果如下

1
2
3
4
5
6
7
8
7
😂
|
🥶
a

一个for循环可以直接遍历出人眼可见的符号, 而不像OC一样使用for i++语法遍历出的是单个UFT16单元, 这个确实可以避免OC中得到一半符号数据的尴尬.
  再看一下, 如果得到指定位置的符号.

1
2
3
4
5
6
7
print("index 0:", str2[str2.index(str2.startIndex, offsetBy: 0)])
print("index 3:", str2[str2.index(str2.startIndex, offsetBy: 3)])
/*运行结果
index 0: 中
index 3: 😂
*/

重点说明, 上面的API就是很多人第一次接触Swift的时候感觉到的困惑, 为什么不直接用
str2[0], str2[1]
这样的形式来取第一个第二个字符? 而把API复杂化. 从上面OC的处理方式我们知道, 直接使用数字获取某一个符号, 能否成功是跟数据的存储有很大关系, 为了让语法看起来更加清晰, Swift采取了Index的形式, 一个Index就代表一个完整单元, 用户不用担心数据完整性问题(我猜的, 实际上你要用str[0]表示第一个单元也可理解). 这里有个小窍门, 如果不考虑性能问题, 可以直接将String转为数组, 这样每个数组元素都是一个Character, 就支持下标访问了.
  深入一点, 如何获取字符串的其他表现形式呢? 像上面直接获取str2.count得到的数量是7, 这个又和OC不同. Swift为字符串提供了多种不同视图, 看下面代码:

1
2
3
4
5
6
7
8
9
10
11
print("count:", str2.count)
print("utf8:", str2.utf8.count)
print("utf16:", str2.utf16.count)
print("unicodeScalars:", str2.unicodeScalars.count)
/* 运行结果
count: 7
utf8: 19
utf16: 9
unicodeScalars: 7
*/

要理解上面的结果, 我们需要知道每一个视图存储的内容是什么.
先从UFT8View开始, 查看源码, 得到UTF8View中存储的元素是public typealias Element = UTF8.CodeUnit, 而UTF8.CodeUnit又是public typealias CodeUnit = UInt8, 所以UTF8View里存的就是字符串的utf8编码, 每个元素都是一个Uint8, 0-255, 也就是一个字节, utf8编码中文占3字节, emoji大部分是4个字节,字母占1个字节, 9+8+2=19, 所以str2.utf8.count为19, 下面代码使用String.UTF8View.Index来获取指定index的单元

1
2
3
4
5
print("index 0:", str2.utf8[str2.utf8.index(str2.utf8.startIndex, offsetBy: 0)])
/* 运行结果
utf8 index 0: 228
*/

其实我们有更简单的方法获取视图里的元素, 代码如下:

1
2
3
4
5
6
7
8
let uft8Arr = Array(str2.utf8)
print(uft8Arr[0])
print(uft8Arr)
/* 运行结果
228
[228, 184, 173, 229, 155, 189, 228, 186, 186, 240, 159, 152, 130, 124, 240, 159, 165, 182, 97]
*/

其他视图, UTF16View存储的元素类型是UInt16, 所以str2.utf16.count为9, 和OC中length相同; UnicodeScalarView 存储的元素类型是Unicode.Scalar, 可以理解为是一个Unicode编码单元, 一个单元对应一个人眼可见的符号, 所以str2.unicodeScalars.count为7.
最后再看一下str2字符串中视图的全部数据,加深理解

1
2
3
4
5
6
7
8
9
10
11
print(Array(str2))
print(Array(str2.utf8))
print(Array(str2.utf16))
print(Array(str2.unicodeScalars))
/* 运行结果
["中", "国", "人", "😂", "|", "🥶", "a"]
[228, 184, 173, 229, 155, 189, 228, 186, 186, 240, 159, 152, 130, 124, 240, 159, 165, 182, 97]
[20013, 22269, 20154, 55357, 56834, 124, 55358, 56694, 97]
["\u{4E2D}", "\u{56FD}", "\u{4EBA}", "\u{0001F602}", "|", "\u{0001F976}", "a"]
*/

让Swift字符串也支持整型下标访问

  下面代码还是比较简单的, 扩展一下String类, 这样就支持便捷的整型下标访问了
str[0], str[0..<str.count], str[0...1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// MARK: - 扩展系统的String类, 支持整型下标访问
extension String {
fileprivate subscript(i: Int) -> Character {
get {
return self[self.index(self.startIndex, offsetBy: i)]
}
}
fileprivate subscript(r: Range<Int>) -> Substring {
get {
let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound)
let endIndex = self.index(self.startIndex, offsetBy: r.upperBound)
return self[startIndex..<endIndex]
}
}
fileprivate subscript(r: ClosedRange<Int>) -> Substring {
get {
let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound)
let endIndex = self.index(self.startIndex, offsetBy: r.upperBound)
return self[startIndex...endIndex]
}
}
}

总结

  OC对于字符串的态度就是, 简单的一律使用UTF16来表示, 开发者想要更仔细处理字符, 需要调用更细致的API; Swift的设计则是一律使用人眼可见的符号表示, 想要更深入的处理字符串, 可以选择不同的视图, 这里仁者见仁智者见智, 你更喜欢哪种方式呢?

文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 如何正确处理逐个字符的问题
  4. 4. Swift中String如何处理逐个字符?
  5. 5. 让Swift字符串也支持整型下标访问
  6. 6. 总结