Создание расширения для фильтрации nils из массива в Swift

Я пытаюсь написать расширение для массива, которое позволит преобразовать массив необязательных T в массив необязательных T.

например, это может быть записано как свободная функция, например:

func removeAllNils(array: [T?]) -> [T] {
    return array
        .filter({  != nil })   // remove nils, still a [T?]
        .map({ ! })            // convert each element from a T? to a T
}

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

extension Array {
    func filterNils<U, T: Optional<U>>() -> [U] {
        return filter({  != nil }).map({ ! })
    }
}

(он не компилируется!)

6 ответов


невозможно ограничить тип, определенный для универсальной структуры или класса - массив предназначен для работы с любым типом, поэтому вы не можете добавить метод, который работает для подмножества типов. Ограничения типа могут быть указаны только при объявлении универсального типа

единственный способ достичь того, что вам нужно, - это создать глобальную функцию или статический метод-в последнем случае:

extension Array {
    static func filterNils(array: [T?]) -> [T] {
        return array.filter {  != nil }.map { ! }
    }
}

var array:[Int?] = [1, nil, 2, 3, nil]

Array.filterNils(array)

или просто использовать compactMap (ранее flatMap), который можно использовать для удалить все нулевые значения:

[1, 2, nil, 4].compactMap {  } // Returns [1, 2, 4]

начиная с Swift 2.0, вам не нужно писать собственное расширение для фильтрации нулевых значений из массива, вы можете использовать flatMap, который выравнивает массив и фильтрует nils:

let optionals : [String?] = ["a", "b", nil, "d"]
let nonOptionals = optionals.flatMap{}
print(nonOptionals)

принты:

[a, b, d]

Примечание:

есть 2 flatMap функции:


TL; DR

чтобы избежать потенциальных ошибок / путаницы, не используйте array.flatMap { } чтобы удалить nils; используйте метод расширения, такой как (реализация ниже обновлено для Swift 3.0).


хотя array.flatMap { } работает большую часть времени, есть несколько причин в пользу :

  • removeNils описывает именно то, что вы хотите сделать: удалить nil значения. Кто-то не знакомый с flatMap нужно будет посмотреть, и, когда они это сделают, если они обратят пристальное внимание, они придут к тому же выводу, что и мой следующий пункт;
  • flatMap имеет две разные реализации, которые делают две совершенно разные вещи. На основе проверки типа, компилятор собирается решать! который вызывается. Это может быть очень проблематично в Swift, так как вывод типа используется в значительной степени. (Например. для определения фактического типа a переменная, вам может потребоваться проверить несколько файлов.) Рефакторинг может привести к тому, что ваше приложение вызовет неправильную версию flatMap что может привести к трудно найти ошибки.
  • поскольку есть две совершенно разные функции, это делает понимание flatMap гораздо сложнее, так как вы можете легко объединить два.
  • flatMap может вызываться на необязательных массивах (например,[Int]), поэтому, если вы рефакторинг массив от [Int?] to [Int] вы можете случайно оставить позади flatMap { } звонки о котором компилятор вас не предупредит. В лучшем случае он просто вернется сам, в худшем-приведет к выполнению другой реализации, что потенциально приведет к ошибкам.
  • в 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: , repeatedValue: ) }
        ///     // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
        ///
        ///     let flatMapped = numbers.flatMap { Array(count: , repeatedValue: ) }
        ///     // [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-это тот, который люди используют для удаления nils, передавая { } as transform. Это работает, так как метод выполняет карту, а затем отфильтровывает все nil элементы.

Вам может быть интересно " почему Apple не переименовала #2 в removeNils()"? одна вещь, чтобы иметь в виду, что использование flatMap удалить nils-это не единственное использование #2. На самом деле, так как обе версии принимают transform функция, они могут значительно более мощные, чем приведенные выше примеры.

например, #1 может легко разбить массив строк на отдельные символы (сгладить) и заглавными буквами (карта):

["abc", "d"].flatMap { .uppercaseString.characters } == ["A", "B", "C", "D"]

в то время как номер #2 может легко удалить все четные числа (сгладить) и умножить каждое число на -1 (карта):

[1, 2, 3, 4, 5, 6].flatMap { ( % 2 == 0) ? nil : - } == [-1, -3, -5]

