Swift中的String.Index如何工作?

148

我一直在使用Swift 3更新我的旧代码和答案,但当我尝试处理Swift字符串和索引时,这使我很苦恼。

具体来说,我正在尝试以下内容:

let str = "Hello, playground"
let prefixRange = str.startIndex..<str.startIndex.advancedBy(5) // error

第二行代码导致以下错误:

'advancedBy'不可用:要将索引向前移动n步,请在生成该索引的CharacterView实例上调用'index(_:offsetBy :)'

我发现String有以下方法。

str.index(after: String.Index)
str.index(before: String.Index)
str.index(String.Index, offsetBy: String.IndexDistance)
str.index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

这些一开始真的让我很困惑,所以我开始尝试操作它们,直到我明白了它们的用法。下面我会添加一个回答来展示它们是如何被使用的。

5个回答

381

在此输入图片描述

以下所有示例均使用:

var str = "Hello, playground"

startIndexendIndex

  • startIndex是第一个字符的索引。
  • endIndex是最后一个字符之后的索引。

示例

// character
str[str.startIndex] // H
str[str.endIndex]   // error: after last character

// range
let range = str.startIndex..<str.endIndex
str[range]  // "Hello, playground"

随着Swift 4的单侧范围,范围可以简化为以下形式之一。

let range = str.startIndex...
let range = ..<str.endIndex

为了清晰起见,我将在以下示例中使用完整形式,但为了易读性,您可能希望在代码中使用单侧范围。

after

即: index(after: String.Index)

  • after 指的是给定索引之后紧随的字符的索引。

示例

// character
let index = str.index(after: str.startIndex)
str[index]  // "e"

// range
let range = str.index(after: str.startIndex)..<str.endIndex
str[range]  // "ello, playground"

before

如: index(before: String.Index)

  • before 指给定索引之前的字符的索引。

示例

// character
let index = str.index(before: str.endIndex)
str[index]  // d

// range
let range = str.startIndex..<str.index(before: str.endIndex)
str[range]  // Hello, playgroun

offsetBy

例如:index(String.Index, offsetBy: String.IndexDistance)

  • offsetBy 值可以是正数或负数,并从给定的索引开始。虽然其类型为String.IndexDistance,但您也可以给它一个Int

示例

// character
let index = str.index(str.startIndex, offsetBy: 7)
str[index]  // p

// range
let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let range = start..<end
str[range]  // play

limitedBy

例如: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)

  • limitedBy 用于确保偏移量不会导致索引越界。它是一个限制性索引。由于偏移量可能超过限制,因此该方法返回一个Optional。如果索引越界,则返回nil

例子:

// character
if let index = str.index(str.startIndex, offsetBy: 7, limitedBy: str.endIndex) {
    str[index]  // p
}
如果偏移量是77而不是7,那么if语句会被跳过。

为什么需要String.Index?

对于字符串来说,使用一个Int类型的索引会更容易。但Swift中的字符在底层并不都是相同的长度。一个单独的Swift字符可能由一个、两个甚至更多的Unicode码点组成。因此,每个唯一的字符串必须计算其字符的索引。

可以通过Int类型的索引扩展来隐藏这种复杂性,但我不愿意这样做。了解实际发生的事情是很好的。


28
为什么 startIndex 会是除了0以外的其他值? - Robo Robok
32
由于Swift使用由“字形簇”组成的Unicode字符,所以它不使用整数表示索引位置。比如说你的第一个字符是é,它实际上由e\u{301}的Unicode表示组成。如果你使用索引0,你将得到e或重音符号(“grave”)字符之一,而不是组成é的整个簇。使用startIndex可以确保您获得任何字符的完整字形簇。 - leanne
3
тюе Swift 4.0 СИГ№╝їТ»ЈСИф Unicode тГЌугджЃйТїЅ 1 У«Ау«ЌсђѓСЙІтдѓ№╝џ"РђЇ".count // уј░тюе№╝џ1№╝їС╣ІтЅЇ№╝џ2 - selva
3
除了构建虚拟字符串并在其上使用.index方法之外,如何从整数构造String.Index?我不知道是否有遗漏什么,但文档中没有说明。 - sudo
5
在使用整数构建String.Index时,你需要小心,因为每个Swift的Character都不一定等于你用一个整数所表示的意思。尽管如此,你可以将整数传入offsetBy参数来创建一个String.Index。但是如果你没有一个字符串,则无法构造一个String.Index(因为Swift只有在了解字符串中前面的字符后才能计算索引)。如果你改变了字符串,那么你必须重新计算索引。你不能在两个不同的字符串上使用相同的String.Index - Suragch
显示剩余4条评论

