下标的良好行为

Good behavior for subscript

我正在为 String 创建一个扩展,我正在尝试确定下标运算符的 proper/expected/good 行为。目前,我有这个:

// Will crash on 0 length strings
subscript(kIndex: Int) -> Character {
    var index = kIndex
    index = index < 0 ? 0 : index
    index = index >= self.length ? self.length-1 : index
    let i = self.startIndex.advancedBy(index)
    return self.characters[i]
}

这会导致字符串范围之外的所有值都被限制在字符串的边缘。虽然这减少了将错误的索引传递给下标而导致的崩溃,但感觉这不是正确的做法。如果索引超出范围,我无法从下标抛出异常并且不检查下标会导致 BAD_INSTRUCTION 错误。我能想到的唯一其他选择是 return 一个可选的,但这看起来很尴尬。权衡选项,我所拥有的似乎是最合理的,但我认为使用它的任何人都不会期望 return 有效结果的错误索引。

所以,我的问题是:下标运算符的 "standard" 预期行为是什么?return 从无效索引 acceptable/appropriate 中获取有效元素?谢谢

如果您要在 String 上实现下标,您可能需要首先考虑为什么标准库选择不这样做。

当你调用 self.startIndex.advancedBy(index) 时,你实际上是在写这样的东西:

var i = self.startIndex
while i < index { i = i.successor() }

这是因为 String.CharacterView.Index 不是随机访问索引类型。请参阅 advancedBy 上的文档。字符串索引不是随机访问的,因为字符串中的每个 Character 可能是字符串底层存储中任意数量的字节——你不能只通过跳转获取字符 n n * characterSize 像使用 C 字符串一样进入存储。

因此,如果要使用下标运算符遍历字符串中的字符:

for i in 0..<string.characters.count {
    doSomethingWith(string[i])
}

...你会有一个循环 看起来 就像它在线性时间内运行一样,因为它看起来就像一个数组迭代 - 每次通过循环都应该采用相同的时间量,因为每个都只是递增 i 并使用恒定时间访问来获得 string[i],对吗?没有。 advancedBy 调用首先通过循环调用 successor 一次,下一次调用它两次,依此类推......如果你的字符串有 n 个字符,最后一次通过循环调用 successor n 次(即使这会生成一个结果,该结果在上一次调用 successor [ 时通过循环使用) =63=]n-1次)。换句话说,您刚刚进行了一个看起来像 O(n) 操作的 O(n2) 操作,为其他使用您的代码的人留下了性能成本炸弹。

这是完全支持 Unicode 的字符串库的价格。


无论如何,回答你的实际问题——下标和域检查有两种思想流派:

  • 有一个可选的 return 类型:func subscript(index: Index) -> Element?

    当客户端没有明智的方法来检查索引是否有效而不执行与查找相同的工作时,这是有意义的——例如对于字典,找出 if 给定键的值与找出 what 键的值是相同的。

  • 要求索引有效,否则报致命错误

    通常情况下,您的 API 的客户端可以并且应该在访问下标之前检查有效性。这就是 Swift 数组的作用,因为数组知道它们的计数,您不需要查看数组来查看索引是否有效。

    对此的规范测试是precondition:例如

    func subscript(index: Index) -> Element {
        precondition(isValid(index), "index must be valid")
        // ... do lookup ...
    }
    

    (这里,isValid 是一些特定于您的 class 的操作,用于验证索引——例如确保它 > 0 且 < 计数。)

在几乎任何用例中,在索引错误的情况下 Swift 到 return 一个 "real" 值都不是惯用的,也不适合 return 哨兵值 — 将带内值与哨兵分开是 Swift 具有可选值的原因。

其中哪一个更适合您的用例是...好吧,由于您的用例存在问题,所以有点洗。如果您 precondition 该索引 < 计数,您仍然会产生 O(n) 成本只是为了检查它(因为 String 必须检查其内容以找出哪些字节序列构成它之前的每个字符知道它有多少个字符)。如果您将 return 类型设为可选,并且在调用 advancedBycount 后 return 为零,您仍然会产生 O(n) 成本。