创build一个扩展来从Swift中的数组中过滤nils

我试图写一个扩展到数组,这将允许一个可选的T数组被转换成一个非可选的T数组。

例如,这可以写成这样一个免费的function:

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

(它不会编译!)

限制为generics结构或类定义的types是不可能的 – 该数组被devise为与任何types一起工作,因此您不能添加适用于子types的方法。 types约束只能在声明generics时指定

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

 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] 

从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函数:

TL; DR

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


虽然array.flatMap { $0 }大部分时间都在工作,但赞成array.removeNils()扩展有几个原因:

  • removeNils 描述了你想要做的事情 :删除nil值。 有人不熟悉flatMap需要查看它,当他们查看它时,如果他们密切关注,他们会得出和我的下一个点相同的结论;
  • flatMap有两个不同的实现,它们完成两个完全不同的事情 。 基于types检查, 编译器将决定哪一个被调用。 这在Swift中可能是非常有问题的,因为type-in​​ference被大量使用。 (例如,要确定variables的实际types,您可能需要检查多个文件。)重构可能会导致您的应用程序调用错误版本的flatMap ,从而导致难以发现的错误
  • 由于有两个完全不同的function,因此可以使得理解flatMap变得更加困难,因为您可以很容易地将这两者混为一谈
  • 可以在非可选数组(例如[Int] )上调用flatMap ,所以如果将数组从[Int?]重构为[Int] ,可能会不小心留下flatMap { $0 }调用 ,编译器不会提醒您。 最好它只会返回自己,在最坏的情况下,会导致其他实现被执行,可能导致错误。
  • 在Swift 3中,如果你没有显式的返回types, 编译器会select错误的版本 ,这会导致意想不到的后果。 (见下面的Swift 3部分)
  • 最后,它会减慢编译器的速度,因为types检查系统需要确定要调用哪个重载函数。

回顾一下,这个函数有两个版本,不幸的是,命名为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的方法。 这是因为该方法执行映射,然后过滤掉所有nil元素。

您可能想知道“为什么苹果不重命名#2 removeNils() ”? 有一件事要记住,使用flatMap删除nils不是#2的唯一用法。 实际上,由于两个版本都具有transformfunction,所以它们可以比上面的例子更加强大。

例如,#1可以很容易地将一个string数组分割成单个字符(拼合),并大写每个字母(地图):

 ["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旋转很长一段时间,因为没有明确的types,进一步certificate为什么这些方法应该有不同的名称)。

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

flatMap { $0 }被用来删除nil的事实似乎更多的是Swift 社区的 推荐,而不是来自Apple的推荐。 也许如果苹果注意到这个趋势,他们最终会提供一个removeNils()函数或类似的东西。

在此之前,我们只剩下我们自己的解决scheme了。


 // 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没有任何关系,它使用Optionalmap函数来获取可展开的可选types,如果省略这部分,你会得到这样的语法错误:“错误:条件绑定的初始值设定项必须具有可选types,而不是'Self.Generator.Element'”有关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]] 。)


解决scheme类似于@fabb链接到的答案 。

但是,我做了一些修改:

  • 我没有把这个方法命名为flatten ,因为已经有了一个flatten的序列types方法,并且把完全不同的方法命名为同一个名字,这首先使我们陷入混乱之中。 更不用说,它更容易误解什么是flatten不是removeNils
  • 而不是在OptionalType上创build一个新的typesT ,它使用与Optional使用( Wrapped )相同的名称。
  • 而不是执行map{}.filter{}.map{} ,导致O(M + N)时间,我循环一次数组。
  • 而不是使用flatMapGenerator.ElementGenerator.Element.Wrapped? ,我用mapmap函数内部不需要返回nil值,所以map就足够了。 通过避免使用flatMap函数,很难将具有完全不同function的另一个(即第三个)方法混合在一起。

使用removeNils vs. flatMap的一个缺点是types检查器可能需要更多的提示:

 [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() )更容易。


分配给自己不同于分配给一个新的variables?!

看看下面的代码:

 var a: [String?] = [nil, nil] var b = a.flatMap{$0} b // == [] a = a.flatMap{$0} a // == [nil, nil] 

令人惊讶的是, a = a.flatMap { $0 }不会在您将其分配给a时删除nils,但是当您将其分配给b时,它将删除nils! 我的猜测是,这与重载的flatMap和Swiftselect我们不想使用的那个有关。

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

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

但这很容易忘记。 相反,我会build议使用上面的removeNils()方法。

从Swift 2.0开始,可以通过使用where子句添加一个方法来处理子types。 正如在这个Apple Forum Thread中所讨论的,这可以用来过滤一个数组的nil值。 积分转到@nnnnnnnn和@SteveMcQwark。

由于where子句尚不支持generics(如Optional<T> ),因此需要通过Protocol协议解决方法。

 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 

斯威夫特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 } } } 

testing:

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