【问题标题】:How does String.Index work in SwiftString.Index 如何在 Swift 中工作
【发布时间】:2017-02-02 06:26:37
【问题描述】:

我一直在使用 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)

一开始我真的很困惑,所以我开始玩弄它们,直到我理解它们。我在下面添加一个答案来说明它们是如何使用的。

【问题讨论】:

    标签: swift string indexing


    【解决方案1】:

    以下所有示例都使用

    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 的 one-sided ranges,范围可以简化为以下形式之一。

    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 索引会更容易很多。您必须为每个字符串创建一个新的String.Index 的原因是 Swift 中的字符在引擎盖下的长度并不完全相同。单个 Swift 字符可能由一个、两个甚至更多的 Unicode 代码点组成。因此,每个唯一的字符串都必须计算其字符的索引。

    可以将这种复杂性隐藏在 Int 索引扩展后面,但我不愿意这样做。提醒一下实际发生的事情是件好事。

    【讨论】:

    • 为什么startIndex 不是 0?
    • @RoboRobok:因为 Swift 使用由“字素簇”组成的 Unicode 字符,所以 Swift 不使用整数来表示索引位置。假设您的第一个字符是é。它实际上是由e 加上\u{301} Unicode 表示。如果您使用零索引,您将获得e 或重音(“grave”)字符,而不是构成é 的整个簇。使用startIndex 可确保您获得任何字符的整个字素簇。
    • 在 Swift 4.0 中,每个 Unicode 字符都按 1 计数。例如:"?‍?".count // 现在:1,之前:2
    • 除了构建一个虚拟字符串并在其上使用.index 方法之外,如何从整数构造String.Index?我不知道我是否遗漏了什么,但文档什么也没说。
    • @sudo,在构造带有整数的String.Index 时必须小心一点,因为每个 Swift Character 不一定等于整数的含义。也就是说,您可以将整数传递给offsetBy 参数以创建String.Index。但是,如果您没有String,则无法构造String.Index(因为Swift 只能在知道字符串中前面的字符是什么时才能计算索引)。如果更改字符串,则必须重新计算索引。您不能在两个不同的字符串上使用相同的 String.Index
    【解决方案2】:

    我很欣赏这个问题以及所有相关信息。当涉及到 String.Index 时,我想到了一个问题和一个答案。

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

    let characterArray = Array(string)
    

    然后访问 characterArray 中的任何位置,但是 SPACE 复杂度是 n = 字符串长度,O(n) 所以有点浪费空间。

    我在 Xcode 中查看 Swift.String 文档,并且有一个名为 Index 的冻结公共结构。我们可以初始化为:

    let index = String.Index(encodedOffset: 0)
    

    然后简单地访问或打印我们的 String 对象中的任何索引:

    print(string[index])
    

    注意:注意不要越界`

    这很好用,但这样做的运行时间和空间复杂度是多少?好点了吗?

    【讨论】:

    • String.Index(encodedOffset: 0) 为您提供基于 UTF-16 编码的索引 - 因此它在 O(1) 中工作,假设您的字符都是 UTF-16。它在 Swift 5 中被弃用,取而代之的是更明确的 (String.Index(utf16Offset: l, in: s))。在我看来,这应该是一个问题而不是一个答案,例如“String.Index(encodedOffset: 0) 是做什么的,为什么它的运行时间是 O(1)?”
    【解决方案3】:
    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)
        }
    
    }
    

    【讨论】:

      【解决方案4】:

      在 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()
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-10-26
        • 1970-01-01
        • 1970-01-01
        • 2016-04-04
        • 1970-01-01
        相关资源
        最近更新 更多