创建一个扩展来从Swift中的Array过滤nils

Jav*_*wag 46 arrays generics swift swift-extensions

我正在尝试编写一个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
}
Run Code Online (Sandbox Code Playgroud)

但是,我无法将其作为扩展工作.我试图告诉编译器扩展只适用于可选值的数组.这是我到目前为止:

extension Array {
    func filterNils<U, T: Optional<U>>() -> [U] {
        return filter({ $0 != nil }).map({ $0! })
    }
}
Run Code Online (Sandbox Code Playgroud)

(它不编译!)

Chr*_*hen 80

从Swift 2.0开始,您不需要编写自己的扩展来过滤数组中的nil值,您可以使用flatMap它来展平数组并过滤nils:

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

打印:

[a, b, d]
Run Code Online (Sandbox Code Playgroud)

注意:

有2个flatMap功能:

  • @kostek,这里是`flatMap`实现的引用: - 复杂性:O(*M*+*N*),其中*M*是`self`的长度,*N*是结果的长度. (2认同)

Sen*_*ful 62

TL; DR

为避免潜在的错误/混淆,请不要使用array.compactMap { $0 }删除nils; 使用诸如array.flatMap { $0 }替代的扩展方法(下面的实现,为Swift 3.0更新).


尽管array.removeNils()大部分时间都有效,但有几个理由支持array.flatMap { $0 }扩展:

  • array.removeNils() 准确描述您要执行的操作:删除removeNils值.不熟悉的nil人需要查阅,当他们查找时,如果他们密切注意,他们会得到与我下一点相同的结论;
  • flatMap有两个不同的实现,做两个完全不同的事情.基于类型检查,编译器将决定调用哪一个.这在Swift中可能非常有问题,因为类型推断被大量使用.(例如,要确定变量的实际类型,您可能需要检查多个文件.)重构可能导致您的应用程序调用错误的版本,flatMap这可能导致难以发现的错误.
  • 由于有两个完全不同的功能,因此flatMap您可以轻松地将两者混为一谈,这使得理解变得更加困难.
  • flatMap可以在非可选数组(例如flatMap)上调用,所以如果你重构一个数组[Int],[Int?]你可能会意外地留下[Int]编译器不会警告你的调用.它最多只会自行返回,最坏的情况是它会导致其他实现被执行,从而可能导致错误.
  • 在Swift 3中,如果您没有显式转换返回类型,编译器将选择错误的版本,这会导致意外的后果.(参见下面的Swift 3部分)
  • 最后,它会降低编译器的速度,因为类型检查系统需要确定要调用哪个重载函数.

回顾一下,遗憾的是,有两个版本的函数都被命名flatMap { $0 }.

  1. 通过删除嵌套级别来平整序列(例如flatMap)

    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]
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 从序列中删除元素(例如[[1, 2], [3]] -> [1, 2, 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]
    }
    
    Run Code Online (Sandbox Code Playgroud)

#2是人们使用通过传递删除尼尔斯的一个[1, nil, 3] -> [1, 3]作为{ $0 }.这是有效的,因为该方法执行地图,然后过滤掉所有transform元素.

您可能想知道"为什么Apple没有将#2重命名为nil"?要记住的一件事是,使用removeNils()删除nils不是#2的唯一用法.实际上,由于两个版本都采用了一个flatMap函数,因此它们比上面的例子更强大.

例如,#1可以轻松地将字符串数组拆分为单个字符(展平)并将每个字母(地图)大写:

["abc", "d"].flatMap { $0.uppercaseString.characters } == ["A", "B", "C", "D"]
Run Code Online (Sandbox Code Playgroud)

数字#2可以轻松删除所有偶数(展平)并将每个数字乘以transform(地图):

[1, 2, 3, 4, 5, 6].flatMap { ($0 % 2 == 0) ? nil : -$0 } == [-1, -3, -5]
Run Code Online (Sandbox Code Playgroud)

(请注意,最后一个示例可能导致Xcode 7.3旋转很长时间,因为没有明确的类型说明.进一步证明了为什么这些方法应该有不同的名称.)

盲目地使用-1删除flatMap { $0 }s 的真正危险不是在你打开它时nil,而是当你在类似的东西上调用它时[1, 2].在前一种情况下,它将无害地调用#2并返回[[1], [2]].在后一种情况下,您可能认为它会做同样的事情([1, 2]因为没有[[1], [2]]值而无害地返回),但它实际上会返回,nil因为它使用了调用#1.

