如何将一个 trait 混入到实例中?

48

给定一个特质 MyTrait

trait MyTrait {
  def doSomething = println("boo")
}

它可以与extendswith一同混合到类中:

class MyClass extends MyTrait

在创建新实例时,也可以混合使用:

var o = new MyOtherClass with MyTrait
o.doSomething

但是...特质(或者其他任何有区别的)能够被添加到现有实例中吗?

我正在使用Java中的JPA加载对象,并且我想使用特质为它们添加一些功能。这是否有可能?

我想按照以下方式混入一个特质:

var o = DBHelper.loadMyEntityFromDB(primaryKey);
o = o with MyTrait //adding trait here, rather than during construction
o.doSomething
5个回答

26
我有一个对于这个用法的想法:
//if I had a class like this
final class Test {
  def f = println("foo")
}
trait MyTrait {
  def doSomething = {
    println("boo")
  }
}
object MyTrait {
  implicit def innerObj(o:MixTest) = o.obj

  def ::(o:Test) = new MixTest(o)
  final class MixTest private[MyTrait](val obj:Test) extends MyTrait
}

您可以按以下方式使用此特性:

import MyTrait._

val a = new Test
val b = a :: MyTrait
b.doSomething
b.f

对于你的示例代码:

val o = DBHelper.loadMyEntityFromDB(primaryKey) :: MyTrait
o.doSomething

我希望这可以帮助到你。 更新时间:
object AnyTrait {
  implicit def innerObj[T](o: MixTest[T]):T = o.obj

  def ::[T](o: T) = new MixTest(o)
  final class MixTest[T] private[AnyTrait](val obj: T) extends MyTrait
}

但是这种模式有一些限制,您不能使用已经定义的某些隐含辅助方法。
val a = new Test
a.f
val b = a :: AnyTrait
b.f1
b.f
val c = "say hello to %s" :: AnyTrait
println(c.intern)  // you can invoke String's method 
println(c.format("MyTrait"))  //WRONG. you can't invoke StringLike's method, though there defined a implicit method in Predef can transform String to StringLike, but implicit restrict one level transform, you can't transform MixTest to String then to StringLike.
c.f1
val d = 1 :: AnyTrait
println(d.toLong)
d.toHexString // WRONG, the same as above
d.f1

2
这是一个非常有用的功能,当你使用implicit定义一个方法,并将该方法导入到你的作用域中时,该方法可以帮助你将由方法参数指定的对象转换为由方法返回指定的另一个对象,以便在需要调用后者的未定义于前者的方法时使用。 - Googol Shan
2
非常好的解决方案,我很喜欢。我想知道它如何轻松地变成通用的 - 可能在 MyTrait 对象中添加一个通用参数到 :: 中就可以让它适用于任何类型。它是否也可以与我们想要混合的任意特性一起使用呢...? - axel22
@axel22 是的,我认为它可以像我的更新答案一样变成通用的。但是我无法让它与任意特质一起工作,因为我是 Scala 的新手。 - Googol Shan
好的,我在下面写了如何使它稍微更通用。不过,在我看来,每个特质对象都无法避免添加一些样板文件。 - axel22

22
JVM中现有的运行时对象在堆上具有一定大小。将一个trait添加到它上面意味着更改它在堆上的大小,并更改其签名。
因此,唯一的方法是在编译时进行某种形式的转换。
在Scala中,Mixin组合发生在编译时。编译器可能会创建一个包装器B,将现有对象A与相同类型的B包装起来,简单地将所有调用转发给现有对象A,然后将一个trait T混入B。然而,这种实现并没有被采用。这是有问题的,因为对象A可能是终态类的实例,不能被扩展。
总之,在现有对象实例上不可能使用Mixin组合。
更新:关于Googol Shan提出的智能解决方案以及使其适用于任何trait,我做到了这一点。思路是提取DynamicMixinCompanion trait中的通用Mixin功能。客户端应该为他想要动态Mixin功能的每个trait创建一个扩展DynamicMixinCompanion的伴随对象。这个伴随对象需要定义匿名trait对象(::)。
trait DynamicMixinCompanion[TT] {                                                                    
  implicit def baseObject[OT](o: Mixin[OT]): OT = o.obj                                              

  def ::[OT](o: OT): Mixin[OT] with TT                                                               
  class Mixin[OT] protected[DynamicMixinCompanion](val obj: OT)                                      
}                                                                                                    

trait OtherTrait {                                                                                   
  def traitOperation = println("any trait")                                                          
}                                                                                                    

