Scala:Case类的unapply与手动实现以及类型擦除

21

我试图理解Scala对Case Classes的处理方式,使它们免受类型擦除警告的影响。

假设我们有以下简单的类结构。这基本上是一个Either

abstract class BlackOrWhite[A, B]

case class Black[A,B]( val left: A ) extends BlackOrWhite[A,B]

case class White[A,B]( val right: B ) extends BlackOrWhite[A,B]

而你正在尝试像这样使用它:

object Main extends App {

    def echo[A,B] ( input: BlackOrWhite[A,B] ) = input match {
        case Black(left) => println( "Black: " + left )
        case White(right) => println( "White: " + right )
    }

    echo( Black[String, Int]( "String!" ) )
    echo( White[String, Int]( 1234 ) )
}

所有东西都能编译并顺利运行。然而,当我尝试自己实现unapply方法时,编译器会抛出警告。我使用了与上述相同的Main类这个类结构:

abstract class BlackOrWhite[A, B]

case class Black[A,B]( val left: A ) extends BlackOrWhite[A,B]

object White {

    def apply[A,B]( right: B ): White[A,B] = new White[A,B](right)

    def unapply[B]( value: White[_,B] ): Option[B] = Some( value.right )

}

class White[A,B]( val right: B ) extends BlackOrWhite[A,B]

使用-unchecked标志编译会发出以下警告:

[info] Compiling 1 Scala source to target/scala-2.9.1.final/classes...
[warn] src/main/scala/Test.scala:41: non variable type-argument B in type pattern main.scala.White[_, B] is unchecked since it is eliminated by erasure
[warn]         case White(right) => println( "White: " + right )
[warn]                   ^
[warn] one warning found
[info] Running main.scala.Main

现在,我理解了类型擦除并尝试使用Manifests来解决警告问题(但迄今为止没有成功),但这两个实现之间有什么区别?Case类是否在做某些需要添加的事情?这可以用Manifests绕过吗?

我甚至尝试使用-Xprint:typer标志在Scala编译器中运行Case类实现,但unapply方法看起来几乎与我预期的相同:

case <synthetic> def unapply[A >: Nothing <: Any, B >: Nothing <: Any](x$0: $iw.$iw.White[A,B]): Option[B] = if (x$0.==(null))
    scala.this.None
else
    scala.Some.apply[B](x$0.right);

你正在使用最新版本的Scala吗?我无法重现你的问题,而几个月前的相关问题确定了一个类似于你的编译器错误。请参见https://dev59.com/2VrUa4cB1Zd3GeqPgxw0 - Destin
我正在使用2.9.1.final版本(在Xubuntu 11.10上,如果有影响的话)。 - Nycto
2个回答

13
我无法提供完整的答案,但是我可以告诉您,尽管编译器为case类生成了一个unapply方法,但是在模式匹配Case类时,并不使用该unapply方法。如果您尝试同时使用内置的Case匹配和自己的unapply方法使用-Ybrowse:typer,则会看到针对match所产生的语法树非常不同,具体取决于使用哪种方法。您还可以浏览后续阶段并观察差异是否仍然存在。

为什么Scala不使用内置的unapply我不确定,可能是因为您提出的原因。至于如何解决自己的unapply方法,我不知道。但这就是Scala似乎神奇地避免问题的原因。

经过实验,显然此版本的unapply有效,尽管我对其原因感到有些困惑:

def unapply[A,B](value: BlackOrWhite[A,B]): Option[B] = value match {
    case w: White[_,_] => Some(w.right)
    case _ => None
}

你的unapply存在的问题在于编译器需要被说服,即如果一个White[A,B]扩展了一个BlackOrWhite[C,D],那么B应该与D相同,在这个版本中编译器似乎能够解决这个问题,但是在你的版本中不能。不确定原因。


2
实际上,unapply 是为了让用户能够复制 case class 提供的功能而创建的。还有一些其他地方,官方解释事物如何工作的方式并非编译器实际执行的方式。 - Daniel C. Sobral

6
我无法告诉您case class match和unapply之间的区别。但在他们的书(Odersky,Spoon,Venners)“Scala编程”第二章26.6“提取器与案例类”的部分中,他们写道:“它们(case class)通常比extractors导致更高效的模式匹配,因为Scala编译器可以优化基于case classes的模式,而不是在extractor上的模式。这是因为case classes的机制是固定的,而提取器中的unapply或unapplySeq方法几乎可以执行任何操作。第三,如果您的case classes继承自一个封闭的基类,则Scala编译器将检查我们的模式匹配是否穷尽,并且如果某些可能值的组合未被模式覆盖,它将发出警告。对于提取器,不存在此类穷尽性检查。”这意味着两者的差异比一开始看到的要大,然而具体的差异没有被具体说明。

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