Безопасный (проверенный границами) поиск в массиве в 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... 
}

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

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

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

24 ответа

Решение

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

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

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

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

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

Если вы уже знаете, что индекс существует, как и в большинстве случаев, когда вы используете массив, этот код отлично подходит. Тем не менее, если доступ к индексу может вернуть nil Затем вы изменили, чтобы вернуть тип Array "s 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?

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

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

Swift 4

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

extension Array where Element: Equatable {

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

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

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

• Что мы хотим сделать?
- получить элемент Array Индекс предоставляется только тогда, когда это безопасно, и nil иначе
• На чем должна основываться эта функциональность?
- Arraysubscript ИНГ
• Откуда он получает эту функцию?
- его определение struct Array в Swift модуль имеет это
• Ничего более общего / абстрактного?
- принимает protocol CollectionType что также гарантирует
• Ничего более общего / абстрактного?
- принимает protocol Indexable также...
• Да, звучит как лучшее, что мы можем сделать. Можем ли мы затем расширить его, чтобы получить эту функцию, которую мы хотим?
- Но у нас очень ограниченные типы (нет Int ) и свойства (нет count ) работать сейчас
• этого будет достаточно. Stdlib Свифта сделан довольно хорошо;)

extension Indexable {
    public subscript(safe safeIndex: Index) -> _Element? {
        return safeIndex.distanceTo(endIndex) > 0 ? self[safeIndex] : 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
    }
}

Я понимаю, что это старый вопрос. Я использую Swift5.1 на данный момент, OP был для Swift 1 или 2?

Мне нужно было что-то подобное сегодня, но я не хотел добавлять полномасштабное расширение только для одного места и хотел чего-то более функционального (более поточно-ориентированного?). Мне также не нужно было защищаться от отрицательных индексов, только от тех, которые могут выходить за пределы массива:

let fruit = ["Apple", "Banana", "Coconut"]

let a = fruit.dropFirst(2).first // -> "Coconut"
let b = fruit.dropFirst(0).first // -> "Apple"
let c = fruit.dropFirst(10).first // -> nil

Для тех, кто спорит о Последовательностях с нулями, что вы делаете с first а также last свойства, возвращающие ноль для пустых коллекций?

Мне это понравилось, потому что я мог просто ухватиться за существующий материал и использовать его для получения желаемого результата. Я также знаю, что dropFirst (n) - это не копия всей коллекции, а просто ее фрагмент. И тогда сначала меня берет на себя уже существующее поведение.

Swift 5.x

Расширение на RandomAccessCollection означает, что это также может работать для ArraySliceот единственной реализации. Мы используем startIndex а также endIndex поскольку срезы массива используют индексы из базового родителя Array.

      public extension RandomAccessCollection {

    /// Returns the element at the specified index if it is within bounds, otherwise nil.
    /// - complexity: O(1)
    subscript (safe index: Index) -> Element? {
        guard index >= startIndex, index < endIndex else {
            return nil
        }
        return self[index]
    }
    
}

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

