这篇文章主要介绍“Scala中Trait有什么作用”,在日常操作中,相信很多人在Scala中Trait有什么作用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Scala中Trait有什么作用”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
Inside Scala - 1:Partially applied functions
Partially applied function(不完全应用的函数)是scala中的一种curry机制,本文将通过一个简单的实例来描述在scala中 partially applied function的内部机制。
// Test3.scala package test object Test3 { def sum(x:Int, y:Int, z:Int) = x + y + z def main(args: Array[String]) { val sum1 = sum _ val sum2 = sum(1, _:Int, 3) println(sum1(1,2,3)) println(sum2(2)) List(1,2,3,4).foreach(println); List(1,2,3,4).foreach(println _) } }
在这个代码中 sum _ 表示了一个 新的类型为 (Int,Int,Int)=>Int 的函数,实际上,Scala 会生成一个新的匿名函数(是一个函数对象,Function3),这个函数对象的apply方法会调用 sum 这个对象方法(在这里,是方法,而不是一个函数)。
sum2 是一个 Int => Int的函数(对象),这个函数的apply方法会调用 sum 对象方法。
后面的两行代码都需要访问 println, println是在在Predef对象中定义的方法,在scala中,实际上都会生成一个临时的函数对象,来包装对 println 方法的调用。如果研究一下scala生成的代码,那么可以发现,目前生成的代码中, 对 println, println _生成的代码是重复的,这也说明,目前,所有的你匿名函数基本上没有进行重复性检查。(这可能导致编译生成的的类更大)。
从这里可以得知,虽然,在语法层面,方法(所有的def出来的东西)与函数看起来是一致的,但实际上,二者在底层有区别,方法仍然是不可以直接定位、传值的,他不是一个对象。而仅仅是JVM底层可访问的一个实体。而函数则是虚拟机层面的一个对象。任何从方法到函数的转换,Scala会自动生成一个匿名的函数对象,来进行相应的转换。
所以, List(1,2,3,4).foreach(println) 在底层执行时,并不是获得了一个println的引用(实际上,根本不存在println这个可访问的对象),而是scala自动产生一个匿名的函数,这个函数会调用println。
当然,将一个函数传递时,Scala是不会再做不必要的包装的,而是直接传递这个函数对象了。
Inside Scala - 2: Curry Functions
Curry,在函数式语言中是很常见的,在scala中,对其有特别的支持。
package test
object TestCurry { def sum(x:Int)(y:Int)(z:Int) = x + y + z def main(args: Array[String]){ val sum1: (Int => Int => Int) = sum(1) val sum12: Int => Int = sum(1)(2) val sum123 = sum(1)(2)(3) println(sum1(2)(3)) println(sum12(3)) println(sum123) } }
在这个例子中, sum 被设计成为一个curried函数,(多级函数?),研究一个函数的实现是很有意思的:
如果看生成的 sum 函数代码,那么,它与 如下编写的
def sum(x:Int, y:Int: z:Int) = x + y + z 是一致的。
而且,如果,你调用sum(1)(2)(3),实际上,scala也并不会产生3次函数调用,而是一次 sum(1,2,3)
也就是说,如果你没有进行 sum(1), sum(1)(2)等调用,那么实际上,上述的代码中根本不会生成额外的函数处理代码。但是,如果我们需要进行一些常用的curry操作时,scala为我们提供了额外的语法级的便利。
Inside Scala - 3: How Trait works
Scala中Trait应该是一个非常强大,但又有些复杂的概念,至少与我,我对trait总是有一些不太明了的地方,求人不如求己,对这些疑问还是自己动手探真的比较好。
还是从一个简单的实例着手。
package test
import java.awt.Point object TestTrait { trait Rectangular { def topLeft: Point def bottomRight: Point def left = topLeft.x def top = topLeft.y def right = bottomRight.x def bottom = bottomRight.y def width = right - left def height = bottom - top } class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular { override def toString = "I am a rectangle" } }
对这段代码,我想问如下的几个问题:
Rectangle是如何继承 Rectangular的行为,如 left, right, width, height的?
Rectangular 对应于Java的接口,那么,相关的实现代码又是如何保存的?
其实,这两个问题是相关的。研究这个问题的最直接的办法莫过于直接分析scalac编译后的结果。
这个类编译后包括:
TestTrait.class 这个类
TestTrait$.class 其实就是 object TestTrait这个对象的类。一个object实际上从属于一个类,scala是对其加后缀$
在这个例子中,TestTrait这个对象实际上并未定义新的属性和方法,因此,并没有包含什么内容
TestTrait$Rectangular.class
对应于代码中的Rectangular这个trait,这实际上是一个接口类。对应的就是这个trait中定义的全部方法。包括topLeft, bottomRight以及后续的实现方法left, width等的接口定义
public interface test.TestTrait$Rectangular extends scala.ScalaObject{ public abstract int height(); public abstract int width(); public abstract int bottom(); public abstract int right(); public abstract int top(); public abstract int left(); public abstract java.awt.Point bottomRight(); public abstract java.awt.Point topLeft(); }
TestTrait$Rectangular$class.class
这个类实际上是trait逻辑的实现类。由于JVM中,接口是不支持任何的实现代码的,因此,scala将相关的逻辑代码编译在这个类中
public abstract class test.TestTrait$Rectangular$class extends java.lang.Object{ public static void $init$(test.TestTrait$Rectangular); // 在这个例子中,没有trait的初始化相关操作 Code: 0: return public static int height(test.TestTrait$Rectangular); // 对应于height = bottom - top这个操作的实现 Code: 0: aload_0 1: invokeinterface #17, 1; //InterfaceMethod test/TestTrait$Rectangular.bottom:()I 6: aload_0 7: invokeinterface #20, 1; //InterfaceMethod test/TestTrait$Rectangular.top:()I 12: isub 13: ireturn
更多的方法并不在此罗列。
首先,这个实现类是抽象的,它不需要被实例化。
所有的trait方法,其实接收一个额外的参数,即 this 对象。对对象的任何的访问,如bottom等操作,实际上是直接调用对象的相应操作。
所有的trait方法,都是static的。
TestTrait$Rectangle.class
这个就是Rectangle这个类的代码了。
// 首先,实现类以implements的方式继承了trait所定义的接口。 public class test.TestTrait$Rectangle extends java.lang.Object implements test.TestTrait$Rectangular,scala.ScalaObject{ // 类的val属性直接对应于一个同名的private字段和相应的读取方法。 private final java.awt.Point bottomRight; private final java.awt.Point topLeft; // scala对象比较特殊的是,相应字段的初始化比调用父类构造函数来得更早。也就是说,在Class(arg)中的参数是最早被初始化的。 // 在构造函数后,可以看到,会调用trait的初始化代码。当然,在我们的这个例子中,trait没有任何的初始化行为。 public test.TestTrait$Rectangle(java.awt.Point, java.awt.Point); Code: 0: aload_0 1: aload_1 2: putfield #13; //Field topLeft:Ljava/awt/Point; 5: aload_0 6: aload_2 7: putfield #15; //Field bottomRight:Ljava/awt/Point; 10: aload_0 11: invokespecial #20; //Method java/lang/Object."":()V 14: aload_0 15: invokestatic #26; //Method test/TestTrait$Rectangular$class.$init$:(Ltest/TestTrait$Rectangular;)V 18: return // height这个函数是从trait中继承的,在这里,继承体现为对trait实现类的一个调用,同时,将对象本身作为this传递给该函数 public int height(); Code: 0: aload_0 1: invokestatic #39; //Method test/TestTrait$Rectangular$class.height:(Ltest/TestTrait$Rectangular;)I 4: ireturn
这里不再罗列其他的函数实现,其基本与height函数是相一致的。
理解了以上的逻辑,trait是如何实现将接口和接口实现溶于一体的,应该就非常的清楚了。我以前一直在纳闷一个问题:接口中不能够包含实现代码,那么,难道每次编译继承trait的类时,这写实现的代码是怎么在子类中继承的呢?难道是编译器将这个逻辑复制了一份?如果这样,不仅生成的代码量很大,而且,还有一个问题,那就是,在编译时需要有trait的源代码才行。经过上面的剖析,我们终于知道scala其实有更***的解决之道的:那就是一个trait辅助类。
Inside Scala - 4: Trait Stacks
这个例子摘自 Programming In Scala 这本书第12.5节。本文将从另外一个角度来分析 Stackable Trait的内部原理。
package test
import scala.collection.mutable.ArrayBuffer object Test7 { abstract class IntQueue { def put(x:Int) def get(): Int } class BasicIntQueue extends IntQueue { private val buf = new ArrayBuffer[Int] def put(x:Int) { buf += x } def get() = buf.remove(0) } trait Doubling extends IntQueue { abstract override def put(x:Int) { super.put(2*x) } } def main(args: Array[String]) { val queue: IntQueue = new BasicIntQueue with Doubling queue.put(1) queue.put(5) println( queue.get ) println( queue.get ) } }
我们来看这一行代码 val queue = new BasicIntQue with Doubling,Scala针对这一行代码干了很多很多的工作,并不是一个简单的操作那么简单
Scala需要新生成一个类型,在我的环境中,这个类叫做:Test7$$anon$1,看看这个代码:
// 新的类以BasicIntQueue为父类,同时实现了Doubling这个trait定义的接口
public final class test.Test7$$anon$1 extends test.Test7$BasicIntQueue implements test.Test7$Doubling{ public test.Test7$$anon$1(); Code: 0: aload_0 1: invokespecial #10; //Method test/Test7$BasicIntQueue."":()V // 父类初始化 4: aload_0 5: invokestatic #16; //Method test/Test7$Doubling$class.$init$:(Ltest/Test7$Doubling;)V // trait辅助类初始化 8: return public void put(int); Code: 0: aload_0 1: iload_1 2: invokestatic #21; //Method test/Test7$Doubling$class.put:(Ltest/Test7$Doubling;I)V // 这个类使用的是Doubling提供的版本 5: return public final void test$Test7$Doubling$$super$put(int); // Doubling所需要的super的版本 Code: 0: aload_0 1: iload_1 2: invokespecial #29; //Method test/Test7$BasicIntQueue.put:(I)V 5: return }
我们来分析一下Doubling这个trait的实现
public interface test.Test7$Doubling extends scala.ScalaObject{ public abstract void put(int); // 这个是trait中实现的方法 public abstract void test$Test7$Doubling$$super$put(int); // 这个是这个trait 额外依赖的方法 } // Doubling这个trait的辅助类 public abstract class test.Test7$Doubling$class extends java.lang.Object{ public static void $init$(test.Test7$Doubling); Code: 0: return public static void put(test.Test7$Doubling, int); Code: 0: aload_0 1: iconst_2 2: iload_1 3: imul 4: invokeinterface #17, 2; //InterfaceMethod test/Test7$Doubling.test$Test7$Doubling$$super$put:(I)V // 这也是 Doubling这个接口中需要 super.init这个方法的原因。 9: return }
由此可见,编译器在处理 val queue: IntQueue = new BasicIntQueue with Doubling这一行代码时,需要确定类、Trait的先后顺序。这也是理解Trait的最为复杂的一环。后续,我将就这个问题进行分析。
Inside Scala - 5: Trait Stacks
继续上一个案例,现在我们将Trait的链搞得更长一些:
trait Incrementing extends IntQueue { abstract override def put(x: Int) { super.put(x + 1) } } trait Filtering extends IntQueue { abstract override def put(x: Int) { if (x >= 0) super.put(x) } } val queue: IntQueue = new BasicIntQueue with Incrementing with Filtering
新的类如何呢?当我们调用 queue的 put方法时,这个的先后顺序究竟如何呢?还是看看生成的代码:
public final class test.Test7$$anon$1 extends test.Test7$BasicIntQueue implements test.Test7$Incrementing,test.Test7$Filtering{ // 初始化的顺序:先父类、再Incremeting、再Filtering,这个顺序与源代码的顺序是一致的。 public test.Test7$$anon$1(); Code: 0: aload_0 1: invokespecial #10; //Method test/Test7$BasicIntQueue."":()V 4: aload_0 5: invokestatic #16; //Method test/Test7$Incrementing$class.$init$:(Ltest/Test7$Incrementing;)V 8: aload_0 9: invokestatic #21; //Method test/Test7$Filtering$class.$init$:(Ltest/Test7$Filtering;)V 12: return // put 方法实际使用的是 Filtering这个Trait的put public void put(int); Code: 0: aload_0 1: iload_1 2: invokestatic #34; //Method test/Test7$Filtering$class.put:(Ltest/Test7$Filtering;I)V 5: return // Filtering Trait的父实现是Incremeting trait public final void test$Test7$Filtering$$super$put(int); Code: 0: aload_0 1: iload_1 2: invokestatic #38; //Method test/Test7$Incrementing$class.put:(Ltest/Test7$Incrementing;I)V 5: return // incrementing的父实现是父类的实现。 public final void test$Test7$Incrementing$$super$put(int); Code: 0: aload_0 1: iload_1 2: invokespecial #26; //Method test/Test7$BasicIntQueue.put:(I)V 5: return }
因此,要理解这个过程,可以这么来分析:val queue: IntQueue = new BasicIntQueue with Incrementing with Filtering
首先初始化的是BasicIntQueue
在这个基础上叠加 Incrementing,super.put引用的是BasicIntQueue的put方法
再在叠加后的基础上叠加 Filtering,super.put引用的是 Incrementing的put方法
叠加后的结果就是***的版本。put引用的是Filtering的put方法
因此,初始化的顺序是从左至右,而方法的可见性则是从右至左(可以理解为上面的叠加关系,叠加之后,上面的trait具有更大的优先可见性。
Inside Scala - 6:Case Class 与 模式匹配
本文将尝试对Case Class是如何参与模式匹配的进行剖析。文中的代码还是来自 Programming In Scala一书。
abstract class Expr; case class Var(name: String) extends Expr; case class Number(num: Double) extends Expr; case class UnOp(operator: String, arg: Expr) extends Expr; case class BinOp(operator:String, left: Expr, right: Expr) extends Expr;
这里我们先来看一个最为简单的模式匹配
some match { case Var(name) => println("a var with name:" + name) }
这几行的代码编译后等效于:
if(some instanceof Var) { Var temp21 = (Var)some; String name = temp21.name(); if(true) { name = temp22; Predef$.MODULE$.println((new StringBuilder()).append("a var with name:").append(name).toString()); } else { throw new MatchError(some.toString()); } } else { throw new MatchError(some.toString()); }
如果从生成的代码的角度上来看,Scala生成的代码质量并不高,其中的 if(true) else 的那个部分就有明显的废代码。(不过,这个对运行效率的影响到时几乎可以忽略,只是编译后的字节码倒是没理由的多了几分)。
上面的这个模式匹配仅仅是匹配一个类型。因此,其对应的java原语就是 instanceof 检测。
让我们更进一步, 看看如下的例子:
some match { case Var("x") => println("a var with name:x") }
这个模式匹配不仅匹配类型,还要匹配构造器中的name属性为 "x"常量。这里我就不在福州 Scala生成的字节码了,而是简单的翻译一下:
if( some instanceof Var) -- 类型检查
var.name() == "x" -- 检查 对象的 name 属性是否等于 "x",编译器非常清楚的指导 Case Class的每一个构造参数所对应的字段名称。
更进一步,让我们看看一个更复杂的模式匹配:嵌套的对象。
some match { case BinOp("+", Var("x"), UnOp("-", Number(num))) => println("x - " + num) }
这个逻辑其实也是上面的一个嵌套:
some instanceof BinOp
some.operator == "+" 编译器进行了特殊的null检测,以防止这个操作出现NPE
some.left instanceof Var
some.left.name == "x"
some.right instanceof UnOp
some.right.operator == "-"
some.right.arg instanceof Number
......
实际上,Scala的模式匹配确实为我们干了很多很多的事情,这也使得在很多的情况下,使用scala的模式匹配为我们提供了一个非常安全的(不用担心大量的Null检查),以及非常复杂的匹配操作。当然,与更复杂的模式匹配相比(譬如,规则引擎其实也是一个模式匹配的引擎),Scala的模式匹配还是相对比较简单的。
这里简单的补充一下 Scala中的几种模式:
1、通配符模式。 也就是说使用 case _ => 来匹配所有的东西。或者,case Var(_) 来对局部进行通配。
2、常量匹配。譬如上述的Var("x") ,其中,"x"就是一个常量。常量除了文字常量外,还可以使用以大写字母开头的scala变量,或者`varname`形式的引用。
3、变量匹配。一个变量匹配实际上匹配任何的类型,并同时赋予其一个变量名。
4、构造函数匹配。匹配一个给定的类型,并且嵌套的对其参数进行匹配。参数可以是通配符模式、常量、变量或者子构造函数匹配
5、对于List类型, _*可以匹配剩余的全部元素。
6、Tuple匹配。(a,b,c)
7、类型匹配。对于java对象,由于并不适合Scala的Case Class模型,因此,可以使用类型进行匹配。在这种情况下,与构造子匹配是不同的。
再摘一段我以前编写的使用scala来编写应用程序的逻辑代码,让我们看看模式匹配在商业应用中的使用:
_req.transType match { case RechargeEcp | RechargeGnete | FreezeToAvailable => // 充值类交易 assert(_req.amount > 0, "金额不正确") case DirectPay | AvailableToFreeze => // 支付、冻结类交易 assert(_req.amount < 0, "金额不正确") case _ => assert(false, "无效交易类型") } val _account = queryEwAccount(_req.userId) assert(_account != null, "用户尚未开通电子钱包") var _accAvail, _accFreeze: EWSubAccount = null var _total: BigDecimal = _req.amount _account.subAccounts.find(_.subTypeCode==Available) match { case Some(x) => _accAvail = x; _total += x.balance case None => } _account.subAccounts.find(_.subTypeCode==Freeze) match { case Some(x) => _accFreeze = x; _total += x.balance case None=> }
这个仅仅是一个很简单的应用,试想使用Java的if/else或者switch来进行相同的代码,你不妨看看代码量会增加多少?可读性又会如何呢?
Scala Actor是一种借鉴于Erlang的进程消息机制的并发编程模式,由于Java中不存在Erlang的进程的概念,因此,Scala的Actor在隔离性上是不如Erlang的,譬如,在Erlang中,可以有效的终止一个进程,不仅仅无需担心死锁(根本没有锁),也可以马上释放掉改进程的内存,这种隔离性在某种程度上是更接近于操作系统的进程的。在Java的世界里暂时没有等效的替代品。
(题外话,最近在我们的Open Service Platform中集成了一个类似于操作系统定时调度的机制,可以定时执行一些任务,但是***,我们仍然决定将部分非交易相关的定时任务,主要是一些日志分析类、管理性批量处理等定时任务放到操作系统上进行调度,毕竟操作系统提供了一个更好的虚拟机,在OSGi层面仍然是有限的隔离,哪一天JVM能够提供像操作系统的隔离特性,那么,操作系统就真的不重要了)。
本文将对actor的机制进行简单的分析,以帮助加强对actor的理解。
package learn.actor object Test1 extends Application { import scala.actors.Actor._ val actor1 = actor { println("i am in " + Thread.currentThread) while(true) { receive { case msg => println("recieve msg:" + msg + " In " + Thread.currentThread); } } } val actor2 = actor { println("i am in " + Thread.currentThread) while(true) { receive { case msg: String => println("recieve msg:" + msg.toUpperCase + " In " + Thread.currentThread); } } } actor1 ! "Hello World" actor2 ! "Hello World" actor1 ! "ok" actor2 ! "ok" }
运行的结果是:
i am in Thread[pool-1-thread-1,5,main] i am in Thread[pool-1-thread-2,5,main] recieve msg:HELLO WORLD In Thread[pool-1-thread-2,5,main] recieve msg:Hello World In Thread[pool-1-thread-1,5,main] recieve msg:OK In Thread[pool-1-thread-2,5,main] recieve msg:ok In Thread[pool-1-thread-1,5,main]
从这个例子来看,actor1和actor2实际上是两个独立的Java线程,任何线程可以将消息以 ! 的方式发给给这个线程进行处理。由于采用消息的方式来进行通信,因此,线程与线程之间无需采用Java的notify/wait机制,而后者是建立在锁的基础之上的。有关于这一点,我不在本文只进行深入的分析了。(有必要的话,我会再写一个帖子来说明)。
那么 Scala Actor 的底层基础是什么呢?与Java的notify/wait就完全没有关系吗?我们将重点分析actor的三个方法:!, receive, react
1、Scala Actor的send(外部调用者发送一个消息给当前actor)和receive(当前actor接收一个消息),这两个操作是同步的(synchronized),也就是说,不可同时进入。(客观的说,这一块应该有很大的优化空间,应该采用乐观锁的机制,可能会有更好的效率,一来,send/receive操作本身都是很快速的操作,即便在出现冲突的情况下,使用乐观锁也可以降低线程切换引起的开销,而且,在大部分情况下,send操作与receive操作引发冲突的可能性并不是很大的。也就是说,在很大的程度上,send和receive还可以有更好的并行性,不知道后续的scala版本是否会进行优化。)
2、执行send操作时,如果当前actor正在等待这个消息(指actor自身已经在receive、react并且期待这个消息的情况下),那么原来的等待将会马上执行,否则,消息会进入到actor的邮箱,等待下次receive/react的处理。这种模式相较于全部放入邮箱更加有效。它避免了一次在邮箱上的同步等待。
3、当执行receive操作时,actor会检查对象的邮箱,如果有匹配的消息的话,则会马上返回该消息进行处理,否则会处在等待状态(当前线程阻塞,采用的是wait原语)当匹配的消息到达时,也是采用notify原语通知等待线程继续actor的处理的。
4、react与receive不同的是,react从不返回。这个在Java的编程世界里,好像还没有看到类似的东西,该如何理解它呢:
react(f: ParticialFunction[Any,Unit]) 首先检查actor的邮箱,如果有符合f的消息,则马上提取该消息,并且在一个ExecutionPool中调度执行f。(因此,f的执行肯定不在请求react这个线程中执行的。当前的调用react的线程,将产生一个 SuspendActorException,从而中断一般的执行过程。(也就是说文档中说的不返回的概念)
如果当前邮箱中没有消息,react将登记一个Continuation对象,将等待的消息(一个等待给定消息的函数)、获得消息后需要继续进行的处理在actor中进行登记,而后,当前线程会产生一个SuspendActorException,中断处理(从而是将当前线程归还到线程池)。
当消息到达(通过send)时,send将检查等待消息的Continuation,如过匹配的话,则会在线程池中的选择一个线程来执行f函数。在f处理完成一个消息后,一般的,它会再次调用 react来处理下一个消息,将再次重复这个过程。
应该说,scala的这个设计是非常精巧,也非常有效的,但这对Java开发程序员来说,就意味着一个新的挑战:看上去的一个函数体,实际上其中的代码不仅是执行不连续的(如closure可能会延迟、重复多次的被调用),甚至可能是在不同的线程中被执行的。
从这个概念上来看,scala的actor并不对应于Java的线程,相反,可以理解为一个行为执行者,是一个有上下文的非操作系统线程,语义其实更接近于现实的一个载体。这个与Erlang的进程还是有很明显的语义上的区别的。从上述的分析中,或许如果切换到乐观锁的机制,Scala的并发效率还能有更进一步的提升。
到此,关于“Scala中Trait有什么作用”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。