Безопасный (проверенный граница) поиск массива в Swift через дополнительные привязки?

Если у меня есть массив в Swift и пытаюсь получить доступ к индексу, который выходит за рамки, возникает неудивительная ошибка времени выполнения:

var str = ["Apple", "Banana", "Coconut"]

str[0] // "Apple"
str[3] // EXC_BAD_INSTRUCTION

Однако я бы подумал со всей возможной цепью и безопасностью, которую приносит Swift, было бы тривиально сделать что-то вроде:

let theIndex = 3
if let nonexistent = str[theIndex] { // Bounds check + Lookup
    print(nonexistent)
    ...do other things with nonexistent...
}

Вместо:

let theIndex = 3
if (theIndex < str.count) {         // Bounds check
    let nonexistent = str[theIndex] // Lookup
    print(nonexistent)   
    ...do other things with nonexistent... 
}

Но это не так - я должен использовать оператор ol 'if для проверки и обеспечения индекса меньше str.count.

Я попытался добавить свою собственную реализацию subscript(), но я не уверен, как передать вызов исходной реализации или получить доступ к элементам (на основе индексов) без использования нотации индекса:

extension Array {
    subscript(var index: Int) -> AnyObject? {
        if index >= self.count {
            NSLog("Womp!")
            return nil
        }
        return ... // What?
    }
}

Ответ 1

У Алекса ответа есть хороший совет и решение для вопроса, однако я случайно наткнулся на более хороший способ реализации этой функциональности:

Swift 3.2 и новее

extension Collection {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

Swift 3.0 и 3.1

extension Collection where Indices.Iterator.Element == Index {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Generator.Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

Благодарим Хэмиша за то, что он предложил решение для Swift 3.

Swift 2

extension CollectionType {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    subscript (safe index: Index) -> Generator.Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

пример

let array = [1, 2, 3]

for index in -20...20 {
    if let item = array[safe: index] {
        print(item)
    }
}

Ответ 2

Если вы действительно хотите этого поведения, пахнет, как будто вы хотите использовать словарь вместо массива. Словари возвращают nil при доступе к отсутствующим клавишам, что имеет смысл, потому что гораздо сложнее узнать, присутствует ли ключ в словаре, поскольку эти ключи могут быть любыми, где в массиве ключ должен находиться в диапазоне: 0 до count. И это невероятно распространено для итерации по этому диапазону, где вы можете быть абсолютно уверены, что имеют реальную ценность на каждой итерации цикла.

Я думаю, что причина, по которой это не работает, - это выбор дизайна, сделанный разработчиками Swift. Возьмем ваш пример:

var fruits: [String] = ["Apple", "Banana", "Coconut"]
var str: String = "I ate a \( fruits[0] )"

Если вы уже знаете, что индекс существует, как и в большинстве случаев, когда вы используете массив, этот код замечательный. Однако, если доступ к индексу мог бы возвратить nil, то вы изменили бы тип возврата метода Array subscript, чтобы быть необязательным. Это изменяет ваш код на:

var fruits: [String] = ["Apple", "Banana", "Coconut"]
var str: String = "I ate a \( fruits[0]! )"
//                                     ^ Added

Это означает, что вам нужно было бы разворачивать необязательный каждый раз, когда вы повторяли массив, или делали что-либо еще с известным индексом, просто потому, что редко вы можете получить доступ к индексу вне пределов. Дизайнеры Swift выбрали меньшее развертывание опций, за счет исключения во время выполнения при доступе к запредельным индексам. И авария предпочтительнее логической ошибки, вызванной nil, которую вы не ожидали в своих данных где-то.

И я согласен с ними. Таким образом, вы не будете изменять реализацию по умолчанию Array, потому что вы разбиваете весь код, ожидающий необязательные значения из массивов.

Вместо этого вы можете подклассом Array и переопределить subscript, чтобы вернуть необязательный. Или, что более эффективно, вы можете расширить Array с помощью метода без индекса, который делает это.

extension Array {

