在for-in循环中使用尾随闭包

10

我正在使用类似这样在for-in循环中的数组上调用map()函数:

let numbers = [2, 4, 6, 8, 10]

for doubled in numbers.map { $0 * 2 } // compile error
{
    print(doubled)
}

出现编译错误:

未解决的标识符使用“doubled”

但是,如果我为map()函数添加括号,则可以正常工作。例如:

for doubled in numbers.map ({ $0 * 2 })
{
    print(doubled)
}

我的问题是,为什么编译器不能区分尾随函数和循环的代码块,假设这是导致问题的原因?

3个回答

12
这是因为存在关于尾随函数应该操作的上下文的歧义。一个可行的语法是:
let numbers = [2, 4, 6, 8, 10]

for doubled in (numbers.map { $0 * 2 }) // All good :)
{
    print(doubled)
}

我会说这很可能是因为'in'运算符的优先级高于尾随函数。
哪种方式更易读,由您决定。

8
尾随闭包语法在语句体(在您的情况下,是 for 循环,但它也适用于其他语句)和尾随闭包体之间产生已知歧义。虽然从技术上讲可以解决这个问题,但编译器设计者决定禁止在各种高级语句的控制部分中使用尾随闭包语法:

虽然在某些情况下通过执行任意前瞻或在解析时执行类型检查可以确定意图,但这些方法对于编译器的架构有重大影响。因此,我们选择保持解析器简单并禁止使用这种语法。

为了理解冲突的性质,请考虑以下示例:
for v in expr { /* code 1 */ } { /* code 2 */ }

解析器需要在 code 1 块上做出选择。它可以将其用作 expr 的尾闭包,并将 code 2 视为 for 循环的主体;或者将 code 1 用作 for 循环的主体,同时将 code 2 视为由大括号括起来的独立语句组 - 这是经典的 移入-归约冲突
解决这样的冲突非常昂贵。实际上,您的解析器需要继续查看更多的令牌,直到只有一种解释是有意义的,或者解析器用完了令牌(在这种情况下,它会随便做出一个决定,需要程序员在不想要该选择的情况下消除歧义)。
添加括号可以消除歧义。考虑通过添加强制关键字来分离循环控制部分和循环主体以消除歧义,即:
// The syntax of rejected proposal
for doubled in numbers.map { $0 * 2 } do {
    print(doubled) //                 ^^
}

添加一个额外的do关键字,如果存在任何左侧的块,则将其“附加”到表达式上,并使右侧的块成为循环语句的主体。这种方法有一个主要缺点,因为它是一项破坏性的变化。这就是为什么该提案被拒绝的原因。


感谢您的详细解释。 - Muhammad Hassan
@M.Hassan 没关系!我想既然你已经有了一个不错的解决方案,我应该集中精力从编译器编写者的角度来看问题的本质。 - Sergey Kalinichenko

2
这句话的意思是“语法不明确(参见dasblinkenlight的答案)。如果需要替代的语法:”。
let numbers = [2, 4, 6, 8, 10]

numbers.map { $0 * 2 }.forEach {
    print(doubled)
}

或者
let numbers = [2, 4, 6, 8, 10]
let doubledNumbers = numbers.map { $0 * 2 }

for doubled in doubledNumbers {
    print(doubled)
}

2
这不是一种功能性的方法,只是一种方法而已。(如果存在功能性的forEach,它将返回一些有用的东西,而不是依赖于副作用。)我之所以强调这一点,是因为我经常看到forEach出现在不适当的地方(在大多数情况下,for-in是更好的Swift),我怀疑“因为这是功能性的方式”是其中一个原因。在这种情况下,在map/filter链的末尾使用它可能很有用,但不是因为它基于函数式编程(相反),只是因为语法有时与map/filter链更加匹配。 - Rob Napier
@RobNapier 我同意forEach并不是纯粹的函数式(尽管许多程序员称其为函数式,因为你可以将一个函数传递给它,而且“函数式”这个词并没有明确定义)。然而,我不同意for-inforEach更好。for-in有什么优势呢? - Sulthan
2
它尊重return/break/continue(forEachreturn的处理可能会让许多开发人员感到惊讶)。它不会导致闭包捕获。它与Swift的其他流程控制(if、do、while、repeat)相匹配。我认为《The Swift Programming Language》书中解释了mapfor-in,但从未提到过forEach并非偶然。请参阅clattner有关如何“(勉强)自给自足”的评论,因为您可以将非匿名函数传递给它。https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151214/003388.html。 - Rob Napier
1
关于移除它的讨论非常有趣(https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151207/001126.html)。大多数人想要保留它,但通常引用的用途是传递命名函数,而不是作为for-in的替代。同时也有很多讨论关于如何改进它,以便它不再存在当前的问题(但很可能在Swift 3之后实现)。 - Rob Napier
它在 xs?.forEach(doThing) 中的使用也是一个有趣的案例,尽管Chris Lattner建议使用 for x in xs ?? []。其他人指出,这不如 xs?.forEach 版本灵活(如果xs是自定义集合),但同样,这总是被讨论为一个边角情况,而不是“你应该用forEach替换for-in”。 - Rob Napier
1
@RobNapier 感谢您提供的信息。所有内容都非常有趣。我错过了continuebreak之间的区别,因为通常我会通过filterfind或类似的函数来解决这些用例。然而,并不总是那么简单。 - Sulthan

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