scala 案例类复制“方法”与超类

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/12370244/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-22 04:31:15  来源:igfitidea点击:

case class copy 'method' with superclass

classscalainheritanceenumeration

提问by nairbv

I want to do something like this:

我想做这样的事情:

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")

I can't, because in the context of getIt, I haven't told the compiler that every Base has a 'copy' method, but copy isn't really a method either so I don't think there's a trait or abstract method I can put in Base to make this work properly. Or, is there?

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

If I try to define Base as abstract class Base{ def copy(myparam:String):Base }, then case class Foo(myparam:String) extends Baseresults in class Foo needs to be abstract, since method copy in class Base of type (myparam: String)Base is not defined

如果我尝试将 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

Is there some other way to tell the compiler that all Baseclasses will be case classes in their implementation? Some trait that means "has the properties of a case class"?

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

I could make Base be a case class, but then I get compiler warnings saying that inheritance from case classes is deprecated?

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

I know I can also:

我知道我还可以:

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

but... that seems very ugly.

但是……这看起来很丑陋。

Thoughts? Is my whole approach just "wrong" ?

想法?我的整个方法只是“错误”吗?

UPDATEI changed the base class to contain the attribute, and made the case classes use the "override" keyword. This better reflects the actual problem and makes the problem more realistic in consideration of Edmondo1984's response.

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

采纳答案by Edmondo1984

This is old answer, before the question was changed.

这是在问题改变之前的旧答案。

Strongly typed programming languages prevent what you are trying to do. Let's see why.

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

The idea of a method with the following signature:

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

def getIt( a:Base ) : Unit

Is that the body of the method will be able to access a properties visible through Base class or interface, i.e. the properties and methods defined only on the Base class/interface or its parents. During code execution, each specific instance passed to the getItmethod might have a different subclass but the compile type of awill always be Base

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

One can reason in this way:

可以这样推理:

Ok I have a class Base, I inherit it in two case classes and I add a property with the same name, and then I try to access the property on the instance of Base.

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

A simple example shows why this is unsafe:

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

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")

In the following case, if the compiler didn't throw an error at compile time, it means the code would try to access a property that does not exist at runtime. This is not possible in strictly typed programming languages: you have traded restrictions on the code you can write for a much stronger verification of your code by the compiler, knowing that this reduces dramatically the number of bugs your code can contain

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



This is the new answer. It is a little long because few points are needed before getting to the conclusion

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

Unluckily, you can't rely on the mechanism of case classes copyto implement what you propose. The way the copy method works is simply a copy constructor which you can implement yourself in a non-case class. Let's create a case class and disassemble it in the REPL:

不幸的是,您不能依靠案例类复制机制来实现您的建议。copy 方法的工作方式只是一个复制构造函数,您可以在非 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();
    public java.lang.String copy$default();
    public java.lang.String copy$default();
    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);
}

In Scala, the copy method takes three parameter and can eventually use the one from the current instance for the one you haven't specified ( the Scala language provides among its features default values for parameters in method calls)

在 Scala 中,copy 方法接受三个参数,最终可以将当前实例中的一个用于您尚未指定的一个(Scala 语言在其功能中为方法调用中的参数提供了默认值)

Let's go down in our analysis and take again the code as updated:

让我们继续分析并再次查看更新后的代码:

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")

Now in order to make this compile, we would need to use in the signature of getIt(a:MyType)a MyTypethat respect the following contract:

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

Anything that has a parameter myparam and maybe other parameters which have default value

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

All these methods would be suitable:

所有这些方法都适用:

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

There is no way to express this contract in Scala, however there are advanced techniques that can be helpful.

没有办法在 Scala 中表达这个契约,但是有一些先进的技术可以提供帮助。

The first observation that we can do is that there is a strict relation between case classesand tuplesin Scala. In fact case classes are somehow tuples with additional behaviour and named properties.

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

The second observation is that, since the number of properties of your classes hierarchy is not guaranteed to be the same, the copymethod signatureis not guaranteed to be the same.

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

In practice, supposing AnyTuple[Int]describes any Tupleof any size where the first value is of type Int, we are looking to do something like that:

在实践中,假设AnyTuple[Int]描述Tuple了第一个值是 Int 类型的任何大小,我们希望做这样的事情:

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

This would not be to difficult if all the elements were Int. A tuple with all element of the same type is a List, and we know how to replace the first element of a List. We would need to convert any TupleXto List, replace the first element, and convert the Listback to TupleX. Yes we will need to write all the converters for all the values that Xmight assume. Annoying but not difficult.

如果所有元素都是Int. 具有相同类型的所有元素的元组是 a List,我们知道如何替换 a 的第一个元素List。我们需要将 any 转换TupleXList,替换第一个元素,然后将List返回的转换为TupleX。是的,我们需要为X可能假设的所有值编写所有转换器。烦人但不难。

In our case though, not all the elements are Int. We want to treat Tuplewhere the elements are of different type as if they were all the same if the first element is an Int. This is called

但在我们的例子中,并非所有元素都是IntTuple如果第一个元素是 Int,我们希望将元素的类型视为相同的元素。这就是所谓的

"Abstracting over arity"

“抽象于arity”

i.e. treating tuples of different size in a generic way, independently of their size. To do it, we need to convert them into a special list which supports heterogenous types, named HList

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



Conclusion

结论

