在foreach中StringBuilder的意外行为

时间:2015-09-26 18:39:43

标签: scala foreach

在回答this question时,我偶然发现了一个无法解释的行为。

来自:

val builder = new StringBuilder("foo bar baz ")

(0 until 4) foreach { builder.append("!") }

builder.toString -> res1: String = foo bar baz !

问题似乎很清楚,提供给foreach的函数缺少Int参数,因此StringBuilder.apply被执行了。但这并没有真正解释为什么它附加'!'只有一次。所以我开始尝试..

我原本期望以下六个陈述是等价的,但结果字符串不同:

(0 until 4) foreach { builder.append("!") }               -> res1: String = foo bar baz !
(0 until 4) foreach { builder.append("!")(_) }            -> res1: String = foo bar baz !!!!
(0 until 4) foreach { i => builder.append("!")(i) }       -> res1: String = foo bar baz !!!!

(0 until 4) foreach { builder.append("!").apply }         -> res1: String = foo bar baz !
(0 until 4) foreach { builder.append("!").apply(_) }      -> res1: String = foo bar baz !!!!
(0 until 4) foreach { i => builder.append("!").apply(i) } -> res1: String = foo bar baz !!!!

因此这些陈述显然不相同。有人可以解释一下这个区别吗?

2 个答案:

答案 0 :(得分:1)

scala.collection.mutable.StringBuilder扩展(Int => Char),因此返回builder.append("!")的{​​{1}}是StringBuilder的有效函数参数。因此,第一行与您写的相同:

foreach

追加的所有线条!!!!实际上创建了一个新的匿名函数val f: Int => Char = builder.append("!").asInstanceOf[Int => Char] // appends "!" once (0 until 4).foreach(f) // fetches the 0th to 3rd chars in the string builder, and does nothing with them ,因此等同于

i => builder.append("!").apply(i)

至于你的第四行,它是奇怪的IMO。在这种情况下,您正在尝试阅读"字段" val f: Int => Char = (i: Int) => builder.append("!").apply(i) (0 until 4).foreach(f) // appends 4 times (and fetches the 0th to 3rd chars in the string builder, and does nothing with them) 中的apply。但builder.append("!")是方法apply,预期类型(由(Int)Char的参数类型确定)为foreach。所以 是一种将方法Int => ?解除为apply(Int)Char的方法,即创建一个将调用该方法的lambda。但在这种情况下,由于您尝试将Int => ?视为字段,因此最初,这意味着apply的{​​{1}}应该评估一次要存储为方法调用的this参数的捕获,给出与此类似的内容:

.apply

答案 1 :(得分:1)

让我们给他们贴上标签:

  • A - (0 until 4) foreach { builder.append("!").apply }
  • B - (0 until 4) foreach { builder.append("!").apply(_) }
  • C - (0 until 4) foreach { i => builder.append("!").apply(i) }
乍一看,这很令人困惑,因为看起来它们应该完全相同。我们先来看看C。如果我们将其视为Function1,那么应该很清楚,每次调用都会评估builder.append("!")

val C = new Function1[Int, StringBuilder] {
    def apply(i: Int): StringBuilder = builder.append("!").apply(i)
}

对于(0 to 4)中的每个元素,调用C,在每次调用时重新评估builder.append("!")

理解这一点的重要一步是BC的语法糖,而不是 A。使用apply(_)中的下划线告诉编译器创建一个 new 匿名函数i => builder.append("!").apply(i)。我们可能不一定会期望这一点,因为builder.append("!").apply可以是它自己的权利,如果是eta扩展的话。编译器似乎更喜欢创建一个新的匿名函数,它只包装builder.append("!").apply,而不是eta扩展它。

来自SLS 6.23.1 - Placeholder Syntax for Anonymous Functions

  

语法类别Expr的表达式e绑定下划线部分u,如果以下两个条件成立:(1)e正确包含u,以及(2)没有其他语法类别Expr的表达式正确包含在e中它本身适当地包含了你。

所以builder.append("!").apply(_)正确包含下划线,因此下划线语法可以应用于匿名函数,它变为i => builder.append("!").apply(i),如C

将其与:

进行比较
(0 until 4) foreach { builder.append("!").apply _ }

此处,下划线未正确包含在表达式中,因此下划线语法不会立即应用,因为builder.append("!").apply _也可能意味着eta扩展。在这种情况下,首先是eta扩展,这相当于A

对于Abuilder.append("!").apply被隐式地eta扩展为一个函数,该函数仅评估builder.append("!")一次。例如它是之类的

val A = new Function1[Int, Char] {
    private val a = builder.append("!")

    // append is not called on subsequent apply calls
    def apply(i: Int): Char = a.apply(i)
}