case类复制'方法'与超类

时间:2012-09-11 12:56:19

标签: class scala inheritance enumeration

我想做这样的事情:

sealed abstract class Base(val myparam:String)

case class Foo(override val myparam:String) extends Base(myparam)
case class Bar(override val myparam:String) extends Base(myparam)

def getIt( a:Base ) = a.copy(myparam="changed")

我不能,因为在getIt的上下文中,我没有告诉编译器每个Base都有一个'copy'方法,但是copy也不是一个真正的方法所以我认为没有特性或者抽象方法我可以放入Base以使其正常工作。或者,有吗?

如果我尝试将Base定义为abstract class Base{ def copy(myparam:String):Base },则case class Foo(myparam:String) extends Base会生成class Foo needs to be abstract, since method copy in class Base of type (myparam: String)Base is not defined

是否有其他方法告诉编译器所有Base类在其实现中都是case类?一些特征意味着“具有案例类的属性”?

我可以让Base成为一个案例类,但后来我得到编译器警告,说不推荐使用case类的继承吗?

我知道我也可以:

def getIt(f:Base)={ 
  (f.getClass.getConstructors.head).newInstance("yeah").asInstanceOf[Base]
}

但......看起来非常难看。

思考?我的整个方法是“错误的”吗?

UPDATE 我更改了基类以包含该属性,并使案例类使用“override”关键字。这更好地反映了实际问题,并且考虑到Edmondo1984的响应,使问题更加真实。

7 个答案:

答案 0 :(得分:22)

在问题发生变化之前,这是旧答案。

强类型编程语言会阻止您尝试执行的操作。让我们看看为什么。

具有以下签名的方法的想法:

def getIt( a:Base ) : Unit

该方法的主体是否能够访问通过Base类或接口可见的属性,即仅在Base类/接口或其父级上定义的属性和方法。在代码执行期间,传递给getIt方法的每个特定实例可能具有不同的子类,但编译类型a将始终为Base

可以这样说:

  

好的我有一个类Base,我在两个case类中继承它,然后添加一个   具有相同名称的属性,然后我尝试访问该属性   Base的实例。

一个简单的例子说明了为什么这是不安全的:

sealed abstract class Base
case class Foo(myparam:String) extends Base
case class Bar(myparam:String) extends Base
case class Evil(myEvilParam:String) extends Base

def getIt( a:Base ) = a.copy(myparam="changed")

在下面的例子中,如果编译器在编译时没有抛出错误,则意味着代码将尝试访问运行时不存在的属性。在严格类型化的编程语言中,这是不可能的:您已编写了可编写代码的限制,以便编译器对代码进行更强大的验证,因为这会大大减少代码可能包含的错误数量


这是新答案。这有点长,因为在得出结论之前需要几点

不幸的是,您不能依赖案例类复制的机制来实现您的建议。复制方法的工作方式只是一个复制构造函数,您可以在非案例类中实现它。让我们创建一个case类并在REPL中反汇编:

scala>  case class MyClass(name:String, surname:String, myJob:String)
defined class MyClass

scala>  :javap MyClass
Compiled from "<console>"
public class MyClass extends java.lang.Object implements scala.ScalaObject,scala.Product,scala.Serializable{
    public scala.collection.Iterator productIterator();
    public scala.collection.Iterator productElements();
    public java.lang.String name();
    public java.lang.String surname();
    public java.lang.String myJob();
    public MyClass copy(java.lang.String, java.lang.String, java.lang.String);
    public java.lang.String copy$default$3();
    public java.lang.String copy$default$2();
    public java.lang.String copy$default$1();
    public int hashCode();
    public java.lang.String toString();
    public boolean equals(java.lang.Object);
    public java.lang.String productPrefix();
    public int productArity();
    public java.lang.Object productElement(int);
    public boolean canEqual(java.lang.Object);
    public MyClass(java.lang.String, java.lang.String, java.lang.String);
}

在Scala中,复制方法有三个参数,并且最终可以使用当前实例中的一个参数来指定未指定的参数(Scala语言为其提供了方法调用中参数的默认值)

让我们分析一下,再把代码更新为:

sealed abstract class Base(val myparam:String)

case class Foo(override val myparam:String) extends Base(myparam)
case class Bar(override val myparam:String) extends Base(myparam)

def getIt( a:Base ) = a.copy(myparam="changed")

现在为了进行此编译,我们需要在getIt(a:MyType) a MyType的签名中使用,尊重以下合同:

  

任何有参数myparam和其他参数的东西   有默认值

所有这些方法都适用:

  def copy(myParam:String) = null
  def copy(myParam:String, myParam2:String="hello") = null
  def copy(myParam:String,myParam2:Option[Option[Option[Double]]]=None) = null

