为什么我们应该使用 String.Index 而不是 Int 作为 Character in String 的索引?
Why should we use String.Index instead of Int as index of Character in String?
我在 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 有什么意义?
Apple 不允许使用整数下标字符串。
看: Get nth character of a string in Swift programming language
首先,您不能使用 Int 作为字符串的索引。该接口需要 String.Index。
为什么?我们使用的是 Unicode,而不是 ASCII。 Swift个字符串的单位是Character,即"Grapheme Cluster"。一个字符可以由多个 Unicode 码位组成,每个 Unicode 码位可以由 1 到 4 个字节组成。
现在假设您有一个 10 兆字节的字符串,并进行了搜索以找到子字符串 "Wysteria"。您要 return 字符串以哪个字符编号开头?如果是字符 123,456 那么要再次找到相同的字符串,我们必须从字符串的开头开始,分析 123,456 个字符以找到该子字符串。那是非常低效的。
相反,我们得到一个 String.Index,它允许 Swift 快速定位该子字符串。它很可能是字节偏移量,因此可以非常快速地访问它。
现在在那个字节偏移量上加“1”是无稽之谈,因为你不知道第一个字符有多长。 (很可能 Unicode 有另一个等于 ASCII 'W' 的字符)。所以你需要调用一个 returns 下一个字符索引的函数。
您可以编写 return 字符串中第二个字符的代码。 return 百万分之一的字符需要大量时间。 Swift 不允许您做效率极低的事情。
正如您从 links/information 其他人提供的(和 )中看到的那样,它与性能有关。
RandomAccessCollection 保证它 "can move indices any distance and measure the distance between indices in O(1) time." 字符串不能那样做。
你可以这样做,它会起作用,但它会破坏契约。
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]
Swift 出于多种原因对字符串索引进行抽象。据我所知,主要目的是让人们 stop 认为他们只是整数。在引擎盖下,它们是,但它们的行为与人们最初的期望背道而驰。
ASCII 作为 "default"
我们对字符串编码的期望通常以英语为中心。 ASCII 通常是人们学习的第一个字符编码,并且通常会以某种借口说它在某种程度上是最流行或最标准的,等等。
问题是,大多数用户不是美国人。他们是西欧人,他们的拉丁字母需要很多不同的重音,或者是东欧人,他们想要西里尔字母,或者是中国用户,他们有一堆不同的字符(over 74,000! 他们需要能够书写。ASCII从来没有打算成为编码所有语言的国际标准。美国标准协会创建了 ASCII 来编码与美国市场相关的字符。其他国家根据自己的需要制定了自己的字符编码。
Unicode 的出现
在使用计算机进行国际交流变得更加普遍之前,区域字符编码一直有效。这些零散的字符编码无法相互操作,导致各种乱码文本和用户 confusion.There 需要一个新的标准来统一它们并允许全球范围内的标准化编码。
因此,Unicode 被发明为统一规则。一个代码 table,包含所有语言的所有字符,并有足够的空间供将来扩展。
每个字符 1 个字节
在 ASCII 中,有 127 个可能的字符。字符串中的每个字符都被编码为一个 8 位字节。这意味着对于 n
个字符串,您恰好有 n
个字节。获取第 i
个字符的下标是一个简单的指针运算问题,就像任何数组下标一样。:
address_of_element_i = base_address + (size_of_each_element * i)
由于 size_of_each_element
只是 1(字节),这进一步减少到 base_address + i
。这真的很快,而且很有效。
ASCII 的这种每个字符 1 个字节的质量为许多(大多数?)编程语言的标准库中的 API 字符串类型设计提供了依据。尽管 ASCII 是 "default" 编码的错误选择(几十年来一直如此),但当 Unicode 变得无处不在时,损害已经造成。
扩展字素簇
用户认为的字符在 Unicode 中称为 "extended grapheme clusters"。它们是一个基本字符,可以选择后跟任意数量的连续字符。这打破了许多语言赖以建立的“1 个字符是 1 个字节”的假设。
将字符视为字节的想法在 Unicode 世界中已被打破。不是 "oh it's good enough, we'll worry about it when we expand to international markets",但绝对和完全行不通。大多数用户不会说英语。英语用户使用表情符号。从 ASCII 建立的假设不再适用。以 Python 2.7 为例,这很好用:
>>> s = "Hello, World!"
>>> print(s)
Hello, World!
>>> print(s[7])
W
而这不是:
>>> s = ""
>>> print(s)
>>> print([2])
[2]
>>> print(s[2])
�
在 Python3 中,引入了一个重大变化:索引现在表示代码点,而不是字节。所以现在上面的代码工作 "as expected",打印 </code>。但这仍然不够。多码点代码还是断的,例如:</p>
<pre><code>>>> s = "AZ"
>>> 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 = "AZ"
s: String = "AZ"
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)
(毕竟"there's only one for loop",对吧?!),但实际上是O(str.count^2)
,因为每个str[i]
操作隐藏了对字符串的线性遍历,这种情况一遍又一遍地发生。
Swift字符串API
Swift 的字符串 API 试图迫使人们远离直接索引,而转向不涉及手动索引的替代模式,例如:
String.prefix
/String.suffix
用于切断字符串的开头或结尾以获得切片
- 使用
String.map
转换字符串中的所有字符
- 并使用其他内置函数进行大写、小写、反转、修剪等
Swift 的字符串 API 尚未完全完成。有很多 desire/intent 可以改善其人体工程学。
但是,人们习惯编写的大部分字符串处理代码都是完全错误的。他们可能从来没有注意到,因为他们从来没有尝试过用外语或表情符号来使用它。 String 试图在默认情况下是正确的,并且很难犯国际化错误。
我在 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 有什么意义?
Apple 不允许使用整数下标字符串。
看: Get nth character of a string in Swift programming language
首先,您不能使用 Int 作为字符串的索引。该接口需要 String.Index。
为什么?我们使用的是 Unicode,而不是 ASCII。 Swift个字符串的单位是Character,即"Grapheme Cluster"。一个字符可以由多个 Unicode 码位组成,每个 Unicode 码位可以由 1 到 4 个字节组成。
现在假设您有一个 10 兆字节的字符串,并进行了搜索以找到子字符串 "Wysteria"。您要 return 字符串以哪个字符编号开头?如果是字符 123,456 那么要再次找到相同的字符串,我们必须从字符串的开头开始,分析 123,456 个字符以找到该子字符串。那是非常低效的。
相反,我们得到一个 String.Index,它允许 Swift 快速定位该子字符串。它很可能是字节偏移量,因此可以非常快速地访问它。
现在在那个字节偏移量上加“1”是无稽之谈,因为你不知道第一个字符有多长。 (很可能 Unicode 有另一个等于 ASCII 'W' 的字符)。所以你需要调用一个 returns 下一个字符索引的函数。
您可以编写 return 字符串中第二个字符的代码。 return 百万分之一的字符需要大量时间。 Swift 不允许您做效率极低的事情。
正如您从 links/information 其他人提供的(和
RandomAccessCollection 保证它 "can move indices any distance and measure the distance between indices in O(1) time." 字符串不能那样做。
你可以这样做,它会起作用,但它会破坏契约。
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]
Swift 出于多种原因对字符串索引进行抽象。据我所知,主要目的是让人们 stop 认为他们只是整数。在引擎盖下,它们是,但它们的行为与人们最初的期望背道而驰。
ASCII 作为 "default"
我们对字符串编码的期望通常以英语为中心。 ASCII 通常是人们学习的第一个字符编码,并且通常会以某种借口说它在某种程度上是最流行或最标准的,等等。
问题是,大多数用户不是美国人。他们是西欧人,他们的拉丁字母需要很多不同的重音,或者是东欧人,他们想要西里尔字母,或者是中国用户,他们有一堆不同的字符(over 74,000! 他们需要能够书写。ASCII从来没有打算成为编码所有语言的国际标准。美国标准协会创建了 ASCII 来编码与美国市场相关的字符。其他国家根据自己的需要制定了自己的字符编码。
Unicode 的出现
在使用计算机进行国际交流变得更加普遍之前,区域字符编码一直有效。这些零散的字符编码无法相互操作,导致各种乱码文本和用户 confusion.There 需要一个新的标准来统一它们并允许全球范围内的标准化编码。
因此,Unicode 被发明为统一规则。一个代码 table,包含所有语言的所有字符,并有足够的空间供将来扩展。
每个字符 1 个字节
在 ASCII 中,有 127 个可能的字符。字符串中的每个字符都被编码为一个 8 位字节。这意味着对于 n
个字符串,您恰好有 n
个字节。获取第 i
个字符的下标是一个简单的指针运算问题,就像任何数组下标一样。:
address_of_element_i = base_address + (size_of_each_element * i)
由于 size_of_each_element
只是 1(字节),这进一步减少到 base_address + i
。这真的很快,而且很有效。
ASCII 的这种每个字符 1 个字节的质量为许多(大多数?)编程语言的标准库中的 API 字符串类型设计提供了依据。尽管 ASCII 是 "default" 编码的错误选择(几十年来一直如此),但当 Unicode 变得无处不在时,损害已经造成。
扩展字素簇
用户认为的字符在 Unicode 中称为 "extended grapheme clusters"。它们是一个基本字符,可以选择后跟任意数量的连续字符。这打破了许多语言赖以建立的“1 个字符是 1 个字节”的假设。
将字符视为字节的想法在 Unicode 世界中已被打破。不是 "oh it's good enough, we'll worry about it when we expand to international markets",但绝对和完全行不通。大多数用户不会说英语。英语用户使用表情符号。从 ASCII 建立的假设不再适用。以 Python 2.7 为例,这很好用:
>>> s = "Hello, World!"
>>> print(s)
Hello, World!
>>> print(s[7])
W
而这不是:
>>> s = ""
>>> print(s)
>>> print([2])
[2]
>>> print(s[2])
�
在 Python3 中,引入了一个重大变化:索引现在表示代码点,而不是字节。所以现在上面的代码工作 "as expected",打印 </code>。但这仍然不够。多码点代码还是断的,例如:</p>
<pre><code>>>> s = "AZ"
>>> 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 = "AZ"
s: String = "AZ"
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)
(毕竟"there's only one for loop",对吧?!),但实际上是O(str.count^2)
,因为每个str[i]
操作隐藏了对字符串的线性遍历,这种情况一遍又一遍地发生。
Swift字符串API
Swift 的字符串 API 试图迫使人们远离直接索引,而转向不涉及手动索引的替代模式,例如:
String.prefix
/String.suffix
用于切断字符串的开头或结尾以获得切片- 使用
String.map
转换字符串中的所有字符 - 并使用其他内置函数进行大写、小写、反转、修剪等
Swift 的字符串 API 尚未完全完成。有很多 desire/intent 可以改善其人体工程学。
但是,人们习惯编写的大部分字符串处理代码都是完全错误的。他们可能从来没有注意到,因为他们从来没有尝试过用外语或表情符号来使用它。 String 试图在默认情况下是正确的,并且很难犯国际化错误。