1
如果你想隐藏这些函数的复杂性,你可以使用这个扩展。
let str = " Hello"
str[0] //

extension String {
    subscript(_ index: Int) -> Character? {
        guard index >= 0, index < self.count else {
            return nil
        }

        return self[self.index(self.startIndex, offsetBy: index)]
    }
}

0

我很感激这个问题以及其中的所有信息。当涉及到String.Index时,我有一个问题和一个答案。

我正在尝试查看是否有一种O(1)的方法来访问字符串中的子字符串(或字符),因为如果您查看索引函数的定义,string.index(startIndex,offsetBy:1)的速度是O(n)。当然,我们可以做类似以下的事情:

let characterArray = Array(string)

然后可以访问characterArray中的任何位置,但是这种方法的空间复杂度为n = 字符串长度,O(n),因此有点浪费空间。

我在Xcode中查看了Swift.String文档,发现有一个名为Index的公共结构体。我们可以这样初始化:

let index = String.Index(encodedOffset: 0)

然后,只需像这样访问或打印我们的字符串对象中的任何索引:

print(string[index])

注意:小心不要越界

这个方法可行,很好,但是用这种方式运行时和空间复杂度如何?有没有更好的方法?


1
String.Index(encodedOffset: 0) 给你基于 UTF-16 编码的索引 - 所以它在假设你的字符都是 UTF-16 的情况下可以在 O(1) 的时间内运行。它在 Swift 5 中被弃用,替换为更明确的 (String.Index(utf16Offset: l, in: s))。在我看来,这应该是一个问题,而不是一个答案,例如“ String.Index(encodedOffset: 0) 是什么,为什么它的运行时间是 O(1)?” - tanz

-2
func change(string: inout String) {

    var character: Character = .normal

    enum Character {
        case space
        case newLine
        case normal
    }

    for i in stride(from: string.count - 1, through: 0, by: -1) {
        // first get index
        let index: String.Index?
        if i != 0 {
            index = string.index(after: string.index(string.startIndex, offsetBy: i - 1))
        } else {
            index = string.startIndex
        }

        if string[index!] == "\n" {

            if character != .normal {

                if character == .newLine {
                    string.remove(at: index!)
                } else if character == .space {
                    let number = string.index(after: string.index(string.startIndex, offsetBy: i))
                    if string[number] == " " {
                        string.remove(at: number)
                    }
                    character = .newLine
                }

            } else {
                character = .newLine
            }

        } else if string[index!] == " " {

            if character != .normal {

                string.remove(at: index!)

            } else {
                character = .space
            }

        } else {

            character = .normal

        }

    }

    // startIndex
    guard string.count > 0 else { return }
    if string[string.startIndex] == "\n" || string[string.startIndex] == " " {
        string.remove(at: string.startIndex)
    }

    // endIndex - here is a little more complicated!
    guard string.count > 0 else { return }
    let index = string.index(before: string.endIndex)
    if string[index] == "\n" || string[index] == " " {
        string.remove(at: index)
    }

}

-8
在tableViewController中创建一个UITextView。我使用了函数:textViewDidChange,然后检查了回车键输入。 如果它检测到回车键输入,则删除回车键的输入并关闭键盘。
func textViewDidChange(_ textView: UITextView) {
    tableView.beginUpdates()
    if textView.text.contains("\n"){
        textView.text.remove(at: textView.text.index(before: textView.text.endIndex))
        textView.resignFirstResponder()
    }
    tableView.endUpdates()
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接