case类复制'方法'与超类

nai*_*rbv 26 inheritance enumeration scala class

我想做这样的事情:

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")
Run Code Online (Sandbox Code Playgroud)

我不能,因为在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]
}
Run Code Online (Sandbox Code Playgroud)

但是......这看起来很难看.

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

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

Edm*_*984 24

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

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

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

def getIt( a:Base ) : Unit
Run Code Online (Sandbox Code Playgroud)

是否该方法的主体将能够访问通过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")
Run Code Online (Sandbox Code Playgroud)

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


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

不幸的是,您不能依赖案例类复制的机制来实现您的建议.复制方法的工作方式只是一个复制构造函数,您可以在非案例类中实现它.让我们创建一个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);
}
Run Code Online (Sandbox Code Playgroud)

在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")
Run Code Online (Sandbox Code Playgroud)

现在,为了使这个编译,我们将需要的签名使用getIt(a:MyType) 一个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
Run Code Online (Sandbox Code Playgroud)

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

我们能够做的第一观察是,有一间严格关系case classestuplesScala中.事实上,case类是以某种方式具有附加行为和命名属性的元组.

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

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

def copyTupleChangingFirstElement(myParam:AnyTuple[Int], newValue:Int) = myParam.copy(_1=newValue)
Run Code Online (Sandbox Code Playgroud)

如果所有要素都是如此,那就不难了Int.具有相同类型的所有元素的元组是a List,并且我们知道如何替换a的第一个元素List.我们需要将any转换TupleXList,替换第一个元素,然后将其转换ListTupleX.是的,我们需要为所有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
    
    Run Code Online (Sandbox Code Playgroud)
  2. 如果你真的想抽象arity,一个解决方案是使用由Miles Sabin开发的名为Shapeless的库.这里有一个问题在讨论后被问到:HLists只不过是一种复杂的编写元组的方式吗?但我告诉你这会让你头疼


ron*_*ron 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)
Run Code Online (Sandbox Code Playgroud)

  • 怎么样`case class MyEnum(etype: Etype, value: String); 密封性状Etype;对象 etype1 扩展 Etype;...`? (2认同)
  • 为什么?模式匹配可以嵌套,例如`case MyEnum(\`etype1 \`,x)=&gt; ...`。请注意,您需要使用反引号,否则Scala会将小写名称视为自由匹配变量。您可以使用大物件防止脚部射击。 (2认同)

Rég*_*les 13

TL; DR:我设法在Base上声明复制方法,同时仍允许编译器在派生的case类中自动生成其实现.这涉及一个小技巧(实际上我自己只是重新设计了类型层次结构),但至少它表明你确实可以在没有在任何派生的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 => }
Run Code Online (Sandbox Code Playgroud)

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

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

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

implicit def toCopyable( base: Base ): Base with Copyable = base.asInstanceOf[Base with Copyable]
Run Code Online (Sandbox Code Playgroud)

总结一下,这给了我们:

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")
Run Code Online (Sandbox Code Playgroud)

额外效果:如果我们尝试使用不同的签名定义一个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
Run Code Online (Sandbox Code Playgroud)

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