为什么要使用String.Index而不是Int作为String中的Character索引?

时间:2020-04-09 13:28:56

标签: swift swift5

我已经在Swift 5中阅读了有关String Unicode 的文档,但无法理解为什么我们无法从Character获得String为:

let someString = "????"
let oneCharacter = someString[2] // Error

为什么我们应该使用更复杂的方法来获取Character

let strawberryIndex = someString.index(someString.startIndex, offsetBy: 2) // String.Index type
someString[strawberryIndex] // Character(?)

使用String.Index类型有什么意义?

4 个答案:

答案 0 :(得分:2)

首先,您不能将Int用作字符串的索引。该接口需要String.Index。

为什么?我们使用的是Unicode,而不是ASCII。 Swift字符串的单位是一个字符,即“字形簇”。一个字符可以包含多个Unicode代码点,每个Unicode代码点可以包含1到4个字节。

现在,假设您有10兆字节的字符串,并进行了搜索以找到子字符串“ Wysteria”。您是否要返回字符串以哪个字符开头?如果它是字符123,456,那么要再次找到相同的字符串,我们必须从字符串的开头开始,然后分析123,456个字符以找到该子字符串。那是疯狂的低效。

相反,我们得到一个String.Index,它使Swift可以快速定位该子字符串。它很可能是字节偏移,因此可以非常快速地对其进行访问。

现在在该字节偏移量上添加“ 1”是无意义的,因为您不知道第一个字符有多长时间。 (Unicode很有可能具有另一个等于ASCII'W'的字符)。因此,您需要调用一个返回下一个字符的索引的函数。

您可以编写代码以返回字符串中的第二个字符。要返回百万分之一的角色需要花费大量时间。 Swift不允许您执行效率极低的事情。

答案 1 :(得分:2)

由于多个原因,Swift抽象了字符串索引。据我所知,主要目的是使人们 stop 认为自己只是整数。在表面之下,它们是,但是它们的行为与人们最初的期望背道而驰。

ASCII作为“默认”

我们对String编码的期望通常以英语为中心。 ASCII通常是人们开始学习的第一个字符编码,并且通常以某种借口说它在某种程度上是最受欢迎的或最标准的,等等。

问题是,大多数用户不是美国人。他们是西欧人,他们的拉丁字母需要很多不同的口音,或者东欧人想要西里尔字母,或者中文用户具有很多不同的字符(over 74,000!,他们需要能够书写)。从来都不打算成为对所有语言进行编码的国际标准。美国标准协会创建了ASCII以对与美国市场相关的字符进行编码。其他国家也根据自己的需要进行了字符编码。

Unicode的出现

在与计算机的国际通信变得更加流行之前,已经使区域字符编码起作用。这些零散的字符编码不能互操作,从而导致各种乱码文本和用户混乱。需要有一个新的标准来统一它们,并允许在世界范围内进行标准化编码。

因此,Unicode被发明为统治一切的一环。单个代码表包含所有语言的所有字符,并为将来的扩展留有足够的空间。

每个字符1个字节

在ASCII中,可能有127个字符。字符串中的每个字符都被编码为单个8位字节。这意味着对于一个n字符串,您正好有n个字节。像任何数组下标一样,下标以获得第i个字符是简单的指针算法问题。

address_of_element_i =基本地址+(size_of_each_element * i)

size_of_each_element仅是1(字节),这进一步减少到base_address + i。这确实非常快,而且有效。

许多字符(大多数?)编程语言的标准库中,这种每字符1个字节的ASCII质量使API设计知道了字符串类型。即使ASCII是“默认”编码的错误选择(已经有几十年的历史了),但到Unicode普遍存在时,损害已经造成了。

扩展的字素簇

用户认为是字符的字符在Unicode中称为“扩展字素簇”。它们是基本字符,可以选择后面跟任意数量的连续字符。假设许多语言都是基于“ 1字符等于1字节”这一假设来实现的。

字符认为字节是在Unicode世界中被破坏。不是“哦,它已经足够好了,我们在扩展到国际市场时会担心它”,但绝对是完全不可行的。大多数用户不会说英语。英文用户使用表情符号。从ASCII建立的假设不再起作用。以Python 2.7为例,它可以正常工作:

>>> s = "Hello, World!"
>>> print(s)
Hello, World!
>>> print(s[7]) 
W

这不是:

>>> s = "????"
>>> print(s)
????
>>> print([2])
[2]
>>> print(s[2])
�

在Python 3中,引入了一项重大更改:索引现在表示代码点,而不是字节。因此,现在上面的代码“按预期”工作,打印?。但这还不够。多代码点代码仍被破坏,例如:

>>> s = "A?‍?‍?‍?Z"
>>> print(s[0])
A
>>> print(s[1])
?
>>> print(s[2]) # Zero width joiner
 ‍
>>> print(s[3])
?
>>> print(s[4])
 ‍
>>> print(s[5])
?
>>> print(s[6])
 ‍
>>> print(s[7])
?
>>> print(s[8])
Z
>>> print(s[9]) # Last index
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: string index out of range

Swift可以轻松处理此问题:

  1> let s = "A?‍?‍?‍?Z"
s: String = "A?‍?‍?‍?Z"
  2> s[s.index(s.startIndex, offsetBy: +0)]
$R0: Character = "A"
  3> s[s.index(s.startIndex, offsetBy: +1)]
$R1: Character = "?‍?‍?‍?"
  4> s[s.index(s.startIndex, offsetBy: +2)]
$R2: Character = "Z"

权衡

按字符下标在Unicode中很慢。从头开始,您被迫沿字符串移动,在行进时应用打破音素的规则,直到达到所需的计数为止。这是一个O(n)进程,与ASCII情况下的O(1)不同。

如果此代码隐藏在下标运算符后面,则代码如下:

for i in 0..<str.count {
    print(str[i])
}

可能看起来就像O(str.count)(毕竟“只有一个for循环”,对吗?!),但实际上是O(str.count^2),因为每个{{1} }操作隐藏了字符串的线性遍历,这种遍历是一遍又一遍。

Swift String API

Swift的String API试图迫使人们远离直接索引,而转向不涉及手动索引的替代模式,例如:

  1. str[i] / String.prefix用于切掉字符串的开头或结尾以获取切片
  2. 使用String.suffix转换字符串中的所有字符
  3. 并使用其他内置函数进行大写,小写,反转,修整等。

Swift的String API尚未完全完成。有很多改善人体工程学的愿望/意图。

但是,人们习惯于编写的许多字符串处理代码完全是错误的。他们可能从未注意到,因为他们从未尝试过将其用于外语或表情符号。 String试图默认情况下是正确的,并且很难犯国际化错误。

答案 2 :(得分:1)

答案 3 :(得分:1)

从其他人(和How does String.Index work in Swift)提供的链接/信息中可以看出,这与性能有关。

RandomAccessCollection确保“可以在O(1)时间内移动索引任何距离并测量索引之间的距离”。字符串不能做到这一点。

您可以执行此操作,它可以工作,但是会破坏合同。

extension RandomAccessCollection {
  subscript(position: Int) -> Element {
    self[index(startIndex, offsetBy: position)]
  }
}
extension Substring: RandomAccessCollection { }
extension String: RandomAccessCollection { }
"????"[2] // "?"

但是,我建议这样!

public extension Collection {
  /// - Complexity: O(`position`)
  subscript(startIndexOffsetBy position: Int) -> Element {
    self[index(startIndex, offsetBy: position)]
  }
}
"????"[startIndexOffsetBy: 2]