/**
 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])
    }
}
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])")

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

В списке "Обычно отклоняемые изменения" для Swift есть упоминание об изменении доступа к индексу Array для возврата необязательного, а не сбоя:

Сделать Array<T> возврат доступа по индексу T? или T! вместо того T: Текущее поведение массива является преднамеренным, поскольку оно точно отражает тот факт, что доступ к массиву за пределами допустимого диапазона является логической ошибкой. Изменение текущего поведения замедлитArrayдоступы в неприемлемой степени. Эта тема неоднократно поднималась ранее, но вряд ли будет принята.

https://github.com/apple/swift-evolution/blob/master/commonly_proposed.md

Таким образом, доступ к базовому индексу не изменится, чтобы вернуть необязательный.

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

Это было предложено и обсуждено на форуме Swift Evolution здесь:

https://forums.swift.org/t/add-accessor-with-bounds-check-to-array/16871

Примечательно, что Крис Латтнер дал этой идее "+1":

Согласны, наиболее часто предлагаемое написание для этого слова: yourArray[safe: idx], что мне кажется большим. Я очень +1 за добавление этого.

https://forums.swift.org/t/add-accessor-with-bounds-check-to-array/16871/13

Так что это может быть возможно в какой-нибудь будущей версии Swift прямо из коробки. Я призываю всех, кто хочет, внести свой вклад в эту ветку Swift Evolution.

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

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

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

Самое близкое, что я могу придумать, это

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

РЕДАКТИРОВАТЬ: Это на самом деле работает. Один лайнер!!

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

Не уверен, почему никто не установил расширение, в котором также есть сеттер для автоматического увеличения массива

extension Array where Element: ExpressibleByNilLiteral {
    public subscript(safe index: Int) -> Element? {
        get {
            guard index >= 0, index < endIndex else {
                return nil
            }

            return self[index]
        }

        set(newValue) {
            if index >= endIndex {
                self.append(contentsOf: Array(repeating: nil, count: index - endIndex + 1))
            }

            self[index] = newValue ?? nil
        }
    }
}

Использование простое и работает со Swift 5.1.

var arr:[String?] = ["A","B","C"]

print(arr) // Output: [Optional("A"), Optional("B"), Optional("C")]

arr[safe:10] = "Z"

print(arr) // [Optional("A"), Optional("B"), Optional("C"), nil, nil, nil, nil, nil, nil, nil, Optional("Z")]

Примечание: вы должны понимать стоимость производительности (как во времени, так и в пространстве) при быстром наращивании массива, но для небольших проблем иногда вам просто нужно заставить Swift остановить сам Swifting

Я добавил массив с nils в моем случае использования:

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/

Чтобы объяснить, почему операции терпят неудачу, лучше использовать ошибки, чем варианты. Индексы не могут вызывать ошибки, поэтому это должен быть метод.

public extension Collection {
  /// - Returns: same as subscript, if index is in bounds
  /// - Throws: CollectionIndexingError
  func element(at index: Index) throws -> Element {
    guard indices.contains(index)
    else { throw CollectionIndexingError() }

    return self[index]
  }
}

/// Thrown when `element(at:)` is called with an invalid index.
public struct CollectionIndexingError: Error { }
XCTAssertThrowsError( try ["", ""].element(at: 2) )

let optionals = [1, 2, nil]
XCTAssertEqual(try optionals.element(at: 0), 1)

XCTAssertThrowsError( try optionals.element(at: optionals.endIndex) )
{ XCTAssert($0 is CollectionIndexingError) }

Можешь попробовать

if index >= 0 && index < array.count {
    print(array[index]) 
}

это то, что я использую в своих проектах

      import Foundation

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

    subscript (safe index: Index) -> Element? {
        guard index >= startIndex, index < endIndex else {
            return nil
        }
        return self[index]
    }

}

public extension RandomAccessCollection {

    subscript (safe index: Index) -> Element? {
        guard index >= startIndex, index < endIndex else {
            return nil
        }
        return self[index]
    }
    
}

Честно говоря, я тоже столкнулся с этой проблемой. И с точки зрения производительности массив Swift должен уметь бросать. let x = try a[y] Это было бы красиво и понятно.

2022

бесконечный доступ к индексу и безопасный доступ к idx (возвращает ноль, если такого idx нет):

      public extension Collection {
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }

    subscript (infinityIdx idx: Index) -> Element where Index == Int {
        return self[ abs(idx) % self.count ]
    }
}

но будьте осторожны, он выдаст исключение, если массив/коллекция пусты

Применение

      (0...10)[safe: 11] // nil

(0...10)[infinityIdx: 11] // 0
(0...10)[infinityIdx: 12] // 1
(0...10)[infinityIdx: 21] // 0
(0...10)[infinityIdx: 22] // 1

Когда вам нужно получить только значения из массива, и вы не возражаете против небольшого снижения производительности (например, если ваша коллекция невелика), существует альтернатива на основе словаря, которая не включает (слишком общий, для моих вкус) расширение коллекции:

// Assuming you have a collection named array:
let safeArray = Dictionary(uniqueKeysWithValues: zip(0..., array))
let value = safeArray[index] ?? defaultValue;

Я сделал простое расширение для массива

extension Array where Iterator.Element : AnyObject {
    func iof (_ i : Int ) -> Iterator.Element? {
        if self.count > i {
            return self[i] as Iterator.Element
        }
        else {
            return nil
        }
    }

}

он отлично работает как задумано

пример

   if let firstElemntToLoad = roots.iof(0)?.children?.iof(0)?.cNode, 

Использование Swift 5

extension WKNavigationType {
    var name : String {
        get {
            let names = ["linkAct","formSubm","backForw","reload","formRelo"]
            return names.indices.contains(self.rawValue) ? names[self.rawValue] : "other"
        }
    }
}

закончил, но очень хотел сделать в целом как

[<collection>][<index>] ?? <default>

но поскольку коллекция контекстная, я думаю, это правильно.

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

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)
}
Другие вопросы по тегам