Scala:如何使案例类复制保留清单信息

时间:2012-08-11 22:02:37

标签: scala manifest type-erasure type-parameter

案例类copy()方法应该创建实例的相同副本,并且按名称替换任何字段。当case类具有带有清单的类型参数时,这似乎失败了。该副本失去了对其参数类型的所有知识。

case class Foo[+A : Manifest](a: A) {
  // Capture manifest so we can observe it
  // A demonstration with collect would work equally well
  def myManifest = implicitly[Manifest[_ <: A]]
}

case class Bar[A <: Foo[Any]](foo: A) {
  // A simple copy of foo
  def fooCopy = foo.copy()
}

val foo = Foo(1)
val bar = Bar(foo)

println(bar.foo.myManifest)     // Prints "Int"
println(bar.fooCopy.myManifest) // Prints "Any"

为什么Foo.copy会丢失参数上的清单以及如何保留它?

2 个答案:

答案 0 :(得分:15)

几种Scala特性相互作用以产生这种行为。第一件事是Manifest不仅附加到构造函数上的秘密隐式参数列表,还附加到复制方法。众所周知,

case class Foo[+A : Manifest](a: A)

只是

的语法糖

case class Foo[+A](a: A)(implicit m: Manifest[A])

但这也会影响复制构造函数,它看起来像这样

def copy[B](a: B = a)(implicit m: Manifest[B]) = Foo[B](a)(m)

所有这些implicit m都是由编译器创建的,并通过隐式参数列表发送给方法。

只要在编译器知道copy类型参数的地方使用Foo方法,就可以了。例如,这将在Bar类之外工作:

val foo = Foo(1)
val aCopy = foo.copy()
println(aCopy.myManifest) // Prints "Int"

这是有效的,因为编译器推断fooFoo[Int]因此它知道foo.aInt所以它可以像这样调用copy

val aCopy = foo.copy()(manifest[Int]())

(注意manifest[T]()是一个创建类型T的清单表示的函数,例如Manifest[T],大写字母为“M”。未显示是添加默认参数进入copy。)它也可以在Foo类中工作,因为它已经具有在创建类时传入的清单。它看起来像这样:

case class Foo[+A : Manifest](a: A) {
  def myManifest = implicitly[Manifest[_ <: A]]

  def localCopy = copy()
}

val foo = Foo(1)
println(foo.localCopy.myManifest) // Prints "Int"

然而,在原始示例中,由于第二个特性,它在Bar类中失败:Bar类中的Bar类型参数已知,类型参数类型参数不是。它知道A中的BarFooSubFooSubSubFoo,但如果它是Foo[Int]或{{{} 1}}。当然,这是Scala中众所周知的类型擦除问题,但它似乎是一个问题,即使它看起来似乎没有使用Foo[String]类型参数的类做任何事情。但是,记住,每次调用foo时都会秘密注入一个清单,而这些清单会覆盖之前存在的清单。由于copy类不知道Bar的类型参数是什么,它只是创建了foo的清单,并像这样发送:

Any

如果一个人可以控制def fooCopy = foo.copy()(manifest[Any])类(例如它不是Foo),那么通过添加一个可以进行正确复制的方法,在Foo类中进行所有复制,可以解决这个问题。如上面的List,并返回结果:

localCopy

另一个解决方案是添加case class Bar[A <: Foo[Any]](foo: A) { //def fooCopy = foo.copy() def fooCopy = foo.localCopy } val bar = Bar(Foo(1)) println(bar.fooCopy.myManifest) // Prints "Int" s类型参数作为Foo的显式类型参数:

Bar

但是如果类层次结构很大,那么这个扩展性很差(即,更多成员具有类型参数,并且这些类也具有类型参数),因为每个类都必须具有其下面的每个类的类型参数。在尝试构建case class Bar[A <: Foo[B], B : Manifest](foo: A) { def fooCopy = foo.copy() }

时,它似乎也会使类型推断变得怪异
Bar

答案 1 :(得分:1)

您发现有两个问题。第一个问题是Bar内部的类型擦除问题,其中Bar不知道Foo清单的类型。我个人会使用您建议的localCopy解决方法。

第二个问题是另一个暗示被秘密注入copy。通过将值再次显式传递到copy来解决该问题。例如:

scala> case class Foo[+A](a: A)(implicit val m: Manifest[A @uncheckedVariance])
defined class Foo

scala> case class Bar[A <: Foo[Any]](foo: A) {
     | def fooCopy = foo.copy()(foo.m)
     | }
defined class Bar

scala> val foo = Foo(1)
foo: Foo[Int] = Foo(1)

scala> val bar = Bar(foo)
bar: Bar[Foo[Int]] = Bar(Foo(1))

scala> bar.fooCopy.m
res2: Manifest[Any] = Int

我们看到该副本保留了Int清单,但由于删除,fooCopyres2的类型为Manifest[Any]

因为我需要访问隐式证据来执行copy我必须使用显式implicit(hah)语法而不是上下文绑定语法。但是使用显式语法会导致错误:

scala> case class Foo[+A](a: A)(implicit val m: Manifest[A])
<console>:7: error: covariant type A occurs in invariant position in type => Manifest[A] of value m
       case class Foo[+A](a: A)(implicit val m: Manifest[A])
                                         ^
scala> case class Foo[+A](a: A)(implicit val m: Manifest[_ <: A])
defined class Foo

scala> val foo = Foo(1)
<console>:9: error: No Manifest available for Int.

WTF?为什么上下文绑定语法有效,而显式implicit则不然?我挖出来找到问题的解决方案:@uncheckedVariance注释。

<强>更新

我挖了更多,发现在Scala 2.10中,案例类已更改为复制copy()中第一个参数列表中的字段。

  

马丁说:案件类别仅限于第一个论点   列表其余部分不应复制。

请在https://issues.scala-lang.org/browse/SI-5009查看此更改的详细信息。