(обратите внимание, что этот последний пример может заставить Xcode 7.3 вращаться очень долго, потому что нет явных типов. Дополнительные доказательства того, почему методы должны иметь разные названия.)

реальная опасность слепого использования flatMap { } удалить nils не приходит, когда вы вызываете его на [1, 2], но скорее, когда вы называете это чем-то вроде [[1], [2]]. В первом случае он вызовет вызов #2 без вреда и вернет [1, 2]. В последнем случае вы можете подумать, что он сделает то же самое (безвредно return [[1], [2]] так как нет nil значения), но он фактически вернет [1, 2] С помощью вызова #1.

тот факт, что flatMap { } используется для удаления nils кажется более быстрым сообщество рекомендация а не один из Apple. Возможно, если Apple заметит эту тенденцию, они в конечном итоге предоставят }) { result.append(element) } } return result } }

(Примечание: не путайте с element.map... это не имеет никакого отношения к flatMap обсуждаться в этом посте. Он использует Optional ' s map функции чтобы получить дополнительный тип, который можно развернуть. Если вы опустите эту часть, вы получите следующую синтаксическую ошибку: "ошибка: инициализатор для условной привязки должен иметь необязательный тип, а не 'Self.Генератор.Элемент"."Для получения дополнительной информации о том, как map() помогает нам увидеть этот ответ я написал о добавлении метода расширения в SequenceType для подсчета нон-Нильс.)

использование

let a: [Int?] = [1, nil, 3]
a.removeNils() == [1, 3]

пример

var myArray: [Int?] = [1, nil, 2]
assert(myArray.flatMap {  } == [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 {  } == [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 {  } == [[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 {  } == [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 on OptionalType, он использует то же имя, что Optional использует (Wrapped).
  • вместо выполняя map{}.filter{}.map{}, что приводит к O(M + N) время, я петлю через массив однажды.
  • вместо flatMap от Generator.Element to Generator.Element.Wrapped?, я использую map. Нет необходимости возвращаться nil значения внутри тегов , так что map хватит. Избегая flatMap функция, сложнее объединить еще один (т. е. 3-й) метод с тем же именем, который имеет совершенно другую функцию.

один недостаток использования removeNils и flatMap это то, что type-checker может потребоваться немного больше намекая:

[1, nil, 3].flatMap {  } // 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 {  }
a.removeNils()

я не смотрел в него много, но, кажется, вы можете добавить:

extension SequenceType {
  func removeNils() -> Self {
    return self
  }
}

если вы хотите иметь возможность вызывать метод для массивов, содержащих необязательные элементы. Это может сделать массовое переименование (например,flatMap { } ->removeNils()) легче.


назначение self отличается от назначения новой переменной?!

взгляните на следующий код:

var a: [String?] = [nil, nil]

var b = a.flatMap{}
b // == []

a = a.flatMap{}
a // == [nil, nil]

удивительно, a = a.flatMap { } не удаляет nils, когда вы назначаете его a, но это тут удалите nils, когда вы назначаете его b! Я предполагаю, что это имеет какое-то отношение к перегруженному flatMap и Свифт выбрал тот, который мы не хотели использовать.

вы можете временно решить проблему, приведя ее к ожидаемому типу:

a = a.flatMap {  } 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 значения массива. Кредиты идут на @nnnnnnnnn и @SteveMcQwark.

As where предложения еще не поддерживают дженерики (например,Optional<T>), обходной путь необходим через протокол.

protocol OptionalType {  
    typealias T  
    func intoOptional() -> T?  
}  

extension Optional : OptionalType {  
    func intoOptional() -> T? {  
        return self.flatMap {}  
    }  
}  

extension SequenceType where Generator.Element: OptionalType {  
    func flatten() -> [Generator.Element.T] {  
        return self.map { .intoOptional() }  
            .filter {  != nil }  
            .map { ! }  
    }  
}  

let mixed: [AnyObject?] = [1, "", nil, 3, nil, 4]  
let nonnils = mixed.flatten()    // 1, "", 3, 4  

Swift 4

Если вам посчастливилось использовать Swift 4, Вы можете отфильтровать значения nil с помощью compactMap

array = array.compactMap { }

Е. Г.

let array = [1, 2, nil, 4]
let nonNilArray = array.compactMap {  }

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 { .optional }
    }
}