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

193

Если у меня есть массив в 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?
    }
}
  • 2
    Я понимаю, что это немного OT, но я также чувствую, что было бы неплохо, если бы у Swift был четкий синтаксис для выполнения каких-либо проверок границ, включая списки. У нас уже есть подходящее ключевое слово для этого, например. Так, например, если X в (1,2,7) ... или если X в myArray
Теги:
xcode

12 ответов

494
Лучший ответ

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

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)
    }
}
  • 37
    Я думаю, что это определенно заслуживает внимания - хорошая работа. Мне нравится включенный safe: имя параметра, чтобы гарантировать разницу.
  • 1
    Это фантастика. ОП, изменить принятый ответ?
Показать ещё 17 комментариев
47

Если вы действительно хотите этого поведения, пахнет, как будто вы хотите использовать словарь вместо массива. Словари возвращают 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?

  • 1
    +1 (и согласие) за упоминание проблемы с изменением типа возвращаемого значения subscript() на необязательный - это было основным препятствием на пути переопределения поведения по умолчанию. (Я вообще не мог заставить его работать . ) Я избегал написания метода расширения get() , который является очевидным выбором в других сценариях (категории Obj-C, кто-нибудь?), Но get( не намного больше чем [ , и дает понять, что поведение может отличаться от того, что другие разработчики могут ожидать от оператора индекса Swift. Спасибо!
  • 2
    Чтобы сделать его еще короче, я использую в ();) Спасибо!
Показать ещё 2 комментария
12

Действителен в 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
    }
}

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

  • 1
    Как новичок Свифта, я не понимаю этот ответ. Что представляет собой код в конце? Это решение, и если да, то как мне его использовать?
  • 2
    Извините, этот ответ больше не действителен для Swift 3, но процесс, безусловно, так и есть. Разница лишь в том, что теперь вам стоит остановиться в Collection наверное :)
7
  • Поскольку массивы могут хранить значения 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
    }
}
  • 0
    Интересный подход. Любая причина SafeIndex это класс, а не структура?
5

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

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
            }
        }
    }
}
  • 1
    Чрезвычайно недооцененный ответ! Это правильный способ сделать это!
5

Swift 4

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

extension Array where Element: Equatable {

    func item(at index: Int) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}
  • 0
    вам не нужно ограничивать элементы массива равными, чтобы проверить, содержит ли индекс свой индекс.
  • 0
    да - хорошая мысль - это понадобится только для дополнительных безопасных методов, таких как deleteObject и т. д.
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?
3

Я нашел безопасный массив 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])
    }
}
1

Я заполнил массив 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/

0
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])")

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

0

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

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

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

Пожалуйста, учтите, что при такой ошибке сбой (как было предложено вашим кодом выше), возвращая 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")
}
  • 6
    Я бы не согласился - смысл необязательного связывания заключается в том, что оно успешно выполняется только при выполнении условия. (Для необязательного значения это означает, что есть значение.) Использование if let в этом случае не делает программу более сложной, а ошибки - более сложными. Он просто уплотняет традиционную проверку двух операторов, if проверка границ и фактический поиск, в единый сжатый оператор. В некоторых случаях (особенно при работе с пользовательским интерфейсом) индекс может выходить за границы, например, запрашивать NSTableView для selectedRow без выделения.
  • 3
    @ Манди, похоже, это комментарий, а не ответ на вопрос ОП.
Показать ещё 8 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню