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