为什么 `map(foo)` 和 `map{ foo($0) }` 返回的结果不同?

8
以下代码片段,为什么 res1res2 的值不同?
func test() {
    let a: String? = nil
    let b: String? = nil

    let foo: (String?) -> Int = { $0 == nil ? 0 : 1}

    let res1 = [a, b].compactMap { $0 }.map(foo)
    let res2 = [a, b].compactMap { $0 }.map { foo($0) }

    print("res1: ", res1)
    print("res2: ", res2)
}

输出结果为:

res1:  [0, 0]
res2:  []

2
在我看来,这似乎是一个错误。在我看来,两者都应该返回一个空集合。 - Leo Dabus
有趣的是,compacted() 对于每个结果都产生了正确的结果。现在通常都使用 compacted。维护算法包的人最关心这个。 - user652038
@Jessy,“compacted()”行为不同的原因是类型完全不同——因为“compacted()”返回一个新的“CompactedSequence”结构体,编译器无法像对待“Array”一样隐式地向上转换它(在编译器中具有特殊的变异处理),因此类型检查器可以非常直接地选择适合的类型。 - Itai Ferber
1个回答

5

这似乎是编译器根据下游操作选择[a, b].compactMap类型的结果。您可以通过检查参数在函数中传递时的类型来了解此情况:

func printType<T>(_ i: String, _ v: T) -> T {
  print(i, "->", T.self)
  return v
}

func test() {
    let a: String? = nil
    let b: String? = nil

    let foo: (String?) -> Int = { $0 == nil ? 0 : 1 }

    let res1 = printType("cm1", printType("a1", [a, b]).compactMap { $0 }).map(foo)
    let res2 = printType("cm2", printType("a2", [a, b]).compactMap { $0 }).map { foo($0) }

    print("res1: ", res1)
    print("res2: ", res2)
}

test()
// a1 -> Array<Optional<Optional<String>>>
// cm1 -> Array<Optional<String>>
// a2 -> Array<Optional<String>>
// cm2 -> Array<String>
// res1:  [0, 0]
// res2:  []

看起来:
1. 对于 `res1`: - 因为 `foo` 接受一个 `String`, 所以 `map(foo)` 的类型会是一个 `String?` 被传递出去 —— 为了让 `map` 接收到 `String?`,`compactMap` 必须返回一个 `[String?]` - 为了让 `compactMap` 返回一个 `[String?]`,它的输入必须是一个 `[String??]` - 尽管 `[a, b]` 默认情况下是一个 `[String?]`,编译器也可以将其隐式地 向上转型 成一个 `[String??]`,这就是它所做的 - 因此,仅从 `[String??]` 中删除了一层选项,每个 `String?` 值都被传递给了 `map` 和 `foo` 2. 对于 `res2`,编译器由于 `map { foo($0) }` 的存在而没有受到严格限制,因为 `$0` 可以在传递给 `foo` 之前在闭包中被隐式向上转型,并且这正是编译器所喜欢的: - `$0` 是一个 `String`,在传递给 `foo` 之前被上转型为 `String?` - 为了让 `map` 接收到 `String` 值,`compactMap` 必须返回一个 `[String]` - 由于 `compactMap` 返回一个 `[String]`,它的输入必须是一个 `[String?]`,而 `[a, b]` 正是如此,无需上转型 - 因此,现在 `compactMap` 实际上将 `nil` 从 `[String?]` 中过滤出来并转换成 `[String]`,由于没有值留下,`foo` 没有被调用(可以通过 `foo` 内部进行更多打印语句来查看这一点) 这种情况在编译器中是非常具体的,并且你恰好找到了其中发生的一个特定情况。例如,编译器会以与多语句闭包不同的方式解析单语句闭包的结果:如果将 `compactMap { $0 }` 改为 `compactMap { _ = 1 + 1; return $0 }`,则闭包将被解析为不同的形式,并且类型检查将按照不同的顺序进行,导致两种情况都返回 `[]`。
let res1 = printType("cm1", printType("a1", [a, b]).compactMap { _ = 1 + 1; return $0 }).map(foo)
let res2 = printType("cm2", printType("a2", [a, b]).compactMap { _ = 1 + 1; return $0 }).map { foo($0) }

// a1 -> Array<Optional<String>>
// cm1 -> Array<Optional<String>>
// a2 -> Array<Optional<String>>
// cm2 -> Array<Optional<String>>
// res1:  [0, 0]
// res2:  [0, 0]

在这种情况下,编译器实际上在两个实例中都倾向于选择令人惊奇的情况,因为return $0允许编译器从String?String??(然后从[String??][String?])进行向上转型! 无论如何,这似乎值得在https://bugs.swift.org 上报告,因为这种行为非常令人惊讶。如果您继续提交报告,我将很高兴添加测试用例和注释到报告中。

1
感谢详细的解释。我已提交了一份报告(https://bugs.swift.org/browse/SR-16075)。 - Boxuan Zhang
2
@BoxuanZhang 很好!希望你不介意,我在问题中添加了更多的细节。 - Itai Ferber

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接