Подтвердить что ты не робот

Создание расширения для фильтрации нилей из массива в 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! })
    }
}

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

4b9b3361

Ответ 1

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

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

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)

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

[1, 2, nil, 4].flatMap { $0 } // Returns [1, 2, 4]

Ответ 2

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

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

Печать

[a, b, d]

Примечание:

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

Ответ 3

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 - это тот, который люди используют для удаления nils, передавая { $0 } как transform. Это работает, поскольку метод выполняет отображение, а затем отфильтровывает все элементы nil.

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

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

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

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

[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 } используется для удаления nil, как представляется, является скорее сообществом 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 map function, чтобы получить необязательный тип, который может быть распакован. Если вы опустите эту часть, вы получите эту синтаксическую ошибку: "error: initializer для условной привязки должен иметь необязательный тип, а не" Self ".Generator.Element '." Для получения дополнительной информации о том, как map() помогает нам, см. этот ответ, я написал о добавлении метода расширения в SequenceType для подсчета non-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 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, сложнее скомпоновать еще один (т.е. Третий) метод с тем же именем, который имеет совершенно другую функцию.

Единственный недостаток использования removeNils vs. flatMap заключается в том, что для проверки типа может потребоваться немного больше намеков:

[1, nil, 3].flatMap { $0 } // works
[1, nil, 3].removeNils() // syntax error: type of expression is ambiguous without more context

// but it 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()).


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

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

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 и Swift, которые мы не хотели использовать.

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

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

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

Ответ 4

С Swift 2.0 можно добавить метод, который работает для подмножества типов с помощью предложений where. Как обсуждалось в этом Apple Forum Thread, это можно использовать для фильтрации значений nil массива. Кредиты идут в @nnnnnnnn и @SteveMcQwark.

Как where предложения еще не поддерживают generics (например, 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  

Ответ 5

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)
    }
}