TL;博士
斯威夫特 4
使用array.compactMap { $0 }
. Apple 更新了框架,使其不再引起错误/混乱。
斯威夫特 3
为避免潜在的错误/混乱,不要使用array.flatMap { $0 }
删除 nils;使用扩展方法,例如array.removeNils()
代替(下面的实现,为 Swift 3.0 更新)。
虽然array.flatMap { $0 }
大部分时间都有效,但有几个理由支持array.removeNils()
扩展:
removeNils
准确描述您想要做的事情:删除nil
值。不熟悉的flatMap
人要查一下,查了,仔细看,得出的结论和我的下一点是一样的;
flatMap
有两种不同的实现,它们做两种完全不同的事情。基于类型检查,编译器将决定调用哪一个。这在 Swift 中可能是非常有问题的,因为类型推断被大量使用。(例如,要确定变量的实际类型,您可能需要检查多个文件。)重构可能会导致您的应用程序调用错误的版本,flatMap
从而导致难以发现的错误。
- 由于有两个完全不同的功能,因此您可以轻松地将两者混为一谈
flatMap
,从而使理解变得更加困难。
flatMap
可以在非可选数组(例如[Int]
)上调用,所以如果你重构一个数组 from [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 是人们用来通过传递{ $0 }
as来删除 nil 的那个transform
。这是有效的,因为该方法执行映射,然后过滤掉所有nil
元素。
您可能想知道“为什么 Apple 没有将 #2 重命名为removeNils()
”?要记住的一件事是,flatMap
用于删除 nil 并不是#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 }
to remove nil
s 的真正危险不是在你调用它时出现[1, 2]
,而是在你调用它时出现[[1], [2]]
。在前一种情况下,它将无害地调用 #2 并返回[1, 2]
。在后一种情况下,您可能认为它会做同样的事情([[1], [2]]
因为没有nil
值而无害地返回),但实际上它会返回[1, 2]
,因为它使用的是调用 #1。
flatMap { $0 }
用于删除s的事实nil
似乎更多是 Swift社区的 推荐,而不是来自 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
'smap
函数来获取可以解包的可选类型。如果省略这部分,你会得到这个语法错误:“错误:条件绑定的初始化程序必须具有可选类型,而不是'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
to go from Generator.Element
to Generator.Element.Wrapped?
,而是使用map
. 函数内部不需要返回nil
值map
,这样map
就足够了。通过避免使用该flatMap
功能,很难将另一个(即第 3 个)具有相同名称但功能完全不同的方法混为一谈。
使用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'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
删除 nils ,但是当您将它分配给时它确实会删除 nils !我的猜测是,这与重载和 Swift 选择我们不想使用的那个有关。b
flatMap
您可以通过将其转换为预期类型来临时解决问题:
a = a.flatMap { $0 } as [String]
a // == []
但这很容易忘记。相反,我建议使用上述removeNils()
方法。
更新
似乎有一个建议至少弃用(3)个重载之一flatMap
:https ://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md