跳到主要内容

用代码示例解释 Swift 中的 Ranges

· 阅读需 7 分钟
GoSwiftUI
goswiftui.com

Swift 中的范围(Ranges)允许我们选择字符串、集合和其他类型的部分内容。它们是我们从 Objective-C 中熟悉的 NSRange 的 Swift 变体,尽管在使用上有所不同,我将在本篇博文中解释清楚。

通过使用范围运算符,范围使我们能够编写优雅的 Swift 代码。你第一次使用范围可能是因为你需要从字符串中选择一段字符,但是实际上你可以做的远不止这些!

Swift 中的范围类型

Swift 中有多种类型的范围可供我们使用。使用范围运算符是与它们交互的最简单方法。让我们来看看 Swift 中可用的不同类型。

闭合范围运算符 a...b

let range: ClosedRange = 0...10
print(range.first!) // 0
print(range.last!) // 10

使用 a...b 的闭合范围运算符定义了一个包含 a 和 b 的范围,其中 a 不能大于 b。

闭合范围运算符在你想要使用所有值的情况下非常有用。例如,如果你想迭代遍历一个集合的所有元素:

let names = ["Antoine", "Maaike", "Jaap"]
for index in 0...2 {
print("Name \(index) is \(names[index])")
}
// 输出:
// Name 0 is Antoine
// Name 1 is Maaike
// Name 2 is Jaap

不过,要从集合中选择元素,需要使用 CountableClosedRange 类型:

let names = ["Antoine", "Maaike", "Jaap"]
let range: CountableClosedRange = 0...2
print(names[range]) // ["Antoine", "Maaike", "Jaap"]

当然,Swift 聪明到足够自动检测到可数范围的类型。因此,你可以这样写上面的代码:

let names = ["Antoine", "Maaike", "Jaap"]
print(names[0...2]) // ["Antoine", "Maaike", "Jaap"]

半开范围运算符 a..<b

swiftCopy code
let range: Range = 0..<10
print(range.first!) // 0
print(range.last!) // 9

半开范围定义了从 a 到 b 的范围,但不包括 b。它被称为半开,因为它包含第一个值但不包含最后一个值。与闭合范围一样,a 的值不能大于 b。

半开范围运算符可用于迭代遍历从零开始的列表,如 Swift 中的数组和集合,其中你希望迭代遍历列表的长度但不包括最后一个元素。这与前面的代码示例相同,但现在我们可以利用

count 属性:

let names = ["Antoine", "Maaike", "Jaap"]
print(names[0..<names.count]) // ["Antoine", "Maaike", "Jaap"]

如果我们使用闭合运算符做同样的操作,将会遇到以下错误:

Fatal error: Array index is out of range

单侧范围运算符 a...

单侧范围运算符只定义了范围的一侧界限,例如 a......b。单侧范围尽可能地在一个方向上延伸,例如,从数组的开头到索引 2,包括数组的所有元素:

let names = ["Antoine", "Maaike", "Jaap"]
print(names[...2]) // ["Antoine", "Maaike", "Jaap"]

或者从索引 1 开始到数组的末尾的所有元素:

let names = ["Antoine", "Maaike", "Jaap"]
print(names[1...]) // ["Maaike", "Jaap"]

单侧范围可以用于迭代,但只有在与起始值 a... 一起使用时才能这样做。否则,循环的起始位置不明确。使用单侧范围进行迭代需要手动检查循环何时应该结束,否则它将无限继续下去。

let names = ["Antoine", "Maaike", "Jaap"]
let neededNames = 2
var collectedNames: [String] = []
for index in 0... {
guard collectedNames.count != neededNames else { break }
collectedNames.append(names[index])
}
print(collectedNames) // ["Antoine", "Maaike"]

在 Swift 中将范围转换为 NSRange

迟早你会遇到一个问题,即想要将 Range 转换为 NSRange 类型。例如,如果你正在使用 NSAttributedString,并想要将属性应用于特定的范围。在下面的示例中,我们想要将橙色应用于标题中的“Swift”:

let title = "A Swift Blog"
let range = title.range(of: "Swift")
let attributedString = NSMutableAttributedString(string: title)
attributedString.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.orange], range: range) // 无法将类型为 'Range<String.Index>?' 的值转换为预期的参数类型 'NSRange'(即 '_NSRange')

由于 Range 无法转换为 NSRange,我们遇到了以下错误:

无法将类型 'Range?' 的值转换为预期的参数类型 'NSRange'(即 '_NSRange')

我们可以通过使用 NSRange 提供的方便的初始化器来解决这个问题,该初始化器接受一个 Swift Range

let convertedRange = NSRange(range, in: title)

最终的代码如下所示:

let title = "A Swift Blog"
let range = title.range(of: "Swift")!
let convertedRange = NSRange(range, in: title)
let attributedString = NSMutableAttributedString(string: title)
attributedString.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.orange], range: convertedRange)
print(attributedString)
// A {
// }Swift{
// NSColor = "UIExtendedSRGBColorSpace 1 0.5 0 1";
// } Blog{
// }

范围和字符串

字符串和范围有一些特殊之处。你可能知道,String 是字符的集合。然而,并非每个字符都具有相同的大小。我们可以通过使用包含表情符号的 NSRangeNSString 来演示这一点:

let emojiText: NSString = "🚀launcher"
print(emojiText.substring(with: NSRange(location: 0, length: 7)))
// 期望输出:🚀launch
// 实际输出:🚀launc
// 缺少了 'h'

如你所见,火箭表情符号的长度超过一个字符。因此,我们的子字符串没有返回预期的结果,缺少了 'h'。

使用 String 索引处理

解决这个问题的方法是使用 Range<String.Index> 而不是 Range<Int>String.Index 考虑了字符的实际大小。我们只能使用半开范围(Range)的形式,因为这是 String 下标所要求的。

let emojiText = "🚀launcher"
let endIndex = emojiText.index(emojiText.startIndex, offsetBy: 7)
let range: Range<String.Index> = emojiText.startIndex..<endIndex
print(emojiText[range]) // 🚀launch