【问题标题】:Swift Array - Check if an index existsSwift Array - 检查索引是否存在
【发布时间】:2014-09-22 14:46:11
【问题描述】:

在 Swift 中,有什么方法可以检查数组中是否存在索引而不抛出致命错误?

我希望我能做这样的事情:

let arr: [String] = ["foo", "bar"]
let str: String? = arr[1]
if let str2 = arr[2] as String? {
    // this wouldn't run
    println(str2)
} else {
    // this would be run
}

但我明白了

致命错误:数组索引超出范围

【问题讨论】:

    标签: swift


    【解决方案1】:

    Swift 中的一种优雅方式:

    let isIndexValid = array.indices.contains(index)
    

    【讨论】:

    • 在现实生活中,这无关紧要,但在时间复杂性方面,使用index < array.count 不是更好吗?
    • 如果你想知道速度差异是什么,我用 Xcode 的工具测量了它,它可以忽略不计。 gist.github.com/masonmark/a79bfa1204c957043687f4bdaef0c2ad
    • 只是补充一点为什么这是正确的:如果您在处理 ArraySlice 时应用相同的逻辑,则第一个索引不会是 0,所以 index >= 0 不会是一个足够好的检查。 .indices 在任何情况下都有效。
    • 为此我喜欢 Swift。这是我的用例:为服务构建糟糕的 JSON 结构,所以必须这样做:“attendee3”:names.indices.contains(2)?名称[2]:“”
    • 这是一个很好的答案。为了回答@funct7 对时间复杂度的担忧,Array 的 indices 方法返回的类型是一个范围,而不是整数数组。因此,时间复杂度为 O(1),就像手动检查上限和下限时一样。
    【解决方案2】:

    类型扩展:

    extension Collection {
    
        subscript(optional i: Index) -> Iterator.Element? {
            return self.indices.contains(i) ? self[i] : nil
        }
    
    }
    

    使用它,您可以在将关键字 optional 添加到索引时返回一个可选值,这意味着即使索引超出范围,您的程序也不会崩溃。在您的示例中:

    let arr = ["foo", "bar"]
    let str1 = arr[optional: 1] // --> str1 is now Optional("bar")
    if let str2 = arr[optional: 2] {
        print(str2) // --> this still wouldn't run
    } else {
        print("No string found at that index") // --> this would be printed
    }
    

    【讨论】:

    • 优秀答案? 最重要的是在参数中使用optional 时它是可读的。谢谢!
    • 真棒:D 拯救了我的一天。
    • 这很漂亮
    【解决方案3】:

    只检查索引是否小于数组大小:

    if 2 < arr.count {
        ...
    } else {
        ...
    }
    

    【讨论】:

    • 如果数组大小未知怎么办?
    • @NathanMcKaskle 数组总是知道它包含多少个元素,所以大小不可能是未知的
    • 对不起,我没想到。早晨的咖啡还在进入血液。
    • @NathanMcKaskle 不用担心......有时这种情况会发生在我身上,即使在喝了几杯咖啡之后 ;-)
    • 在某些方面,这是最好的答案,因为它在 O(1) 而不是 O(n) 中运行。
    【解决方案4】:

    添加一些扩展糖:

    extension Collection {
      subscript(safe index: Index) -> Iterator.Element? {
        guard indices.contains(index) else { return nil }
        return self[index]
      }
    }
    if let item = ["a", "b", "c", "d"][safe: 3] { print(item) } // Output: "d"
    // or with guard:
    guard let anotherItem = ["a", "b", "c", "d"][safe: 3] else {return}
    print(anotherItem) // "d"
    

    在结合数组进行if let 样式编码时提高可读性

    【讨论】:

    • 老实说,这是以最大的可读性和清晰度做到这一点的最快捷的方式
    • @barndog 也喜欢。我通常在我开始的任何项目中将其添加为 Sugar。还添加了守卫示例。感谢 swift slack 社区提出了这个问题。
    • 很好的解决方案...但是在您给出的示例中输出将打印“d”...
    • 这应该是 Swift 语言的一部分。索引超出范围的问题不亚于 nil 值。我什至建议使用类似的语法:可能类似于:myArray[?3]。如果 myArray 是可选的,你会得到 myArray?[?3]
    • @Andy Weinstein 你应该把它推荐给苹果?
    【解决方案5】:

    Swift 4 扩展:

    对我来说,我更喜欢喜欢的方法。

    // MARK: - Extension Collection
    
    extension Collection {
    
        /// Get at index object
        ///
        /// - Parameter index: Index of object
        /// - Returns: Element at index or nil
        func get(at index: Index) -> Iterator.Element? {
            return self.indices.contains(index) ? self[index] : nil
        }
    }
    

    感谢@Benno Kress

    【讨论】:

      【解决方案6】:

      您可以用更安全的方式重写它来检查数组的大小,并使用三元条件:

      if let str2 = (arr.count > 2 ? arr[2] : nil) as String?
      

      【讨论】:

      • Antonio 的建议更加透明(和高效) 三元组适合的地方是,如果您有一个现成的值可以使用并完全消除 if,只需留下一个 let 语句。
      • @David Antonio 的建议需要两个 if 语句,而不是原始代码中的一个 if 语句。我的代码用条件运算符替换了第二个if,让您保留一个else,而不是强制使用两个单独的else 块。
      • 我在他的代码中没有看到两个 if 语句,将 let str2 = arr[1] 放入省略号即可。如果您将 OP if let 语句替换为 antonio 的 if 语句,并将赋值移动到内部(或不移动,因为 str2 的唯一用途是打印它,根本不需要,只需将取消引用内联放在 println 中。更不用说三元运算符只是一个晦涩的(对某些人来说:))if / else。如果到达。 count > 2,那么 arr[2] 永远不可能是 nil,那为什么要把它映射到 String 呢?只是这样您就可以应用另一个 if 语句。
      • @David 来自 OP 问题的整个 if 将最终出现在 Antonio 答案的“then”分支中,因此会有两个嵌套的 ifs。我将 OPs 代码视为一个小例子,所以我假设他仍然想要if。我同意你的观点,在他的例子中if 是不必要的。但是话又说回来,整个语句毫无意义,因为 OP 知道数组没有足够的长度,并且它的元素都不是nil,所以他可以删除if 并只保留它的else 块。
      • 不,不会。 Antonio 的 if 替换了 OP 的 if 语句。由于数组类型是 [String] 你知道它永远不能包含 nil,所以不需要检查超出长度的任何内容。
      【解决方案7】:

      断言是否存在数组索引:

      如果您不想添加扩展糖,这种方法非常有用:

      let arr = [1,2,3]
      if let fourthItem = (3 < arr.count ?  arr[3] : nil ) {
           Swift.print("fourthItem:  \(fourthItem)")
      }else if let thirdItem = (2 < arr.count ?  arr[2] : nil) {
           Swift.print("thirdItem:  \(thirdItem)")
      }
      //Output: thirdItem: 3
      

      【讨论】:

        【解决方案8】:
        extension Array {
            func isValidIndex(_ index : Int) -> Bool {
                return index < self.count
            }
        }
        

        let array = ["a","b","c","d"]
        
        func testArrayIndex(_ index : Int) {
        
            guard array.isValidIndex(index) else {
                print("Handle array index Out of bounds here")
                return
            }
        
        }
        

        我可以处理indexOutOfBounds

        【讨论】:

        • 如果索引为负数怎么办?您可能也应该检查一下。
        【解决方案9】:

        Swift 4 和 5 扩展:

        就我而言,我认为这是最安全的解决方案:

        public extension MutableCollection {
            subscript(safe index: Index) -> Element? {
                get {
                    return indices.contains(index) ? self[index] : nil
                }
                set(newValue) {
                    if let newValue = newValue, indices.contains(index) {
                        self[index] = newValue
                    }
                }
            }
        }
        

        例子:

        let array = ["foo", "bar"]
        if let str = array[safe: 1] {
            print(str) // "bar"
        } else {
            print("index out of range")
        }
        

        【讨论】:

          【解决方案10】:

          我相信现有答案可以进一步改进,因为代码库中的多个地方可能需要此功能(重复常见操作时的代码气味)。所以考虑添加我自己的实现,并说明我为什么考虑这种方法(效率良好的 API 设计的重要组成部分,应尽可能首选 只要可读性不会受到太大影响)。除了使用类型本身的方法强制执行良好的面向对象设计之外,我认为协议扩展很棒,我们可以使现有的答案更加更敏捷。限制扩展非常棒,因为您不要创建不使用的代码。使代码更简洁和可扩展通常可以使维护更容易,但有权衡(简洁是我首先想到的)。

          因此,您可以注意,如果您想使用 可重用性的扩展想法,但更喜欢上面引用的 contains 方法,则可以修改这个答案。我试图让这个答案灵活地用于不同的用途。

          TL;DR

          您可以使用更高效的算法(空间和时间)并使用具有通用约束的协议扩展使其可扩展:

          extension Collection where Element: Numeric { // Constrain only to numerical collections i.e Int, CGFloat, Double and NSNumber
            func isIndexValid(index: Index) -> Bool {
              return self.endIndex > index && self.startIndex <= index
            }
          }
          
          // Usage
          
          let checkOne = digits.isIndexValid(index: index)
          let checkTwo = [1,2,3].isIndexValid(index: 2)
          

          深入研究

          效率

          @Manuel 的答案确实非常优雅,但它使用了额外的间接层(请参阅here)。 indices 属性就像 startIndexendIndex 创建的引擎盖下的 CountableRange&lt;Int&gt;,没有这个问题的原因(空间复杂度略高,特别是如果 String 很长)。话虽如此,时间复杂度应该与 endIndexstartIndex 属性之间的直接比较大致相同,因为 N = 2 即使 contains(_:) 是 O(N) 对于 Collections (Ranges只有开始和结束索引的两个属性)。

          为了获得最佳的空间和时间复杂度、更大的可扩展性和稍微长一点的代码,我建议使用以下代码:

          extension Collection {
            func isIndexValid(index: Index) -> Bool {
              return self.endIndex > index && self.startIndex <= index
            }
          }
          

          请注意我是如何使用startIndex 而不是0 - 这是为了支持ArraySlices 和其他SubSequence 类型。这是发布解决方案的另一个动机

          示例用法:

          let check = digits.isIndexValid(index: index)
          

          对于一般的Collections,很难在 Swift 中通过设计创建一个无效的Index,因为 Apple 已将 associatedtype Index 的初始化程序限制在 Collection 上 - 只能从现有的有效 @ 创建987654343@(如startIndex)。

          话虽如此,对Arrays 使用原始Int 索引是很常见的,因为在很多情况下您需要检查随机Array 索引。因此,您可能希望将该方法限制为更少的结构...

          限制方法范围

          您会注意到此解决方案适用于所有 Collection 类型(可扩展性),但只有在您想限制特定应用程序的范围时(例如,如果您不想不想要添加的 String 方法,因为您不需要它)。

          extension Array {
            func isIndexValid(index: Index) -> Bool {
              return self.endIndex > index && self.startIndex <= index
            }
          }
          

          对于Arrays,您不需要明确使用Index 类型:

          let check = [1,2,3].isIndexValid(index: 2)
          

          您可以根据自己的用例随意调整此处的代码,还有许多其他类型的 Collections,例如LazyCollections。您还可以使用通用约束,例如:

          extension Collection where Element: Numeric {
            func isIndexValid(index: Index) -> Bool {
              return self.endIndex > index && self.startIndex <= index
            }
          }
          

          这将范围限制为NumericCollections,但您也可以相反地显式使用String。同样,最好将函数限制为您专门用于避免代码蠕变

          跨不同模块引用方法

          编译器已经应用了多项优化来防止泛型成为一般问题,但是当从单独的模块调用代码时这些不适用。对于这样的情况,使用@inlinable 可以为您带来有趣的性能提升,但代价​​是增加了框架二进制文件的大小。一般来说,如果您真的想提高性能并希望将函数封装在单独的 Xcode 目标中以获得 良好的 SOC,您可以尝试:

          extension Collection where Element: Numeric {
            // Add this signature to the public header of the extensions module as well.
            @inlinable public func isIndexValid(index: Index) -> Bool {
              return self.endIndex > index && self.startIndex <= index
            }
          }
          

          我可以推荐尝试模块化代码库结构,我认为这有助于确保项目中的单一职责(和SOLID)以进行常见操作。我们可以尝试按照here 的步骤进行操作,这就是我们可以使用此优化的地方(尽管要谨慎)。可以为该函数使用该属性,因为编译器操作每个调用站点只添加一行额外的代码,但它可以进一步提高性能,因为没有将方法添加到调用堆栈中(所以没有' t 需要被跟踪)。如果您需要最先进的速度,并且您不介意小的二进制大小增加,这将非常有用。 (-: 或者试试新的XCFrameworks (但要注意

          【讨论】:

            【解决方案11】:

            最好的方法。

              let reqIndex = array.indices.contains(index)
              print(reqIndex)
            

            【讨论】:

            • 请在您的答案中添加一些解释,以便其他人可以从中学习
            【解决方案12】:

            我认为我们应该将这个扩展添加到 Swift 中的每个项目中

            extension Collection {
                @inlinable func isValid(position: Self.Index) -> Bool {
                    return (startIndex..<endIndex) ~= position
                }
                
                @inlinable func isValid(bounds: Range<Self.Index>) -> Bool {
                    return (startIndex..<endIndex) ~= bounds.upperBound
                }
                
                @inlinable subscript(safe position: Self.Index) -> Self.Element? {
                    guard isValid(position: position) else { return nil }
                    return self[position]
                }
                
                @inlinable subscript(safe bounds: Range<Self.Index>) -> Self.SubSequence? {
                    guard isValid(bounds: bounds) else { return nil }
                    return self[bounds]
                }
            }
            
            extension MutableCollection {
                @inlinable subscript(safe position: Self.Index) -> Self.Element? {
                    get {
                        guard isValid(position: position) else { return nil }
                        return self[position]
                    }
                    set {
                        guard isValid(position: position), let newValue = newValue else { return }
                        self[position] = newValue
                    }
                }
                @inlinable subscript(safe bounds: Range<Self.Index>) -> Self.SubSequence? {
                    get {
                        guard isValid(bounds: bounds) else { return nil }
                        return self[bounds]
                    }
                    set {
                        guard isValid(bounds: bounds), let newValue = newValue else { return }
                        self[bounds] = newValue
                    }
                }
            }
            

            请注意,我的 isValid(position:)isValid(bounds:) 函数具有复杂性 O(1),与下面的大多数答案不同,它使用复杂性 contains(_:) 方法 O(n)


            示例用法:

            let arr = ["a","b"]
            print(arr[safe: 2] ?? "nil") // output: nil
            print(arr[safe: 1..<2] ?? "nil") // output: nil
            
            var arr2 = ["a", "b"]
            arr2[safe: 2] = "c"
            print(arr2[safe: 2] ?? "nil") // output: nil
            arr2[safe: 1..<2] = ["c","d"]
            print(arr[safe: 1..<2] ?? "nil") // output: nil
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2021-07-22
              • 2012-11-30
              • 2015-09-16
              • 2023-01-24
              • 2016-11-19
              • 2013-05-28
              • 2010-09-20
              • 2016-05-03
              相关资源
              最近更新 更多