无法在Scala中表达此合同,但是有一些先进的技术可以提供帮助。

我们可以做的第一个观察是Scala中case classestuples之间存在严格的关系。事实上,case类是以某种方式具有附加行为和命名属性的元组。

第二个观察结果是,由于您的类层次结构的属性数量不能保证相同,因此复制方法签名不能保证是相同。

实际上,假设AnyTuple[Int]描述了第一个值为Int的任何大小的Tuple,我们希望做类似的事情:

def copyTupleChangingFirstElement(myParam:AnyTuple[Int], newValue:Int) = myParam.copy(_1=newValue)

如果所有元素都是Int,那就不难了。包含相同类型的所有元素的元组是List,我们知道如何替换List的第一个元素。我们需要将TupleX转换为List,替换第一个元素,然后将List转换回TupleX。是的,我们需要为X可能假设的所有值编写所有转换器。烦人但并不困难。

在我们的例子中,并非所有元素都是Int。我们想要处理Tuple,其中元素的类型不同,如果第一个元素是Int,它们都是相同的。这称为

  

“抽象过度”

即。以通用方式处理不同大小的元组,与其大小无关。为此,我们需要将它们转换为支持异类型的特殊列表,名为HList


<强>结论

案例类继承已被弃用,因为您可以从邮件列表中的多个帖子中找到:http://www.scala-lang.org/node/3289

您有两种策略可以解决您的问题:

  1. 如果您需要更改有限数量的字段,请使用@Ron建议的方法,该方法具有复制方法。如果你想在不丢失类型信息的情况下这样做,我会去泛化基类

    sealed abstract class Base[T](val param:String){
      def copy(param:String):T
    }
    
    class Foo(param:String) extends Base[Foo](param){
      def copy(param: String) = new Foo(param)
    }
    
    def getIt[T](a:Base[T]) : T = a.copy("hello")
    
    scala>  new Foo("Pippo")
    res0: Foo = Foo@4ab8fba5
    
    scala>  getIt(res0)
    res1: Foo = Foo@5b927504
    
    scala>  res1.param
    res2: String = hello
    
  2. 如果你真的想抽象过arity,一个解决方案就是使用由Miles Sabin开发的名为Shapeless的库。这里有一个问题在讨论后被问到:Are HLists nothing more than a convoluted way of writing tuples?但是我告诉你这会给你一些麻烦

答案 1 :(得分:13)

如果两个案例类会随着时间的推移发生分歧,以便它们具有不同的字段,则共享的copy方法将停止工作。

最好定义一个抽象def withMyParam(newParam: X): Base。更好的是,您可以引入一个抽象类型来在返回时保留case类类型:

scala> trait T {
     |   type Sub <: T
     |   def myParam: String
     |   def withMyParam(newParam: String): Sub
     | }
defined trait T

scala> case class Foo(myParam: String) extends T {
     |   type Sub = Foo
     |   override def withMyParam(newParam: String) = this.copy(myParam = newParam)
     | }
defined class Foo

scala>

scala> case class Bar(myParam: String) extends T {
     |   type Sub = Bar
     |   override def withMyParam(newParam: String) = this.copy(myParam = newParam)
     | }
defined class Bar

scala> Bar("hello").withMyParam("dolly")
res0: Bar = Bar(dolly)

答案 2 :(得分:12)

TL; DR:我设法在Base上声明复制方法,同时仍允许编译器在派生的case类中自动生成其实现。这涉及一个小技巧(实际上我自己只是重新设计了类型层次结构),但至少它表明你确实可以在没有在任何派生的案例类中编写样板代码的情况下使其工作。

首先,正如ron和Edmondo1984已经提到的,如果您的案例类有不同的字段,您将遇到麻烦。

我会严格遵循你的例子,并假设你的所有案例类都有相同的字段(查看你的github链接,这似乎也是你实际代码的情况)。

鉴于您的所有案例类都具有相同的字段,自动生成的copy方法将具有相同的签名,这是一个良好的开端。那么在Base中添加通用定义似乎是合理的,就像你做的那样: abstract class Base{ def copy(myparam: String):Base } 问题是scala不会生成copy方法,因为基类中已有一个方法。

事实证明,有另一种方法可以静态地确保Base具有正确的copy方法,并且通过结构类型和自我类型注释:

type Copyable = { def copy(myParam: String): Base }
sealed abstract class Base(val myParam: String) { this : Copyable => }

与我们之前的尝试不同,这不会阻止scala自动生成copy方法。 最后一个问题是:自我类型注释确保Base的子类具有copy方法,但它不会在Base上公开提供:

