【问题标题】:Unexpected behavior of StringBuilder in foreachforeach 中 StringBuilder 的意外行为
【发布时间】:2015-09-26 18:39:43
【问题描述】:

在回答 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 !!!!

所以这些陈述显然是不等价的。有人能解释一下区别吗?

【问题讨论】:

  • a.b.c(_)x => a.b.c(x) 的糖(通过 SLS 6.23.1)。 a.b.c _val tmp = a.b; tmp.c _ 的糖(由 SLS 6.26.5 提供)。在前一种情况下,a.b 会被一遍又一遍地评估,而在后一种情况下只会被评估一次。

标签: scala foreach


【解决方案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-expanded。编译器似乎更喜欢创建一个新的匿名函数,它只是包装 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-expansion。在这种情况下,eta-expansion 先出现,相当于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)
}

【讨论】:

    【解决方案2】:

    scala.collection.mutable.StringBuilder 扩展了(Int => Char),因此返回StringBuilderbuilder.append("!")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),因此等价于

    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)
    

    至于您的第四行,IMO 更奇怪。在这种情况下,您正在尝试读取builder.append("!") 中的“字段”apply。但是apply是一个方法(Int)Char,预期的类型(由foreach的参数类型决定)是Int => ?。所以一种方法将apply(Int)Char 提升为Int => ?,即创建一个将调用该方法的lambda。但在这种情况下,由于您尝试将 apply 读取为字段,因此最初,这意味着应该评估 .applythis 一次以存储为this 方法调用的参数,给出相当于这个的东西:

    val this$1: StringBuilder = builder.append("!") // appends "!" once
    val f: Int => Char = (i: Int) => this$1.apply(i)
    (0 until 4).foreach(f) // fetches the 0th to 3rd chars in the string builder, and does nothing with them
    

    【讨论】:

      猜你喜欢
      • 2017-05-19
      • 2018-12-01
      • 1970-01-01
      • 2018-07-08
      • 2013-01-28
      • 2011-08-25
      • 2021-08-12
      • 1970-01-01
      • 2015-02-09
      相关资源
      最近更新 更多