在回答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 !!!!
因此这些陈述显然不相同。有人可以解释一下这个区别吗?
答案 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("!")
。
理解这一点的重要一步是B
是C
的语法糖,而不是 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
。
对于A
,builder.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)
}