    // Safely lookup an index that might be out of bounds,
    // returning nil if it does not exist
    func get(index: Int) -> T? {
        if 0 <= index && index < count {
            return self[index]
        } else {
            return nil
        }
    }
}

var fruits: [String] = ["Apple", "Banana", "Coconut"]
if let fruit = fruits.get(1) {
    print("I ate a \( fruit )")
    // I ate a Banana
}

if let fruit = fruits.get(3) {
    print("I ate a \( fruit )")
    // never runs, get returned nil
}

Обновление Swift 3

func get(index: Int) -> T? необходимо заменить на func get(index: Int) -> Element?

Ответ 3

Действителен в Swift 2

Несмотря на то, что это уже было дано много раз, я хотел бы представить ответ больше в строке, где идет мода программирования Swift, в словах Crusty¹: "Подумайте protocol first"

• Что мы хотим сделать?
- Получить элемент Array с учетом индекса только тогда, когда он безопасен, и nil в противном случае
• В чем должна заключаться эта функциональность? - Array subscript ing
• Откуда у вас эта функция?
- Его определение struct Array в модуле Swift имеет его • Ничего более общего или абстрактного?
- Он принимает protocol CollectionType, который обеспечивает его также • Ничего более общего или абстрактного?
- Он принимает protocol Indexable, а также...
• Да, это звучит как лучшее, что мы можем сделать. Можем ли мы затем расширить его, чтобы иметь эту функцию, которую мы хотим?
- Но у нас есть очень ограниченные типы (нет Int) и свойства (no count) для работы сейчас!
• Этого будет достаточно. Swift stdlib выполняется довольно хорошо;)

extension Indexable {
    public subscript(safe safeIndex: Index) -> _Element? {
        return safeIndex.distanceTo(endIndex) > 0 ? self[safeIndex] : nil
    }
}

¹: не верно, но оно дает идею

Ответ 4

  • Поскольку массивы могут хранить значения nil, нет смысла возвращать нуль, если вызов массива [index] выходит за пределы.
  • Поскольку мы не знаем, как пользователь хотел бы справиться с проблемами с ограничениями, нет смысла использовать пользовательские операторы.
  • В отличие от этого, используйте традиционный поток управления для разворачивания объектов и обеспечения безопасности типа.

если пусть index = array.checkIndexForSafety(index: Int)

  let item = array[safeIndex: index] 

если пусть index = array.checkIndexForSafety(index: Int)

  array[safeIndex: safeIndex] = myObject
extension Array {

    @warn_unused_result public func checkIndexForSafety(index: Int) -> SafeIndex? {

        if indices.contains(index) {

            // wrap index number in object, so can ensure type safety
            return SafeIndex(indexNumber: index)

        } else {
            return nil
        }
    }

    subscript(index:SafeIndex) -> Element {

        get {
            return self[index.indexNumber]
        }

        set {
            self[index.indexNumber] = newValue
        }
    }

    // second version of same subscript, but with different method signature, allowing user to highlight using safe index
    subscript(safeIndex index:SafeIndex) -> Element {

        get {
            return self[index.indexNumber]
        }

        set {
            self[index.indexNumber] = newValue
        }
    }

}

public class SafeIndex {

    var indexNumber:Int

    init(indexNumber:Int){
        self.indexNumber = indexNumber
    }
}

Ответ 5

extension Array {
    subscript (safe index: Index) -> Element? {
        return 0 <= index && index < count ? self[index] : nil
    }
}
  • O (1) производительность
  • безопасный тип
  • правильно использует опции для [MyType?] (возвращает MyType?, который может быть развернут на обоих уровнях)
  • не приводит к проблемам для наборов
  • сокращенный код

Вот несколько тестов, которые я использовал для вас:

let itms: [Int?] = [0, nil]
let a = itms[safe: 0] // 0 : Int??
a ?? 5 // 0 : Int?
let b = itms[safe: 1] // nil : Int??
b ?? 5 // nil : Int?
let c = itms[safe: 2] // nil : Int??
c ?? 5 // 5 : Int?

Ответ 6

Swift 4

Расширение для тех, кто предпочитает более традиционный синтаксис:

extension Array where Element: Equatable {

    func item(at index: Int) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

Ответ 7

Чтобы построить ответ Никиты Кукушкина, иногда нужно безопасно присваивать индексам массива, а также читать из них, т.е.

myArray[safe: badIndex] = newValue

Итак, вот обновление ответа Никиты (Swift 3.2), которое также позволяет безопасно записывать в индексы изменяемого массива, добавляя параметр safe: name.

extension Collection {
    /// Returns the element at the specified index iff it is within bounds, otherwise nil.
    subscript(safe index: Index) -> Element? {
        return indices.contains(index) ? self[ index] : nil
    }
}

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
            }
        }
    }
}

Ответ 8

Я нашел безопасный массив get, set, insert, remove очень полезным. Я предпочитаю регистрировать и игнорировать ошибки, так как все остальное скоро становится трудно управлять. Полный текст ниже.

/**
 Safe array get, set, insert and delete.
 All action that would cause an error are ignored.
 */
extension Array {

    /**
     Removes element at index.
     Action that would cause an error are ignored.
     */
    mutating func remove(safeAt index: Index) {
        guard index >= 0 && index < count else {
            print("Index out of bounds while deleting item at index \(index) in \(self). This action is ignored.")
            return
        }

        remove(at: index)
    }

