Создание расширения для фильтрации 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 44

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 функции:

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. Сглаживание последовательностей путем удаления уровня вложенности (например, [[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]
    }
    
  2. Удаление элементов из последовательности (например, [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 в removeNils()"? следует иметь в виду, что использование flatMap для удаления nils-это не единственное использование #2. На самом деле, поскольку обе версии принимают функцию transform, они могут быть гораздо более мощными, чем приведенные выше примеры.

Например, #1 может легко разбить массив строк на отдельные символы (сгладить) и прописать каждую букву (сопоставить):
["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 } используется для удаления nils, кажется, больше из 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's map функция для получения необязательного типа, который можно развернуть. Если вы опустите эту часть, вы получите следующую синтаксическую ошибку: "ошибка: инициализатор для условной привязки должен иметь необязательный тип, а не' 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)
    }
}