Создание расширения для фильтрации nils из массива в Swift
Я пытаюсь написать расширение для массива, которое позволит массиву необязательных T быть преобразованным в массив необязательных T.
Например, это можно записать как свободную функцию, например:
func removeAllNils(array: [T?]) -> [T] {
return array
.filter({ $0 != nil }) // remove nils, still a [T?]
.map({ $0! }) // convert each element from a T? to a T
}
Но я не могу заставить это работать как расширение. Я пытаюсь сказать компилятору, что расширение применяется только к массивам необязательных значений. Вот что у меня есть до сих пор:
extension Array {
func filterNils<U, T: Optional<U>>() -> [U] {
return filter({ $0 != nil }).map({ $0! })
}
}
(он не компилируется!)
6 ответов:
Невозможно ограничить тип, определенный для универсальной структуры или класса-массив предназначен для работы с любым типом, поэтому вы не можете добавить метод, который работает для подмножества типов. Ограничения типа могут быть указаны только при объявлении универсального типа
Единственный способ достичь того, что вам нужно, - это создать либо глобальную функцию, либо статический метод - в последнем случае:extension Array { static func filterNils(array: [T?]) -> [T] { return array.filter { $0 != nil }.map { $0! } } } var array:[Int?] = [1, nil, 2, 3, nil] Array.filterNils(array)
Или просто использовать
compactMap
(ранееflatMap
), который может быть использован для удаления всех нулей значения:[1, 2, nil, 4].compactMap { $0 } // Returns [1, 2, 4]
Начиная с Swift 2.0, вам не нужно писать собственное расширение для фильтрации нулевых значений из массива, вы можете использовать
flatMap
, который выравнивает массив и фильтрует nils:let optionals : [String?] = ["a", "b", nil, "d"] let nonOptionals = optionals.flatMap{$0} print(nonOptionals)
Отпечатки пальцев:
[a, b, d]
Примечание:
Есть 2
flatMap
функции:
Один
flatMap
используется для удаления ненулевых значений, которые показаны выше. См. - https://developer.apple.com/documentation/swift/sequence/2907182-flatmapДругой
flatMap
используется для объедините результаты. См. - https://developer.apple.com/documentation/swift/sequence/2905332-flatmap
TL; DR
Чтобы избежать потенциальных ошибок/путаницы, не используйте
array.flatMap { $0 }
для удаления nils; вместо этого используйте метод расширения, такой какarray.removeNils()
(реализация ниже, обновлена для Swift 3.0).
Хотя
array.flatMap { $0 }
работает большую часть времени, есть несколько причин в пользу расширенияarray.removeNils()
:
removeNils
точно описывает, что вы хотите сделать : удалить значенияnil
. Кто-то, не знакомый сflatMap
, должен был бы посмотреть его, и, когда они это сделают посмотрите на это, если они обратят пристальное внимание, они придут к тому же выводу, что и мой следующий пункт;- Поскольку существуют две совершенно разные функции, это делает понимание
flatMap
имеет две различные реализации, которые делают две совершенно разные вещи. Основываясь на проверке типов, компилятор собирается решить, какой из них вызывается. Это может быть очень проблематично в Swift, так как вывод типа используется в значительной степени. (Например, чтобы определить фактический тип переменной, может потребоваться проверить несколько файлов.) Рефактор может привести к тому, что ваше приложение вызовите неправильную версиюflatMap
, которая может привести ктруднодоступным ошибкам .flatMap
намного более трудным, поскольку вы можете легко объединить эти два.flatMap
может вызываться на необязательных массивах (например,[Int]
), поэтому при рефакторинге массива из[Int?]
в[Int]
Вы можете случайно оставить после себяflatMap { $0 }
вызовы, о которых компилятор вас не предупредит. Около в лучшем случае он просто вернет себя, в худшем-вызовет выполнение другой реализации, что потенциально приведет к ошибкам.- в Swift 3, Если вы явно не приведете возвращаемый тип, компилятор выберет неправильную версию, что приведет к непредвиденным последствиям. (См. раздел Swift 3 ниже)
- и, наконец, это замедляет компилятор , потому что система проверки типов должна определить, какая из перегруженных функций будет вызов.
Напомним, что существует две версии рассматриваемой функции, обе, к сожалению, называютсяflatMap
.
Сглаживание последовательностей путем удаления уровня вложенности (например,
[[1, 2], [3]] -> [1, 2, 3]
)public struct Array<Element> : RandomAccessCollection, MutableCollection { /// Returns an array containing the concatenated results of calling the /// given transformation with each element of this sequence. /// /// Use this method to receive a single-level collection when your /// transformation produces a sequence or collection for each element. /// /// In this example, note the difference in the result of using `map` and /// `flatMap` with a transformation that returns an array. /// /// let numbers = [1, 2, 3, 4] /// /// let mapped = numbers.map { Array(count: $0, repeatedValue: $0) } /// // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]] /// /// let flatMapped = numbers.flatMap { Array(count: $0, repeatedValue: $0) } /// // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] /// /// In fact, `s.flatMap(transform)` is equivalent to /// `Array(s.map(transform).joined())`. /// /// - Parameter transform: A closure that accepts an element of this /// sequence as its argument and returns a sequence or collection. /// - Returns: The resulting flattened array. /// /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence /// and *n* is the length of the result. /// - SeeAlso: `joined()`, `map(_:)` public func flatMap<SegmentOfResult : Sequence>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element] }
Удаление элементов из последовательности (например,
[1, nil, 3] -> [1, 3]
)public struct Array<Element> : RandomAccessCollection, MutableCollection { /// Returns an array containing the non-`nil` results of calling the given /// transformation with each element of this sequence. /// /// Use this method to receive an array of nonoptional values when your /// transformation produces an optional value. /// /// In this example, note the difference in the result of using `map` and /// `flatMap` with a transformation that returns an optional `Int` value. /// /// let possibleNumbers = ["1", "2", "three", "///4///", "5"] /// /// let mapped: [Int?] = numbers.map { str in Int(str) } /// // [1, 2, nil, nil, 5] /// /// let flatMapped: [Int] = numbers.flatMap { str in Int(str) } /// // [1, 2, 5] /// /// - Parameter transform: A closure that accepts an element of this /// sequence as its argument and returns an optional value. /// - Returns: An array of the non-`nil` results of calling `transform` /// with each element of the sequence. /// /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence /// and *n* is the length of the result. public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] }
#2-это тот, который люди используют для удаления Нилов, передавая
{ $0 }
какtransform
. Это работает, так как метод выполняет отображение, а затем отфильтровывает всеnil
элементы.Вам может быть интересно "почему Apple не переименовала #2 в
Например, #1 может легко разбить массив строк на отдельные символы (сгладить) и прописать каждую букву (сопоставить):removeNils()
"? следует иметь в виду, что использованиеflatMap
для удаления nils-это не единственное использование #2. На самом деле, поскольку обе версии принимают функциюtransform
, они могут быть гораздо более мощными, чем приведенные выше примеры.["abc", "d"].flatMap { $0.uppercaseString.characters } == ["A", "B", "C", "D"]
В то время как номер #2 можно легко удалить все четные числа (сгладить) и умножить каждое число на
-1
(карта):[1, 2, 3, 4, 5, 6].flatMap { ($0 % 2 == 0) ? nil : -$0 } == [-1, -3, -5]
(обратите внимание, что этот последний пример может привести к тому, что Xcode 7.3 будет вращаться очень долго, потому что явных типов не указано. Еще одно доказательство того, почему методы должны иметь разные названия.)
Реальная опасность слепого использованияflatMap { $0 }
для удаленияnil
s возникает не тогда, когда вы вызываете его на[1, 2]
, а когда вы вызываете его на что-то вроде[[1], [2]]
. В первом случае он вызовет вызов №2 безвредно и возвращение[1, 2]
. В последнем случае вы можете подумать, что он будет делать то же самое (безвредно возвращать[[1], [2]]
, так как нет значенийnil
), но на самом деле он вернет[1, 2]
, так как он использует вызов #1.Тот факт, что
flatMap { $0 }
используется для удаленияnil
s, кажется, больше из Swift сообщества рекомендация , а не исходящая от Apple. Возможно, если Apple заметит эту тенденцию, они в конечном итоге предоставят функциюremoveNils()
или что-то подобное.Пока затем нам остается придумать собственное решение.
Решение
// Updated for Swift 3.0 protocol OptionalType { associatedtype Wrapped func map<U>(_ f: (Wrapped) throws -> U) rethrows -> U? } extension Optional: OptionalType {} extension Sequence where Iterator.Element: OptionalType { func removeNils() -> [Iterator.Element.Wrapped] { var result: [Iterator.Element.Wrapped] = [] for element in self { if let element = element.map({ $0 }) { result.append(element) } } return result } }
(Примечание: не путайте с
element.map
... это не имеет никакого отношения кflatMap
, обсуждаемому в этом посте. Он используетOptional
'smap
функция для получения необязательного типа, который можно развернуть. Если вы опустите эту часть, вы получите следующую синтаксическую ошибку: "ошибка: инициализатор для условной привязки должен иметь необязательный тип, а не' Self.Генератор.Элемент"."Для получения дополнительной информации о том, какmap()
помогает нам, см. этот ответ, который я написал о добавлении метода расширения на SequenceType для подсчета не-nils.)Использование
let a: [Int?] = [1, nil, 3] a.removeNils() == [1, 3]
Пример
var myArray: [Int?] = [1, nil, 2] assert(myArray.flatMap { $0 } == [1, 2], "Flat map works great when it's acting on an array of optionals.") assert(myArray.removeNils() == [1, 2]) var myOtherArray: [Int] = [1, 2] assert(myOtherArray.flatMap { $0 } == [1, 2], "However, it can still be invoked on non-optional arrays.") assert(myOtherArray.removeNils() == [1, 2]) // syntax error: type 'Int' does not conform to protocol 'OptionalType' var myBenignArray: [[Int]?] = [[1], [2, 3], [4]] assert(myBenignArray.flatMap { $0 } == [[1], [2, 3], [4]], "Which can be dangerous when used on nested SequenceTypes such as arrays.") assert(myBenignArray.removeNils() == [[1], [2, 3], [4]]) var myDangerousArray: [[Int]] = [[1], [2, 3], [4]] assert(myDangerousArray.flatMap { $0 } == [1, 2, 3, 4], "If you forget a single '?' from the type, you'll get a completely different function invocation.") assert(myDangerousArray.removeNils() == [[1], [2, 3], [4]]) // syntax error: type '[Int]' does not conform to protocol 'OptionalType'
(обратите внимание, как на последний, помощью flatMap возвращает
[1, 2, 3, 4]
, а removeNils() можно было бы ожидать, чтобы вернуться[[1], [2, 3], [4]]
.)
Решение аналогичноответу @fabb, связанному с.
Однако я внес несколько изменений:
- я не назвал метод
flatten
, поскольку уже существует методflatten
для типов последовательностей, и присвоение одного и того же имени совершенно разным методам-это то, что привело нас в этот беспорядок в первую очередь. Не говоря уже о том, что гораздо легче неверно истолковать то, что делаетflatten
, чем то, что естьremoveNils
.- вместо того, чтобы создавать новый тип
T
наOptionalType
, он использует то же имя, что иOptional
(Wrapped
).- вместо выполнение
map{}.filter{}.map{}
, что приводит кO(M + N)
времени, я петлю через массив один раз.- вместо этого чтобы перейти от
Generator.Element
кGenerator.Element.Wrapped?
, я используюmap
. Нет необходимости возвращать значенияnil
внутри функцииmap
, поэтомуmap
будет достаточно. Избегая функцииflatMap
, труднее объединить еще один (т. е. 3-й) метод с тем же именем, который имеет совершенно другую функцию.Единственный недостаток использования
removeNils
противflatMap
заключается в том, что для проверки типа может потребоваться немного больше намеков:[1, nil, 3].flatMap { $0 } // works [1, nil, 3].removeNils() // syntax error: type of expression is ambiguous without more context // but it's not all bad, since flatMap can have similar problems when a variable is used: let a = [1, nil, 3] // syntax error: type of expression is ambiguous without more context a.flatMap { $0 } a.removeNils()
Я не очень вникал в это, но, кажется, вы можете добавить:
extension SequenceType { func removeNils() -> Self { return self } }
Если вы хотите иметь возможность вызывать метод для массивов, содержащих необязательные элементы. Это может привести к массовому переименованию (например,
flatMap { $0 }
->removeNils()
) проще.
Присвоение себе отличается от присвоения новой переменной?!
Взгляните на следующий код:
var a: [String?] = [nil, nil] var b = a.flatMap{$0} b // == [] a = a.flatMap{$0} a // == [nil, nil]
Удивительно, но
a = a.flatMap { $0 }
не удаляет nils, когда вы назначаете егоa
, но онудаляет nils, когда вы назначаете егоb
! Я предполагаю, что это имеет какое-то отношение к перегруженномуflatMap
и быстрому выбору того, который мы не собирались использовать.Можно временно решить проблему, приведя ее к ожидаемому типу:
a = a.flatMap { $0 } as [String] a // == []
Но это может быть легко забыть. Вместо этого я бы рекомендовал использовать метод
removeNils()
выше.
Обновление
Похоже, что есть предложение отменить по крайней мере одну из (3) перегрузок
flatMap
: https://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md
Начиная с Swift 2.0 можно добавить метод, который работает для подмножества типов, используя предложения
where
. Как описано в этой темефорума Apple , это может быть использовано для фильтрацииnil
значений массива. Кредиты идут на @nnnnnnnn и @SteveMcQwark.Поскольку предложения
where
еще не поддерживают генераторы (например,Optional<T>
), обходной путь необходим через протокол.protocol OptionalType { typealias T func intoOptional() -> T? } extension Optional : OptionalType { func intoOptional() -> T? { return self.flatMap {$0} } } extension SequenceType where Generator.Element: OptionalType { func flatten() -> [Generator.Element.T] { return self.map { $0.intoOptional() } .filter { $0 != nil } .map { $0! } } } let mixed: [AnyObject?] = [1, "", nil, 3, nil, 4] let nonnils = mixed.flatten() // 1, "", 3, 4
Swift 4
Если вам посчастливилось использовать Swift 4, то вы можете отфильтровать нулевые значения с помощью
compactMap
array = array.compactMap { $0 }
Например
let array = [1, 2, nil, 4] let nonNilArray = array.compactMap { $0 } print(nonNilArray) // [1, 2, 4]
Swift 4
Это работает с Swift 4:
protocol OptionalType { associatedtype Wrapped var optional: Wrapped? { get } } extension Optional: OptionalType { var optional: Wrapped? { return self } } extension Sequence where Iterator.Element: OptionalType { func removeNils() -> [Iterator.Element.Wrapped] { return self.flatMap { $0.optional } } }
Тест:
class UtilitiesTests: XCTestCase { func testRemoveNils() { let optionalString: String? = nil let strings: [String?] = ["Foo", optionalString, "Bar", optionalString, "Baz"] XCTAssert(strings.count == 5) XCTAssert(strings.removeNils().count == 3) let integers: [Int?] = [2, nil, 4, nil, nil, 5] XCTAssert(integers.count == 6) XCTAssert(integers.removeNils().count == 3) } }