    /**
     Inserts element at index.
     Action that would cause an error are ignored.
     */
    mutating func insert(_ element: Element, safeAt index: Index) {
        guard index >= 0 && index <= count else {
            print("Index out of bounds while inserting item at index \(index) in \(self). This action is ignored")
            return
        }

        insert(element, at: index)
    }

    /**
     Safe get set subscript.
     Action that would cause an error are ignored.
     */
    subscript (safe index: Index) -> Element? {
        get {
            return indices.contains(index) ? self[index] : nil
        }
        set {
            remove(safeAt: index)

            if let element = newValue {
                insert(element, safeAt: index)
            }
        }
    }
}

Испытания

import XCTest

class SafeArrayTest: XCTestCase {
    func testRemove_Successful() {
        var array = [1, 2, 3]

        array.remove(safeAt: 1)

        XCTAssert(array == [1, 3])
    }

    func testRemove_Failure() {
        var array = [1, 2, 3]

        array.remove(safeAt: 3)

        XCTAssert(array == [1, 2, 3])
    }

    func testInsert_Successful() {
        var array = [1, 2, 3]

        array.insert(4, safeAt: 1)

        XCTAssert(array == [1, 4, 2, 3])
    }

    func testInsert_Successful_AtEnd() {
        var array = [1, 2, 3]

        array.insert(4, safeAt: 3)

        XCTAssert(array == [1, 2, 3, 4])
    }

    func testInsert_Failure() {
        var array = [1, 2, 3]

        array.insert(4, safeAt: 5)

        XCTAssert(array == [1, 2, 3])
    }

    func testGet_Successful() {
        var array = [1, 2, 3]

        let element = array[safe: 1]

        XCTAssert(element == 2)
    }

    func testGet_Failure() {
        var array = [1, 2, 3]

        let element = array[safe: 4]

        XCTAssert(element == nil)
    }

    func testSet_Successful() {
        var array = [1, 2, 3]

        array[safe: 1] = 4

        XCTAssert(array == [1, 4, 3])
    }

    func testSet_Successful_AtEnd() {
        var array = [1, 2, 3]

        array[safe: 3] = 4

        XCTAssert(array == [1, 2, 3, 4])
    }

    func testSet_Failure() {
        var array = [1, 2, 3]

        array[safe: 4] = 4

        XCTAssert(array == [1, 2, 3])
    }
}

Ответ 9

Я заполнил массив nil в моем случае:

let components = [1, 2]
var nilComponents = components.map { $0 as Int? }
nilComponents += [nil, nil, nil]

switch (nilComponents[0], nilComponents[1], nilComponents[2]) {
case (_, _, .Some(5)):
    // process last component with 5
default:
    break
}

Также проверьте расширение индекса с меткой safe: Эрика Садун/Майк Эш: http://ericasadun.com/2015/06/01/swift-safe-array-indexing-my-favorite-thing-of-the-new-week/

Ответ 10

Я думаю, что это не очень хорошая идея. Кажется предпочтительным создать сплошной код, который не приводит к попытке применить индексы вне границ.

Пожалуйста, учтите, что при такой ошибке сбой (как было предложено вашим кодом выше), возвращая nil, подвержен возникновению еще более сложных и более сложных ошибок.

Вы можете сделать свое переопределение так же, как вы использовали, и просто написать индексы по-своему. Единственным недостатком является то, что существующий код не будет совместим. Я считаю, что найти крючок для переопределения общего x [i] (также без текстового препроцессора, как на C) будет сложным.

Ближайшим, о котором я могу думать, является

// compile error:
if theIndex < str.count && let existing = str[theIndex]

EDIT. Это действительно работает. Однострочник!!

func ifInBounds(array: [AnyObject], idx: Int) -> AnyObject? {
    return idx < array.count ? array[idx] : nil
}

if let x: AnyObject = ifInBounds(swiftarray, 3) {
    println(x)
}
else {
    println("Out of bounds")
}

Ответ 11

extension Array {
  subscript (safe index: UInt) -> Element? {
    return Int(index) < count ? self[Int(index)] : nil
  }
}

Используя вышеуказанное расширение, верните nil, если индекс в любое время выходит за пределы.

let fruits = ["apple","banana"]
print("result-\(fruits[safe : 2])")

результат - ноль

Ответ 12

Как поймать такое исключение с неправильной индексацией:

extension Array {
    func lookup(index : UInt) throws -> Element {
        if Int(index) >= count {
            throw NSError(
                domain: "com.sadun",
                code: 0,
                userInfo: [NSLocalizedFailureReasonErrorKey: "Array index out of bounds"]
            )
        }

        return self[Int(index)]
    }
}

Пример:

do {
    try ["Apple", "Banana", "Coconut"].lookup(index: 3)
} catch {
    print(error)
}