object OtherTrait extends DynamicMixinCompanion[OtherTrait] {                                        
  def ::[T](o: T) = new Mixin(o) with OtherTrait                                                     
}                                                                                                    

object Main {                                                                                        
  def main(args: Array[String]) {                                                                    
    val a = "some string"                                                                            
    val m = a :: OtherTrait                                                                          
    m.traitOperation                                                                                 
    println(m.length)                                                                                
  }                                                                                                  
}                                                                                                    

只是为了澄清的小备注:变量mOtherTrait的一个实例,但不是String的实例。(它是“隐式”的,每当需要时在编译时将其“转换”回字符串。)您可以通过在main函数的末尾添加println(“m是String / OtherTrait的实例:”+ m.isInstanceOf [String] +“/”+ m.isInstanceOf [OtherTrait])来清楚地看到这一点。 - Hbf
@axel22 如果我理解正确的话,您可以将具有一些 def-s 的行为 trait 混合到某个实例中。但是无法混入一个同时具有一些值的 trait,对吗? - Alexander Arendar

7

我通常使用 implicit 将新方法混合到现有对象中。

比如,如果我有以下代码:

final class Test {
  def f = "Just a Test"
  ...some other method
}
trait MyTrait {
  def doSomething = {
    println("boo")
  }
}
object HelperObject {
  implicit def innerObj(o:MixTest) = o.obj

  def mixWith(o:Test) = new MixTest(o)
  final class MixTest private[HelperObject](obj:Test) extends MyTrait
}

然后,您可以使用已经存在的对象Test来调用MyTrait方法。

val a = new Test
import HelperObject._
val b = HelperObject.mixWith(a)
println(b.f)
b.doSomething

在您的示例中,您可以像这样使用:
import HelperObject._
val o = mixWith(DBHelper.loadMyEntityFromDB(primaryKey));
o.doSomething

我正在思考一种完美的语法来定义这个HelperObject:

trait MyTrait {
  ..some method
}
object MyTrait {
  implicit def innerObj(o:MixTest) = o.obj

  def ::(o:Test) = new MixTest(o)
  final class MixTest private[MyTrait](obj:Test) extends MyTrait
}
//then you can use it
val a = new Test
val b = a :: MyTrait
b.doSomething
b.f
// for your example
val o = DBHelper.loadMyEntityFromDB(primaryKey) :: MyTrait
o.doSomething

1

那么隐式类呢?与其他答案中的最终内部类和“mixin”函数相比,它对我来说似乎更容易。

trait MyTrait {

    def traitFunction = println("trait function executed")

}

class MyClass {

    /**
     * This inner class must be in scope wherever an instance of MyClass
     * should be used as an instance of MyTrait. Depending on where you place
     * and use the implicit class you must import it into scope with
     * "import mypackacke.MyImplictClassLocation" or
     * "import mypackage.MyImplicitClassLocation._" or no import at all if
     * the implicit class is already in scope.
     * 
     * Depending on the visibility and location of use this implicit class an
     * be placed inside the trait to mixin, inside the instances class,
     * inside the instances class' companion object or somewhere where you
     * use or call the class' instance with as the trait. Probably the
     * implicit class can even reside inside a package object. It also can be
     * declared private to reduce visibility. It all depends on the structure
     * of your API.
     */
    implicit class MyImplicitClass(instance: MyClass) extends MyTrait

    /**
     * Usage
     */
    new MyClass().traitFunction

}

很好,但是使用您的解决方案,特质只能附加到使用new创建并在作用域中的实例上。有时您希望将Trait附加到其他地方创建的对象上,例如来自ORM层。 - SkyWalker

0
为什么不使用Scala的扩展我的库模式?

https://alvinalexander.com/scala/scala-2.10-implicit-class-example

我不确定以下代码的返回值是什么:

var o = DBHelper.loadMyEntityFromDB(primaryKey);

但假设它是DBEntity。你可以将类DBEntity转换为扩展你的特质MyTrait的类。

像这样:

trait MyTrait {
  def doSomething = {
    println("boo")
  }
}

class MyClass() extends MyTrait

// Have an implicit conversion to MyClass
implicit def dbEntityToMyClass(in: DBEntity): MyClass = 
new MyClass()

我相信你也可以通过使用隐式类来简化这个问题。

implicit class ConvertDBEntity(in: DBEntity) extends MyTrait

我特别不喜欢这里被接受的答案,因为它重载了::运算符来混合一个trait。
在Scala中,::运算符用于序列,例如:
val x = 1 :: 2 :: 3 :: Nil

在我看来,将其用作继承的手段有点别扭。


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