Case classes inheritance is deprecated for very good reason, as you can find out from multiple posts in the mailing list: http://www.scala-lang.org/node/3289

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

You have two strategies to deal with your problem:

您有两种策略来处理您的问题:

  1. If you have a limited number of fields you require to change, use an approach such as the one suggested by @Ron, which is having a copy method. If you want to do it without losing type information, I would go for generifying the base class

    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. If you really want to abstract over arity, a solution is to use a library developed by Miles Sabin called Shapeless. There is a question here which has been asked after a discussion : Are HLists nothing more than a convoluted way of writing tuples?but I tell you this is going to give you some headache

  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 的库。这里有一个问题,经过讨论后被问到:HList 只是一种编写元组的复杂方式吗?但我告诉你这会让你头疼

回答by Régis Jean-Gilles

TL;DR:I managed to declare the copy method on Base while still letting the compiler auto generate its implementations in the derived case classes. This involves a little trick (and actually I'd myself just redesign the type hierarchy) but at least it goes to show that you can indeed make it work without writing boiler plate code in any of the derived case classes.

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

First, and as already mentioned by ron and Edmondo1984, you'll get into troubles if your case classes have different fields.

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

I'll strictly stick to your example though, and assume that all your case classes have the same fields (looking at your github link, this seems to be the case of your actual code too).

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

Given that all your case classes have the same fields, the auto-generated copymethods will have the same signature which is a good start. It seems reasonable then to just add the common definition in Base, as you did: abstract class Base{ def copy(myparam: String):Base }The problem is now that scala won't generate the copymethods, because there is already one in the base class.

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

It turns out that there is another way to statically ensure that Basehas the right copymethod, and it is through structural typing and self-type annotation:

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

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

And unlike in our earlier attempt, this will not prevent scala to auto-generate the copymethods. There is one last problem: the self-type annotation makes sure that sub-classes of Basehave a copymethod, but it does not make it publicly availabe on Base:

与我们之前的尝试不同,这不会阻止 Scala 自动生成copy方法。还有最后一个问题:self-type 注释确保 的子类Base有一个copy方法,但它并没有使其公开可用Base

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

To work around this we can add an implicit conversion from Base to Copyable. A simple cast will do, as a Base is guaranteed to be a Copyable:

为了解决这个问题,我们可以添加一个从 Base 到 Copyable 的隐式转换。一个简单的强制转换就可以了,因为 Base 保证是可复制的:

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

Wrapping up, this gives us:

总结一下,这给了我们:

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")

Bonus effect: if we try to define a case class with a different signature, we get a compile error:

额外的效果:如果我们尝试定义一个具有不同签名的 case 类,我们会得到一个编译错误:

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

To finish, one warning: you should probably just revise your design to avoid having to resort to the above trick. In your case, ron's suggestion to use a single case class with an additional etypefield seems more than reasonable.

最后,一个警告:您可能应该修改您的设计以避免不得不诉诸上述技巧。在您的情况下,ron 建议使用带有附加etype字段的单个案例类似乎非常合理。

回答by ron

If the two case classes would diverge over time so that they have different fields, then the shared copyapproach would cease to work.

如果这两个案例类会随着时间的推移而出现分歧,从而使它们具有不同的字段,那么共享copy方法将停止工作。

It is better to define an abstract def withMyParam(newParam: X): Base. Even better, you can introduce an abstract type to retain the case class type upon return:

最好定义一个抽象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)

回答by Luigi Plinge

This works fine for me:

这对我来说很好用:

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)

回答by som-snytt

I think this is what extension methods are for. Take your pick of implementation strategies for the copy method itself.

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

I like here that the problem is solved in one place.

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

It's interesting to ask why there is no trait for caseness: it wouldn't say much about how to invoke copy, except that it can always be invoked without args, copy().

有趣的是,为什么没有 caseness 的 trait:它不会说太多关于如何调用 copy,除了它总是可以在没有 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)
}

Just for fun, reflective copy:

只是为了好玩,反射性副本:

  // 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]
  }

回答by satyagraha

There is a very comprehensive explanation of how to do this using shapeless at http://www.cakesolutions.net/teamblogs/copying-sealed-trait-instances-a-journey-through-generic-programming-and-shapeless; in case the link breaks, the approach uses the copySyntax utilities from shapeless, which should be sufficient to find more details.

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

回答by Chris

Its an old problem, with an old solution,

这是一个老问题,一个老办法,

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

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

made before the case class copy method existed.

在 case 类复制方法存在之前制作。

So in reference to this problem each case class MUST be a leaf node anyway, so define the copy and a MyType / thisType plus the newThis function and you are set, each case class fixes the type. If you want to widen the tree/newThis function and use default parameters you'll have to change the name.

所以参考这个问题,每个案例类无论如何都必须是一个叶节点,所以定义副本和一个 MyType / thisType 加上 newThis 函数,你被设置,每个案例类修复类型。如果要加宽树/newThis 函数并使用默认参数,则必须更改名称。

as an aside - I've been waiting for compiler plugin magic to improve before implementing this but type macros may be the magic juice. Search in the lists for Kevin's AutoProxy for a more detailed explanation of why my code never went anywhere

顺便说一句 - 在实现这个之前,我一直在等待编译器插件魔法的改进,但类型宏可能是魔法汁。在列表中搜索 Kevin 的 AutoProxy 以获得更详细的解释为什么我的代码从来没有去任何地方