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

Я пытаюсь написать расширение для Array, которое позволит преобразовать массив необязательных 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! })
    }
}

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


person Javawag    schedule 28.01.2015    source источник


Ответы (7)


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

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

extension Array {
    static func filterNils(_ array: [Element?]) -> [Element] {
        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]
person Antonio    schedule 28.01.2015
comment
Достаточно справедливо, теперь, когда я думаю об этом, это имеет смысл - я просто подумал, что он сделает этот конкретный метод доступным только для необязательных типов, но, конечно, он попытается применить ко всем из них. Итак, я думаю, я буду делать то, что вы тогда предложили, или создам бесплатную функцию вне класса Array! - person Javawag; 28.01.2015
comment
.flatMap делает то же самое, но вам не нужно писать расширение. Вам действительно следует обновить свой ответ, чтобы отразить это. - person Claus Jørgensen; 10.02.2017
comment
Начиная со Swift 4, flatMap устарел. Мы можем использовать compactMap вместо flatMap - person Dinesh Balasubramanian; 19.05.2018
comment
@ Антонио В итоге я сделал это: func filterNils<T>() -> [T] where Element == T? { self.compactMap { $0 } } - person Sentry.co; 15.07.2020

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

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

Отпечатки:

[a, b, d]

Примечание:

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

person Chris Trevarthen    schedule 11.08.2015
comment
Единственная плохая вещь (незначительная) - это сложность: O (M + N) вместо O (N). Похоже, что внутри flatMap это соединение map и filter вместо немного более быстрой собственной реализации с 1 циклом. Но для этого вопроса это не проблема - person zxcat; 10.03.2016
comment
@zxcat Что такое М и Н? - person kostek; 13.04.2016
comment
@kostek, вот цитата из реализации flatMap: - Сложность: O(M + N), где M — длина self и N — длина результата. - person zxcat; 13.04.2016
comment
Хотя flatMap можно использовать большую часть времени, на самом деле это гораздо более мощная функция, которая может привести к непредвиденным последствиям. Вместо этого я бы рекомендовал использовать removeNils. - person Senseful; 24.07.2016
comment
Есть 2 функции flatMap (перегруженные), одна для возврата конкатенированных результатов, а другая для возврата ненулевых значений. См. - developer.apple.com/documentation/swift/sequence/ - person user1046037; 13.08.2017

TL;DR

Свифт 4

Используйте 1_. Apple обновила фреймворк, чтобы он больше не вызывал ошибок/путаницы.

Свифт 3

Чтобы избежать потенциальных ошибок/путаницы, не используйте array.flatMap { $0 } для удаления нулей; вместо этого используйте метод расширения, такой как 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 для удаления нулей — не единственное использование #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 возникает не тогда, когда вы вызываете его для [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 function, чтобы получить необязательный тип, который можно развернуть. Если вы пропустите эту часть, вы получите следующую синтаксическую ошибку: " ошибка: инициализатор для условной привязки должен иметь дополнительный тип, а не 'Self.Generator.Element'." Дополнительную информацию о том, как map() помогает нам, см. >этот ответ я написал о добавлении метода расширения в SequenceType для подсчета ненулевых значений.)

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

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) время, я перебираю массив один раз.
  • Вместо использования flatMap для перехода от 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 } не удаляет нули, когда вы назначаете его a, но удаляет нули, когда вы назначаете его b! Я предполагаю, что это как-то связано с перегруженным flatMap и Swift, выбравшим тот, который мы не собирались использовать.

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

a = a.flatMap { $0 } as [String]
a // == []

Но об этом можно легко забыть. Вместо этого я бы рекомендовал использовать описанный выше метод removeNils().


Обновлять

Похоже, есть предложение отказаться от поддержки хотя бы одной из (3) перегрузок flatMap: https://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md

person Senseful    schedule 24.07.2016
comment
Я категорически не согласен с частью Will lead to bugs. Да, конечно, это неопределенное поведение, когда вы внезапно меняете [Int?] на [Int]... но, как вы упомянули, вывод типов будет автоматически определять типы... ЕДИНСТВЕННЫЙ случай, когда это будет ошибкой, это когда .flatMap является последней строкой вашего кода (как и ваши модульные тесты), что означает, что это не было необходимо для начала. потому что вы будете использовать его результат где-нибудь! в пользовательском интерфейсе привязать к какой-либо другой переменной, вернуться из метода... и компилятор поймает эту ошибку там... - person farzadshbfn; 23.04.2017
comment
Но я согласен, что эта часть была действительно запутанной: a = a.flatMap{$0} a // == [nil, nil] - person farzadshbfn; 23.04.2017
comment
Причина, по которой a остается [nil, nil], заключается в том, что a по-прежнему относится к типу [Int?]. В котором действительны nils. - person Tjeerd in 't Veen; 05.05.2017
comment
Это решение больше не работает в Swift 4, я еще не заставил его работать. Любая подсказка? - person Sajjon; 18.06.2017
comment
Подумайте о том, чтобы разместить код решения ближе к началу ответа. - person Petrus Theron; 12.09.2018

Свифт 4

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

array = array.compactMap { $0 }

E.g.

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

print(nonNilArray)
// [1, 2, 4]
person Adam Wareing    schedule 26.06.2018
comment
Это должен быть утвержденный ответ. - person masty; 27.06.2018

Начиная с 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  
person fabb    schedule 17.06.2015
comment
В окончательной версии Swift ответ @Chris Trevarthen стал намного проще. Единственный плюс этого решения заключается в том, что flatten нельзя вызывать для массива, который уже не содержит никаких опций. - person fabb; 09.11.2015
comment
... и что элегантнее писать только flatten(), чем flatMap{$0}... но я согласен, что это слишком много кода только для этого... кстати, почему вы не используете flatMap в расширении вместо filter и map? - person User; 29.08.2016
comment
Насколько я помню, flatMap добавили в Options только после 2.0, мой ответ старше этого - person fabb; 29.08.2016
comment
На самом деле это не так уж много кода, учитывая, что OptionalType также полезен для других вещей, таких как stackoverflow.com/questions/33138712/ это предпочтительнее, чем каждый раз писать flatMap{$0}. - person User; 30.08.2016

Свифт 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)
    }
}
person Sajjon    schedule 18.06.2017

Решение для Swift 5.3 и выше

extension Array where Element == Any? {
   /**
    * Remove optionals from array
    * ## Examples:
    * Array.filterNils([2,nil,1,0]) // [2,1,0]
    * let someArr: [Int?] = [2,nil,1,0]
    * Array.filterNils(someArr) // [2,1,0]
    */
   static func filterNils<T>(_ array: [T?]) -> [T] {
      return array.compactMap { $0 }
   }
}
person Sentry.co    schedule 07.07.2020
comment
Зачем вам нужно определять where Element == Any?, если функция все равно статическая? - person Ferdz; 14.05.2021
comment
Хороший вопрос. Я думаю, что это было нестатично. А потом я перешел на статику и не стал менять пункт. - person Sentry.co; 19.05.2021