val foo: Base = Foo("hello")
foo.copy()
scala> error: value copy is not a member of Base

要解决此问题,我们可以添加从Base到Copyable的隐式转换。一个简单的演员会做,因为Base保证是可复制的:

implicit def toCopyable( base: Base ): Base with Copyable = base.asInstanceOf[Base with Copyable]

结束,这给了我们:

object Base {
  type Copyable = { def copy(myParam: String): Base }
  implicit def toCopyable( base: Base ): Base with Copyable = base.asInstanceOf[Base with Copyable]
}
sealed abstract class Base(val myParam: String) { this : Base. Copyable => }

case class Foo(override val myParam: String) extends Base( myParam )
case class Bar(override val myParam: String) extends Base( myParam )

def getIt( a:Base ) = a.copy(myParam="changed")

奖金效果:如果我们尝试使用不同的签名定义案例类,我们会收到编译错误:

case class Baz(override val myParam: String, truc: Int) extends Base( myParam ) 
scala> error: illegal inheritance; self-type Baz does not conform to Base's selftype Base with Base.Copyable

要完成,一个警告:你应该修改你的设计,以避免诉诸上述技巧。 在你的情况下,ron建议使用一个带有额外etype字段的单个案例类似乎更合理。

答案 3 :(得分:2)

我认为这是扩展方法的用途。选择复制方法本身的实施策略。

我喜欢这里的问题在一个地方解决了。

有趣的是,为什么没有特征性的特征:它不会说明如何调用副本,除了它总是可以在没有args copy()的情况下被调用。

sealed trait Base { def p1: String }

case class Foo(val p1: String) extends Base
case class Bar(val p1: String, p2: String) extends Base
case class Rab(val p2: String, p1: String) extends Base
case class Baz(val p1: String)(val p3: String = p1.reverse) extends Base

object CopyCase extends App {

  implicit class Copy(val b: Base) extends AnyVal {
    def copy(p1: String): Base = b match {
      case foo: Foo => foo.copy(p1 = p1)
      case bar: Bar => bar.copy(p1 = p1)
      case rab: Rab => rab.copy(p1 = p1)
      case baz: Baz => baz.copy(p1 = p1)(p1.reverse)
    }
    //def copy(p1: String): Base = reflect invoke
    //def copy(p1: String): Base = macro xcopy
  }

  val f = Foo("param1")
  val g = f.copy(p1="param2") // normal
  val h: Base = Bar("A", "B")
  val j = h.copy("basic")     // enhanced
  println(List(f,g,h,j) mkString ", ")

  val bs = List(Foo("param1"), Bar("A","B"), Rab("A","B"), Baz("param3")())
  val vs = bs map (b => b copy (p1 = b.p1 * 2))
  println(vs)
}

只是为了好玩,反思的副本:

  // finger exercise in the api
  def copy(p1: String): Base = {
    import scala.reflect.runtime.{ currentMirror => cm }
    import scala.reflect.runtime.universe._
    val im = cm.reflect(b)
    val ts = im.symbol.typeSignature
    val copySym = ts.member(newTermName("copy")).asMethod
    def element(p: Symbol): Any = (im reflectMethod ts.member(p.name).asMethod)()
    val args = for (ps <- copySym.params; p <- ps) yield {
      if (p.name.toString == "p1") p1 else element(p)
    }
    (im reflectMethod copySym)(args: _*).asInstanceOf[Base]
  }

答案 4 :(得分:2)

http://www.cakesolutions.net/teamblogs/copying-sealed-trait-instances-a-journey-through-generic-programming-and-shapeless处有一个非常全面的解释,说明如何使用无形;如果链接断开,该方法使用无形的copySyntax实用程序,这应该足以找到更多细节。

答案 5 :(得分:1)

这对我来说很好用:

sealed abstract class Base { def copy(myparam: String): Base }

case class Foo(myparam:String) extends Base {
  override def copy(x: String = myparam) = Foo(x)
}

def copyBase(x: Base) = x.copy("changed")

copyBase(Foo("abc")) //Foo(changed)

答案 6 :(得分:1)

这是一个老问题,旧解决方案,

https://code.google.com/p/scala-scales/wiki/VirtualConstructorPreSIP

在案例类复制方法存在之前制作。

因此,在引用这个问题时,每个case类都必须是一个叶子节点,所以定义副本和MyType / thisType加上newThis函数并设置,每个case类修复该类型。如果你想加宽树/ newThis函数并使用默认参数,你必须更改名称。

作为旁白 - 我一直在等待编译器插件魔术在实现之前改进,但类型宏可能是神奇的果汁。在Kevin的AutoProxy列表中搜索更详细的解释,为什么我的代码永远不会去任何地方