Scala reduceLeft: 0.asInstanceOf[B]

3

我将继续翻译关于奇怪源代码的系列文章。这是这个系列的一篇

看着Scala 2.12.12中的scala.collection.TraversableOnce#reduceLeft#reducer,我发现了一行非常奇怪的代码:

def reduceLeft[B >: A](op: (B, A) => B): B = {
  if (isEmpty)
    throw new UnsupportedOperationException("empty.reduceLeft")

  object reducer extends Function1[A, Unit] {
    var first = true
    var acc: B = 0.asInstanceOf[B] // <<<<===

    override def apply(x: A): Unit =
      if (first) {
        acc = x
        first = false
      }
      else acc = op(acc, x)
  }
  self foreach reducer
  reducer.acc
}

0.asInstanceOf[B]实际上是什么意思?它是一种使每种类型都可为空的解决方法吗?

例如,有以下代码:

Seq("1", "2").reduceLeft(_ + _)

意思是在运行时执行以下代码。
var acc: B = 0.asInstanceOf[String]

为什么这不能简单地替换为var acc: B = null?因为这将需要引入implicit ev: Null <:< A1或者其他什么?
更新:此外,将Int强制转换为任何其他类型都会抛出异常。
println(0.asInstanceOf[String])

抛出运行时异常:

Exception in thread "main" java.lang.ClassCastException: 
    java.lang.Integer cannot be cast to java.lang.String

但为什么在reducer的情况下它并没有抛出异常呢?

更新2:

深入探究,

def foo[A]: A = 1.asInstanceOf[A]

println(foo[String])                 // 1
println(foo[LocalDateTime])          // 1
println(foo[LocalDateTime].getClass) // java.lang.Integer

源代码


是的,这似乎是一种欺骗编译器产生空值的方法。 - Luis Miguel Mejía Suárez
1
@LuisMiguelMejíaSuárez 是的。但是它的行为非常奇怪...我已经添加了更多的示例到问题中,以展示它的荒谬行为:D - Andrii Abramov
1
坏的类型转换有点像树倒在森林里:只有你“看”它时才会抛出异常。 - Dima
1
如果 B 是一个原始类型,那么 var acc: B = null 将无法工作。 - Dima
2个回答

4

这是部分答案。

确实,如果未将类型B指定为B <: AnyRef,则无法将null赋值给其类型为B的值。但是,Scala泛型在运行时被擦除。去除后装箱的代码可能如下所示:

def reduceLeft(op: Function2): Object = {
  if (isEmpty)
    throw new UnsupportedOperationException("empty.reduceLeft")

  object reducer extends Function1 {
    var first = true
    var acc: Object = BoxesRunTime.boxToInteger(0)

    override def apply(x: Object): Unit =
      if (first) {
        acc = x
        first = false
      }
      else acc = op(acc, x)
  }
  self foreach reducer
  reducer.acc
}

注意,类型转换已经消失了。对于变量 B 的信息为零,因此没有检查可以进行。

与此不同的是:

println(0.asInstanceOf[String])

由于 String 是已知类型,因此此转换不会被擦除。

类型擦除也解释了为什么 foo 的调用可行,因为 foo 的擦除基本上是:

def foo: Object = BoxesRunTime.boxToInteger(1)

回忆一下,println 的定义是 def println(any: Any): Unit,而在 scalac 之后,Any 被替换为 Object。因此,在进行下列操作时:

println(foo[String])                 // 1
println(foo[LocalDateTime])          // 1

结果的1从未被分配给任何需要运行时类检查的内容。您获取的对象直接传递给println

但是,这将导致ClassCastException,因为foo的结果需要向下转换为String,以供printString调用。

def printString(s: String) = println(s)
printString(foo[String])

这段代码也将在运行时失败:

val str = foo[String]

由于Scala会将str的类型推断为String类型,导致运行时无法将其强制转换为该类型。


现在,我不知道的部分是他们为什么没有使用var acc: B = _(只适用于类/对象内部),或者var acc: B = null.asInstanceOf[B](可以在任何您可以定义var的地方使用)。可能只是一行代码在多次重构中幸存下来。


我喜欢你的个人资料中写下的“在大脑日常运转不足时学习 Scala”。 - Andrii Abramov

2
在Scala 2.13中,0.asInstanceOf[B]被更改为null.asInstanceOf[B],见Expression for all zero bits #8767

null.asInstanceOf[A]比0更规范。

这里并不重要,因为该值从未使用且总是被重新分配。

很遗憾,有人开始指责var x: A = _太丑陋了。这仍然是非赋值的最佳表达式。

Scala 3中,我们可以编写

trait Foo[B]:                                                                                                                                        
  var acc: B = compiletime.uninitialized

这句话的意思是“在我看来,它清晰地传达了意图。”
相关:

Scala reduceLeft 实现中的 0.asInstanceOf[B] 是什么意思?


哦,这个问题已经有答案了!我没能找到它。将其标记为重复的并感谢您指出PR。 - Andrii Abramov

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