[1, 2]用于删除flatMap { $0 }s 的事实似乎更多是Swift 社区的 推荐,而不是来自Apple的推荐.也许如果Apple注意到这种趋势,他们最终会提供nil类似的功能.

在那之前,我们只能提出自己的解决方案.


// 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
    }
}
Run Code Online (Sandbox Code Playgroud)

(注意:不要混淆removeNils()......它与element.map本文中讨论的内容无关.它使用flatMapOptional函数来获取可以解包的可选类型.如果省略这部分,你会得到这个语法错误:"错误:条件绑定的初始化程序必须具有可选类型,而不是'Self.Generator.Element'."有关如何map帮助我们的更多信息,请参阅此答案我写了关于在SequenceType上添加扩展方法来计算非nils.)

用法

let a: [Int?] = [1, nil, 3]
a.removeNils() == [1, 3]
Run Code Online (Sandbox Code Playgroud)

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'
Run Code Online (Sandbox Code Playgroud)

(注意在最后一个上,flatMap会返回,map()而removeNils()本来会返回[1, 2, 3, 4].)


该解决方案类似于@fabb链接的答案.

但是,我做了一些修改:

  • 我没有给方法命名[[1], [2, 3], [4]],因为已经有一个flatten序列类型的方法,并且给完全不同的方法赋予相同的名称,这首先使我们陷入了混乱.更不用说错误解释它的含义要容易flatten得多flatten.
  • 而不是创建一个新的类型removeNilsT,它使用相同的名称OptionalType使用(Optional).
  • 而不是执行Wrapped,这导致map{}.filter{}.map{}时间,我循环数组一次.
  • 而不是使用O(M + N)从去flatMapGenerator.Element,我用Generator.Element.Wrapped?.没有必要mapnil函数内部返回值,因此map就足够了.通过避免该map功能,将另一个(即第三个)方法与具有完全不同功能的相同名称混淆起来更加困难.

使用flatMapvs. 的一个缺点removeNils是类型检查器可能需要更多提示:

[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()
Run Code Online (Sandbox Code Playgroud)

我没有太多关注,但似乎你可以添加:

extension SequenceType {
  func removeNils() -> Self {
    return self
  }
}
Run Code Online (Sandbox Code Playgroud)

如果您希望能够在包含非可选元素的数组上调用该方法.这可以使重大的重命名(例如flatMap- > flatMap { $0 })更容易.


分配给self不同于分配给新变量?!

看看下面的代码:

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

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

a = a.flatMap{$0}
a // == [nil, nil]
Run Code Online (Sandbox Code Playgroud)

令人惊讶的是,removeNils()在分配nils时不会删除nils a = a.flatMap { $0 },但是在分配nils时删除nils a!我的猜测是,这与重载b和Swift选择我们不打算使用的那个有关.

您可以通过将其转换为预期类型来暂时解决问题:

a = a.flatMap { $0 } as [String]
a // == []
Run Code Online (Sandbox Code Playgroud)

但这很容易忘记.相反,我建议使用flatMap上面的方法.


更新

似乎有人建议弃用以下至少一个(3)重载removeNils():https://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md

  • 绝对应该是公认的答案. (2认同)

Ant*_*nio 43

不可能限制为通用结构或类定义的类型 - 该数组旨在用于任何类型,因此您无法添加适用于类型子集的方法.只能在声明泛型类型时指定类型约束

实现所需要的唯一方法是创建全局函数或静态方法 - 在后一种情况下:

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)
Run Code Online (Sandbox Code Playgroud)

或者简单地使用compactMap(之前flatMap),可以用来删除所有nil值:

[1, 2, nil, 4].compactMap { $0 } // Returns [1, 2, 4]
Run Code Online (Sandbox Code Playgroud)

  • .flatMap完全相同,但您不需要编写扩展名.你真的应该更新你的答案以反映这一点. (4认同)

Ada*_*ing 12

斯威夫特4

如果您有幸使用Swift 4,那么您可以使用过滤掉nil值 compactMap

array = array.compactMap { $0 }

例如

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

print(nonNilArray)
// [1, 2, 4]
Run Code Online (Sandbox Code Playgroud)

  • 这应该是批准的答案。 (2认同)

fab*_*abb 11

从Swift 2.0开始,可以通过使用where子句添加适用于类型子集的方法.正如Apple Forum Thread中所讨论的,这可用于过滤掉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  
Run Code Online (Sandbox Code Playgroud)


Saj*_*jon 5

斯威夫特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 }
    }
}
Run Code Online (Sandbox Code Playgroud)

测试:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)