格式与命名
1) 代码格式用两个空格缩进。避免每行长度超过100列。在两个方法、类、对象定义之间使用一个空白行。
2) 优先考虑使用val,而非var。
3) 当引入多个包时,使用花括号:
import jxl.write.{WritableCell, Number, Label}
当引入的包超过6个时,应使用通配符_:
import org.scalatest.events._
4)若方法暴露为接口,则返回类型应该显式声明。例如:
def execute(conn: Connection) : Boolean =
{ executeCommand(conn, sqlStatement) match
{ case Right(result) => result case Left(_) => false } }
5) 集合的命名规范
xs, ys, as, bs等作为某种Sequence对象的名称;
x, y, z, a, b作为sequence元素的名称。
h作为head的名称,t作为tail的名称。
6)避免对简单的表达式采用花括号;
//suggestion def square(x: Int) = x * x //avoid def square(x: Int) =
{ x * x }
7) 泛型类型参数的命名虽然没有限制,但建议遵循如下规则:
A 代表一个简单的类型,例如List[A]
B, C, D 用于第2、第3、第4等类型。例如:
class List[A] {
def mapB: List[B] = …
}
N 代表数值类型
注意:在Java中,通常以K、V代表Map的key与value,但是在Scala中,更倾向于使用A、B代表Map的key与value。
8)数值类型变量
scala有7种数值类型:Byte、Char、Short、Int、Long、Float和Double,以及2种非数值类型:Boolean和Unit(只有一个值“()”,相当于java和c++中的void,即空值)。
这些类型都是抽象的final类(不能使用new新建,也不能被继承),在scala包中定义,是对java基本数据类型的包装,因此与java基本数据类型有相同的长度。
同时,scala还提供了RichInt、RichChar等等,它们分别提供Int、Char等所不具备的便捷方法。
另外,scala沿用了java.lang包中的String。在scala中,常量也称作字面量,字符串字面量由双引号包含的字符组成,同时scala提供了另一种定义字符串常量的语法——原始字符串,它以三个双引号作为开始和结束,字符串内部可以包含无论何种任意字符。
在scala中,我们使用方法,而不是强制类型转换,来做数值类型之间的转换,如99.44.toInt、97.toChar。另外也可以参见显式类型转换和隐式转换。
1.1. 命名规则
1.1.1. 程序文件
采用unix标准,比如user_info.scala,而不使用win32标准的UserInfo.scala。
文件后缀说明 |
|
后缀 |
说明 |
.scala |
scala源代码文件 |
.py |
python源代码文件 |
.java |
java源代码文件 |
.r |
r源代码文件 |
.proto | protobuf协议文件 |
.iml 、.xml |
配置文件 |
.htm、.html、.shtml |
页面文件 |
.sql |
SQL脚本、hivesql脚本 |
.sh |
/bin/sh脚本 |
Makefile |
make文件,无后缀 |
1.1.2. 变量、函数、类
个体类型前缀,驼峰式命名方法。
前缀 |
基本数据类型 |
说明 |
举例 |
b |
Boolean |
|
Boolean bStatus |
ch |
Char |
|
Char chStx |
l |
Long |
*因为32位操作系统和64操作系统在数据长度上的问题,强烈要求不要使用 |
Long lTimeValue |
i |
Int |
|
Int iFunReturn |
sh |
Short |
多用于表示尺寸 |
Short shCodeLen |
s |
java.lang包中的String。 |
|
String sUsrName |
f |
Float |
|
Float fMoney |
d |
Double |
|
Double dMoney |
o |
scala类实例对象 Vector List Queue Array HashMap HashSet Map |
|
通常命名时4个字母一下
全名,非时采用缩进剪短命名
在加变量本身含义
比如:
Vector oVecSeq List oListVal
HashMap oHMapVal
|
生存周期前缀列表
生命周期范围 |
前缀 |
备注 |
全局变量 |
g_ |
String g_sProgramName |
类生存周期变量 |
m_ |
String CClass::m_sName |
语法特性
1) 定义隐式类时,应该将构造函数的参数声明为val。
2)使用for表达式;如果需要条件表达式,应将条件表达式写到for comprehension中:
//not good for (file <- files) { if (hasSoundFileExtension(file) && !soundFileIsLong(file)) { soundFiles += file } } //better for { file <- files if hasSoundFileExtension(file) if !soundFileIsLong(file) } yield file
通常情况下,我们应优先考虑filter, map, flatMap等操作,而非for comprehension:
//best files.filter(hasSourceFileExtension).filterNot(soundFileIsLong)
3) 避免使用isInstanceOf,而是使用模式匹配,尤其是在处理比较复杂的类型判断时,使用模式匹配的可读性更好。
//avoid if (x.isInstanceOf[Foo]) { do something … //suggest def isPerson(x: Any) : Boolean = x match { case p: Person => true case _ => false }
4)以下情况使用abstract class,而不是trait:
- 想要创建一个需要构造函数参数的基类
- 代码可能会被Java代码调用
5) 如果希望trait只能被某个类(及其子类)extend,应该使用self type:
trait MyTrait { this: BaseType => }
如果希望对扩展trait的类做更多限制,可以在self type后增加更多对trait的混入:
trait WarpCore { this: Starship with WarpCoreEjector with FireExtinguisher => } // this works class Enterprise extends Starship
with WarpCore
with WarpCoreEjector
with FireExtinguisher
// won't compile
class Enterprise extends Starship
with WarpCore
with WarpCoreEjector
如果要限制扩展trait的类必须定义相关的方法,可以在self type中定义方法,这称之为structural type(类似动态语言的鸭子类型):
trait WarpCore { this: { def ejectWarpCore(password: String): Boolean def startWarpCore: Unit } => } class Starship class Enterprise extends Starship with WarpCore { def ejectWarpCore(password: String): Boolean = { if (password == "password") { println("core ejected"); true } else false } def startWarpCore { println("core started") } }
6) 对于较长的类型名称,在特定上下文中,以不影响阅读性和表达设计意图为前提,建议使用类型别名,它可以帮助程序变得更简短。例如:
class ConcurrentPool[K, V] { type Queue = ConcurrentLinkedQueue[V] type Map = ConcurrentHashMap[K, Queue] }
7) 如果要使用隐式参数,应尽量使用自定义类型作为隐式参数的类型,而避免过于宽泛的类型,如String,Int,Boolean等。
//suggestion def maxOfList[T](elements: List[T]) (implicit orderer: T => Ordered[T]): T = elements match { case List() => throw new IllegalArgumentException("empty list!") case List(x) => x case x :: rest => val maxRest = maxListImpParm(rest)(orderer) if (orderer(x) > maxRest) x else maxRest } //avoid def maxOfListPoorStyle[T](elements: List[T]) (implicit orderer: (T, T) => Boolean): T
8) 对于异常的处理,Scala除了提供Java风格的try…catch…finally之外,还提供了allCatch.opt、Try…Success…Failure以及Either…Right…Left等风格的处理方式。其中,Try是2.10提供的语法。根据不同的场景选择不同风格:
优先选择Try风格。Try很好地支持模式匹配,它兼具Option与Either的特点,因而既提供了集合的语义,又支持模式匹配,又提供了getOrElse()方法。同时,它还可以组合多个Try,并支持运用for combination。
val z = for { a <- Try(x.toInt ) b <- Try(y.toInt ) } yield a * b val answer = z.getOrElse (0) * 2
如果希望清楚的表现非此即彼的特性,应考虑使用Either。
注意,约定成俗下,我们习惯将正确的结果放在Either的右边(Right既表示右边,又表示正确)
如果希望将异常情况处理为None,则应考虑使用allCatch.opt。
import scala.util.control.Exception._ def readTextFile(f: String) : Option[List[String]] = allCatch.opt(Source.fromFile(f).getLines.toList)
如果希望在执行后释放资源,从而需要使用finally时,考虑try…catch…finally,或者结合try…catch…finally与Either。
private def executeQuery(conn: Connection, sql: String) : Either[SQLException, ResultSet] = { var stmt: Statement = null var rs: ResultSet = null try { stmt = conn.createStatement() rs = stmt.executeQuery(sql) Right(rs) } catch { case e: SQLException => { e.printStackTrace() Left(e) } } finally { try { if (rs != null) rs.close() if (stmt != null) stmt.close() } catch { case e: SQLException => e.printStackTrace() } } }
为避免重复,还应考虑引入Load Pattern。
编码风格
1) 尽可能直接在函数定义的地方使用模式匹配。例如,在下面的写法中,match应该被折叠起来(collapse):
list map { item => item match { case Some(x) => x case None => default } }
用下面的写法替代:
list map { case Some(x) => x case None => default }
它很清晰的表达了 list中的元素都被映射,间接的方式让人不容易明白。此时,传入map的函数实则为partial function。
2)避免使用null,而应该使用Option的None。
import java.io._ object CopyBytes extends App { var in = None: Option[FileInputStream] var out = None: Option[FileOutputStream] try { in = Some(new FileInputStream("/tmp/Test.class")) out = Some(new FileOutputStream("/tmp/Test.class.copy")) var c = 0 while ({c = in.get.read; c != −1}) { out.get.write(c) } } catch { case e: IOException => e.printStackTrace } finally { println("entered finally ...") if (in.isDefined) in.get.close if (out.isDefined) out.get.close } }
方法的返回值也要避免返回Null。应考虑返回Option,Either,或者Try。例如:
import scala.util.{Try, Success, Failure} def readTextFile(filename: String) : Try[List[String]] = { Try(io.Source.fromFile(filename).getLines.toList ) val filename = "/etc/passwd" readTextFile(filename) match { case Success(lines) => lines.foreach(println) case Failure(f) => println(f) }
3)若在Class中需要定义常量,应将其定义为val,并将其放在该类的伴生对象中:
class Pizza (var crustSize: Int, var crustType: String) { def this(crustSize: Int) { this(crustSize, Pizza.DEFAULT_CRUST_TYPE) } def this(crustType: String) { this(Pizza.DEFAULT_CRUST_SIZE, crustType) } def this() { this(Pizza.DEFAULT_CRUST_SIZE, Pizza.DEFAULT_CRUST_TYPE) } override def toString = s"A $crustSize inch pizza with a $crustType crust" } object Pizza { val DEFAULT_CRUST_SIZE = 12 val DEFAULT_CRUST_TYPE = "THIN" }
4)合理为构造函数或方法提供默认值。例如:
class Socket (val timeout: Int = 10000)
5)如果需要返回多个值时,应返回tuple。
def getStockInfo = { // ("NFLX", 100.00, 101.00) }
6) 作为访问器的方法,如果没有副作用,在声明时建议定义为没有括号。
例如,Scala集合库提供的scala.collection.immutable.Queue中,dequeue方法没有副作用,声明时就没有括号:
import scala.collection.immutable.Queue val q = Queue(1, 2, 3, 4) val value = q.dequeue
7) 将包的公有代码(常量、枚举、类型定义、隐式转换等)放到package object中。
package com.agiledon.myapp package object model { // field val MAGIC_NUM = 42 182 | Chapter 6: Objects  // method def echo(a: Any) { println(a) } // enumeration object Margin extends Enumeration { type Margin = Value val TOP, BOTTOM, LEFT, RIGHT = Value } // type definition type MutableMap[K, V] = scala.collection.mutable.Map[K, V] val MutableMap = scala.collection.mutable.Map }
8) 建议将package object放到与包对象命名空间一致的目录下,并命名为package.scala。以model为例,package.scala文件应放在:
+– com
+– agiledon
+– myapp
+– model
+– package.scala
9) 若有多个样例类属于同一类型,应共同继承自一个sealed trait。
sealed trait Message case class GetCustomers extends Message case class GetOrders extends Message
注:这里的sealed,表示trait的所有实现都必须声明在定义trait的文件中。
10) 考虑使用renaming clause来简化代码。例如,替换被频繁使用的长名称方法:
import System.out.{println => p} p("hallo scala") p("input")
11) 在遍历Map对象或者Tuple的List时,且需要访问map的key和value值时,优先考虑采用Partial Function,而非使用_1和_2的形式。例如:
val dollar = Map("China" -> "CNY", "US" -> "DOL") //perfer dollar.foreach { case (country, currency) => println(s"$country -> $currency") } //avoid dollar.foreach ( x => println(s"$x._1 -> $x._2") )
或者,考虑使用for comprehension:
for ((country, currency) <- dollar) println(s"$country -> $currency")
12) 遍历集合对象时,如果需要获得并操作集合对象的下标,不要使用如下方式:
val l = List("zero", "one", "two", "three") for (i <- 0 until l.length) yield (i, l(i))
而应该使用zipWithIndex方法:
for ((number, index) <- l.zipWithIndex ) yield (index, number)
或者:
l.zipWithIndex.map(x => (x._2, x._1))
当然,如果需要将索引值放在Tuple的第二个元素,就更方便了。直接使用zipWithIndex即可。
zipWithIndex的索引初始值为0,如果想指定索引的初始值,可以使用zip:
l.zip(Stream from 1)
13) 应尽量定义小粒度的trait,然后再以混入的方式继承多个trait。例如ScalaTest中的FlatSpec:
class FlatSpec extends FlatSpecLike ... trait FlatSpecLike extends Suite with ShouldVerb with MustVerb with CanVerb with Informing …
小粒度的trait既有利于重用,同时还有利于对业务逻辑进行单元测试,尤其是当一部分逻辑需要依赖外部环境时,可以运用“关注点分离”的原则,将不依赖于外部环境的逻辑分离到单独的trait中。
14) 优先使用不可变集合。如果确定要使用可变集合,应明确的引用可变集合的命名空间。不要用使用import scala.collection.mutable._;然后引用 Set,应该用下面的方式替代:
import scala.collections.mutable val set = mutable.Set()
这样更明确在使用一个可变集合。
15) 在自己定义的方法和构造函数里,应适当的接受最宽泛的集合类型。通常可以归结为一个: Iterable, Seq, Set, 或 Map。如果你的方法需要一个 sequence,使用 Seq[T],而不是List[T]。这样可以分离集合与它的实现,从而达成更好的可扩展性。
16) 应谨慎使用流水线转换的形式。当流水线转换的逻辑比较复杂时,应充分考虑代码的可读性,准确地表达开发者的意图,而不过分追求函数式编程的流水线转换风格。例如,我们想要从一组投票结果(语言,票数)中统计不同程序语言的票数并按照得票的顺序显示:
val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10)) val orderedVotes = votes .groupBy(_._1) .map { case (which, counts) => (which, counts.foldLeft(0)(_ + _._2)) }.toSeq .sortBy(_._2) .reverse
上面的代码简洁并且正确,但几乎每个读者都不好理解作者的原本意图。一个策略是声明中间结果和参数:
val votesByLang = votes groupBy { case (lang, _) => lang } val sumByLang = votesByLang map { case (lang, counts) => val countsOnly = counts map { case (_, count) => count } (lang, countsOnly.sum) } val orderedVotes = sumByLang.toSeq .sortBy { case (_, count) => count } .reverse
代码也同样简洁,但更清晰的表达了转换的发生(通过命名中间值),和正在操作的数据的结构(通过命名参数)。
17) 对于Options对象,如果getOrElse能够表达业务逻辑,就应避免对其使用模式匹配。许多集合的操作都提供了返回Options的方法。例如headOption等。
val x = list.headOption getOrElse 0
这要比模式匹配更清楚:
val x = list match case head::_ => head case Nil: => 0
18) 当需要对两个或两个以上的集合进行操作时,应优先考虑使用for表达式,而非map,flatMap等操作。此时,for comprehension会更简洁易读。例如,获取两个字符的所有排列,相同的字符不能出现两次。使用flatMap的代码为:
val chars = 'a' to 'z' val perms = chars flatMap { a => chars flatMap { b => if (a != b) Seq("%c%c".format(a, b)) else Seq() } }
使用for comprehension会更易懂:
val perms = for { a <- chars b <- chars if a != b } yield "%c%c".format(a, b)
高效编码
1) 应尽量避免让trait去extend一个class。因为这种做法可能会导致间接的继承多个类,从而产生编译错误。同时,还会导致继承体系的复杂度。
class StarfleetComponent trait StarfleetWarpCore extends StarfleetComponent class Starship extends StarfleetComponent with StarfleetWarpCore class RomulanStuff // won't compile class Warbird extends RomulanStuff with StarfleetWarpCore
2) 选择使用Seq时,若需要索引下标功能,优先考虑选择Vector,若需要Mutable的集合,则选择ArrayBuffer;若要选择Linear集合,优先选择List,若需要Mutable的集合,则选择ListBuffer。
3) 如果需要快速、通用、不变、带顺序的集合,应优先考虑使用Vector。Vector很好地平衡了快速的随机选择和快速的随机更新(函数式)操作。Vector是Scala集合库中最灵活的高效集合。一个原则是:当你对选择集合类型犹疑不定时,就应选择使用Vector。
需要注意的是:当我们创建了一个IndexSeq时,Scala实际上会创建Vector对象:
scala> val x = IndexedSeq(1,2,3) x: IndexedSeq[Int] = Vector(1, 2, 3)
4) 如果需要选择通用的可变集合,应优先考虑使用ArrayBuffer。尤其面对一个大的集合,且新元素总是要添加到集合末尾时,就可以选择ArrayBuffer。如果使用的可变集合特性更近似于List这样的线性集合,则考虑使用ListBuffer。
5) 如果需要将大量数据添加到集合中,建议选择使用List的prepend操作,将这些数据添加到List头部,最后做一次reverse操作。例如:
var l = List[Int]() (1 to max).foreach { i => i +: l } l.reverse
6) 当一个类的某个字段在获取值时需要耗费资源,并且,该字段的值并非一开始就需要使用。则应将该字段声明为lazy。
lazy val field = computation()
7) 在使用Future进行并发处理时,应使用回调的方式,而非阻塞:
//avoid val f = Future { //executing long time } val result = Await.result(f, 5 second) //suggesion val f = Future { //executing long time } f.onComplete { case Success(result) => //handle result case Failure(e) => e.printStackTrace }
8) 若有多个操作需要并行进行同步操作,可以选择使用par集合。例如:
val urls = List("http://scala-lang.org", "http://agiledon.github.com") def fromURL(url: String) = scala.io.Source.fromURL(url).getLines().mkString("\n") val t = System.currentTimeMillis() urls.par.map(fromURL(_)) println("time: " + (System.currentTimeMillis - t) + "ms")
9) 若有多个操作需要并行进行异步操作,则采用for comprehension对future进行join方式的执行。例如,假设Cloud.runAlgorithm()方法返回一个Futrue[Int],可以同时执行多个runAlgorithm方法:
val result1 = Cloud.runAlgorithm(10) val result2 = Cloud.runAlgorithm(20) val result3 = Cloud.runAlgorithm(30) val result = for { r1 <- result1 r2 <- result2 r3 <- result3 } yield (r1 + r2 + r3) result onSuccess { case result => println(s"total = $result") }
编码模式
1) Loan Pattern: 确保打开的资源(如文件、数据库连接)能够在操作完毕后被安全的释放。
Loan Pattern的通用格式如下:
def using[A](r : Resource)(f : Resource => A) : A = try { f(r) } finally { r.dispose() }
这个格式针对Resource类型进行操作。还有一种做法是:只要实现了close方法,都可以运用Loan Pattern:
def using[A <: def close():Unit, B][resource: A](f: A => B): B = try { f(resource) } finally { resource.close() }
以FileSource为例:
using(io.Source.fromFile("example.txt")) { source => { for (line <- source.getLines ) { println(line) } } }
2) Cake Pattern: 利用self type实现依赖注入
例如,对于DbAccessor而言,需要提供不同的DbConnectionFactory来创建连接,从而访问不同的Data Source。
trait DbConnectionFactory { def createDbConnection: Connection } trait SybaseDbConnectionFactory extends DbConnectionFactory… trait MySQLDbConnectionFactory extends DbConnectionFactory…
运用Cake Pattern,DbAccessor的定义应该为:
trait DbAccessor { this: DbConnectionFactory => //… }
由于DbAccessor使用了self type,因此可以在DbAccessor中调用DbConnectionFactory的方法createDbConnection()。客户端在创建DbAccessor时,可以根据需要选择混入的DbConnectionFactory:
val sybaseDbAccessor = new DbAccessor with SybaseDbConnectionFactory
当然,也可以定义object:
object SybaseDbAccessor extends DbAccessor with SybaseDbConnectionFactory object MySQLDbAccessor extends DbAccessor with MySQLDbConnectionFactory
测试
1) 测试类应该与被测试类处于同一包下。如果使用Spec2或ScalaTest的FlatSpec等,则测试类的命名应该为:被测类名 + Spec;若使用JUnit等框架,则测试类的命名为:被测试类名 + Test
2) 测试含有具体实现的trait时,可以让被测试类直接继承Trait。例如:
trait RecordsGenerator { def generateRecords(table: List[List[String]]): List[Record] { //... } } class RecordsGeneratorSpec extends FlatSpec with ShouldMatcher with RecordGenerator { val table = List(List("abc", "def"), List("aaa", "bbb")) it should "generate records" in { val records = generateRecords(table) records.size should be(2) } }
3) 若要对文件进行测试,可以用字符串假装文件:
type CsvLine = String def formatCsv(source: Source) : List[CsvLine] = { source.getLines(_.replace(", ", "|")) }
formatCsv需要接受一个文件源,例如Source.fromFile(“testdata.txt”)。但在测试时,可以通过Source.fromString方法来生成formatCsv需要接收的Source对象:
it should "format csv lines" in { val lines = Source.fromString("abc, def, hgi\n1, 2, 3\none, two, three") val result = formatCsv(lines) result.mkString("\n") should be("abc|def|hgi\n1|2|3\none|two|three") }
避免直接借用其他语言的编码规范
本文将讨论Scala中的一些编码规范,它们有助于减少编译和运行时的错误。
大家都有几门开发语言的编写能力,每种语言又有自己的语法格式和高质量编程要求,也有一些编码规范是在各种语言通用的,比如良好的注释。
但Scala与我们经常碰到的Java,C,C++还是有很大的不同,在深入理解Scala之前,最好谨慎使用其他语言相关的编码规范。
编码规范在团队开发中必须的。它帮助团队避免一些曾经出现的错误,提供代码层面交流的一致性语言。我们也许没有去看自己公司、团队的编码规范,但可以从代码中略知一二。
建立团队的编码规范的步骤有:
- 首先建立预防错误的一些规则。这些规则可能是来自使用相同语言的其他项目。然后自己添加一些从过去出错项目中总结的一些规则。比如C++中析构函数应该声明为虚函数。
- 然后根据团队自己的开发环境,来发现、定义一些新的编码规范。比如包的命名方式。
- 坚持执行前面的制定的规则。最好有一个自动化工具,能够检测我们的编码是否符合规范,然后可以自动做一些重构工作。
核心规则8条
大家肯定见过关于左大括号应该换行写,还是同行写的争论。当然这个争论无关痛痒,对编译器来说没有什么影响。我们看一个Scala例子:
class FooHolder
{
def foo1()
{
println("foo1 was called")
}
def foo2(): Unit =
{
println("foo2 was called")
}
def foo3() =
println("foo3 was called")
}
foo1、foo2、foo3都是正确的。都是定义一个函数,然后输出一个字符串。不同的是书写风格。foo1类似于C语言风格,但是没有指定返回值;foo2是标准完整的Scala函数定义,有返回值,有表达式;foo3虽然没有返回值,但是有表达式赋值。
需要注意的是,对于foo3,如果没有=。那么在实例化一个FooHolder的时候编译器会报错:class FooHolder needs to be abstract, since method foo3 is not defined。因为编译器会把它认为是一个抽象函数,这样的类去实例化是不允许的。虽然Scala给大家提供了一个非常宽松的环境,但为了避免类似的错误,也从人们理解上考虑,避免歧义,避免猜测,建议大家使用第二种foo2的方式。
悬垂操作符
dangling operator怎么翻译,没有找到一个现成的答案,那就自己定义一个名字:悬垂操作符。它指的是位于每行最后的操作符,比如+、-都可以作为悬垂操作符,它告诉Scala编译器本行还没有结束。
在Java中我们连接一个字符串可以随便写,同行也可以,分行也可以。但是Scala中,操作符需要考虑它们所在的位置。比如下面的代码:
val x = 5 def foo2 = "HAI" + x
+ "ZOMG"
+ "\n"
这个函数编译是会出错的:value unary_+ is not a member of String。String没有一元操作符+(Scala直接使用的Java的String)。但是x(类型为Int,Scala自己的一个类型)却是有的,所以+x没有报错。
为了解决这个编译错误,我们有两种办法:
一是告诉编译器+表示的是一行尚未完,即使用悬垂操作符:
val x = 5 def foo1 = "HAI" + x +
"ZOMG" +
"\n"
二是加上括号:
def foo2 = ("HAI" + x + "ZOMG" + "\n")
使用有意义的变量名
一般的语言标识符只能是字母、数字和下划线,外加一些限制。相比之下,Scala提供非常灵活的命名方式。Scala有三种方法可以构造一个标识符:
第一,首字符是字母,后续字符是任意字母和数字。这种标识符还可后接下划线‟_‟,然后是任意字母和数字。
第二,首字符是算符字符,后续字符是任意算符字符。这两种形式是普通标识符。
最后,标识符可以是由反引号‟`‟括起来的任意字符串(宿主系统可能会对字符串和合法性有些限制)。这种标识符可以由除了反引号的任意字符构成。
第二条规则的存在,很容易让人回想起C++的操作符重载。Scala直接将其当作标识符来处理,应该是更进来一步。
避免在标示符中使用$。因为编译器内部为内部类、闭包等生成的内部标示符使用了$。如果出现同名,将会导致代码奇怪的行为。有兴趣的话,可以看看编译生成的汇编代码。
在Scala2.8引入命名参数和默认参数。命名参数会作为API的一部分,名字的改变会使客户端代码出错。因此请使用有意义的名字来对参数进行命名。这就是核心规则6。
class Foo {
def foo(one: Int = 1,
two: String = "two",
three: Double = 2.5): String =
two + one + three
}
object Test extends scala.App{
val x = new Foo
println(x.foo())
println(x.foo(two = "not two"))
println(x.foo(0,"zero",0.1))
println(x.foo(4, three = 0.4))
println(x.foo(three = 0.4, one = 3, two = "two here"))
}
运行结果:
two12.5
not two12.5
zero00.1
two40.4
two here30.4
C++也有默认参数。但是没有参数命名。这样C++就有一些限制,需要默认参数放到右边。Scala参数有了名字,调用的时候,它们的顺序就可以任意存放。但如果调用的时候,有的参数直接传值,有的使用参数名字,比如上面的x.foo(4, three = 0.4),我们就需要注意没有使用名字的参数的顺序。
Scala使用变量的静态类型来绑定参数名字,但是缺省值是由运行时类型决定的。一句话:名字是静态的、值是动态。看一个例子:
class Parent {
def foo(bar: Int = 1, baz: Int = 2): Int =
bar + baz
}
class Child extends Parent {
override def foo(baz: Int = 3, bar: Int = 4): Int =
super.foo(baz,bar)
}
object Test extends scala.App{
val p = new Parent
println(p.foo())
val y = new Child
println(y.foo())
val z: Parent = new Child
println(z.foo())
println(y.foo(bar = 1))
println(z.foo(bar = 1))
println(z.foo(baz = 4))
}
输出如下:
3
7
7
4
5
7
Parent定义了foo,子类Child覆盖了foo。z.foo()使用的是缺省值,缺省值是由z的运行时类型Child提供的,所以baz=3,bar=4,输出为7。y.foo(bar = 1)运行时类型和静态类型都是Child,所以baz=3,bar=1,输出为4。z.foo(bar = 1)的运行时类型是Child,静态类型是Parent,函数使用静态类型的,即Parent的def foo(bar: Int = 1, baz: Int = 2): Int。但要注意的是Child和Parent的命名参数位置是反的。缺省值使用静态类型的,所以baz=4(Parent的baz对应Child的bar),bar=1,输出为5。同样的,可以得到z.foo(baz = 4)的结果是7。
可以看到对于交换了命名参数位置的重载,从理解上看,不直观,比较困难。所有大家应该保持重载的命名参数是一致的。不要随意交换它们的位置。
重载带有命名参数的函数,可以修改函数的默认值。
告诉大家这是重载函数
核心规则7:Scala中,虽然有的时候override是可选的,但是坚持使用override是安全的。
Trait与Java的interface类似,但是可以拥有方法体,并且可以在类实例化的时候混入。我们编写下面的代码:
trait UserService {
def login(credentials: Credentials): UserSession
def logout(session: UserSession): Unit
def isLoggedIn(session: UserSession): Boolean
def changePassword(new_credentials: Credentials,
old_credentials: Credentials): Boolean
}
class UserServiceImpl extends UserService {
def login(credentials: Credentials): UserSession =
new UserSession {}
def logout(session: UserSession): Unit //class UserServiceImpl needs to be abstract, since method logout is not defined
def isLoggedIn(session: UserSession): Boolean = true
def changePassword(session: UserSession,
credentials: Credentials): Boolean = true
}
编译失败。因为编译器发现UserServiceImpl的logout还是一个抽象函数,没有实现,这在class中是不允许的。还有一个错误是UserService的ChangePassword作为一个抽象函数也没有实现。
如果我们给UserServiceImp加上override关键字,也可以通过编译:
trait UserServiceImpl extends UserService {
override def login(credentials: Credentials): UserSession =
new UserSession {}
override def logout(session: UserSession): Unit
override def isLoggedIn(session: UserSession): Boolean = true
override def changePassword(session: UserSession, credentials: Credentials): Boolean = true
}
这是因为Scala在实现一个抽象方法的时候,不需要override关键字。这样的设置是为了解决多重继承的菱形继承问题。举个例子:
trait Animal {
def talk: String
}
trait Cat extends Animal {
override def talk: String = "Meow"
}
trait Dog extends Animal {
override def talk: String = "Woof"
}
object Test extends scala.App
{
val kittydoggy = new Cat with Dog
println(kittydoggy.talk) //Woof
val kittydoggy2 = new Dog with Cat
println(kittydoggy2.talk) //Meow
}
大家可能不理解为什么输出是这样?这是Scala的类线性化(Class linearization)决定的。当存在多个函数的多个重载实现的时候,Scala从类声明的最右边开始,在每个类中寻找函数,如果找到,就停止查找。new Cat with Dog会先看Dog有没有定义talk,Dog定义了talk,并返回Woof,所以输出是Woof。类似地,new Dog with Cat会在Cat里面找talk函数。
如果我们去掉Dog和Cat里面的override。编译器会报下面的错误:
anonymous class $anon inherits conflicting members:
method talk in trait Cat of type => String and
method talk in trait Dog of type => String
(Note: this can be resolved by declaring an override in anonymous class $anon.)
val kittydoggy = new Cat with Dog
^
anonymous class $anon inherits conflicting members:
method talk in trait Dog of type => String and
method talk in trait Cat of type => String
(Note: this can be resolved by declaring an override in anonymous class $anon.)
val kittydoggy2 = new Dog with Cat
^
如果我们只是去掉Dog的override,编译器会报:
anonymous class $anon inherits conflicting members:
method talk in trait Cat of type => String and
method talk in trait Dog of type => String
(Note: this can be resolved by declaring an override in anonymous class $anon.)
val kittydoggy = new Cat with Dog
^
在Cat中混入Dog是不允许的,因为Dog没有说它的talk方法可以被重载。反过来,Dog中混入Cat是可以的,因为Cat说明了自己的talk方法可以被重载。
为了重载某个方法,我们可以在类实例化的时候混入trait,而不需要定义一个新的类。
对期待的优化使用注解
Scala编译器在生成字节码的时候做一些优化操作:
- 优化尾递归【注1】
- 优化模式匹配
Scala通过将模式匹配当做switch来处理,来优化模式匹配的效率。模式匹配优化时,编译器编译生成分支表,而不是决策树。这意味着,我们不是在拿值做比较。而是用匹配的值来直接定位分支表。通过JVM的tableswitch操作码可以直接完成。
Scala使用tableswitch进行优化,必须满足3个条件:
- 用俩匹配的值必须是一个已知的整数
- 每个匹配表达式必须足够简单:不能包含类型检查、if语句和extractors。如果是表达式需要在编译时就是可用的:它保持不变,不能在运行时求值。
- 至少有两个以上分支,否则优化是没有必要的。
我们继续看个例子:
def unannotated(x: Int) = x match {
case 1 => "One"
case 2 => "Two!"
case z => z + "?"
}
使用javap -c得到汇编代码:
public java.lang.String unannotated(int);
Code:
0: iload_1
1: istore_2
2: iload_2
3: tableswitch{ //1 to 2
1: 51;
2: 46;
default: 24 }
24: new #12; //class scala/collection/mutable/StringBuilder
27: dup
28: invokespecial #16; //Method scala/collection/mutable/StringBuilder.”<init>”:()V
31: iload_2
32: invokevirtual #20; //Method scala/collection/mutable/StringBuilder.append:(I)Lscala/collection/mutable/StringBuilder;
35: ldc #22; //String ?
37: invokevirtual #25; //Method scala/collection/mutable/StringBuilder.append:(Ljava/lang/Object;)Lscala/collection/mutable/StringBuilder;
40: invokevirtual #29; //Method scala/collection/mutable/StringBuilder.toString:()Ljava/lang/String;
43: goto 53
46: ldc #31; //String Two!
48: goto 53
51: ldc #33; //String One
53: areturn
在得到参数后,放到临时变量,然后调用tableswith指令,通过索引访问跳转表,并跳转。
我们修改一下上面的代码:
def notOptimised(x: Int) = x match {
case 1 => "One"
case 2 => "Two!"
case i: Int => "Other"
}
在最后一个分支上,加上了类型检查。这样就不满足前面的条件2,Scala不会进行优化了。
public java.lang.String notOptimised(int);
Code:
0: iload_1
1: istore_2
2: iconst_1
3: iload_2
4: if_icmpne 13
7: ldc #12; //String One
9: astore_3
10: goto 27
13: iconst_2
14: iload_2
15: if_icmpne 24
18: ldc #14; //String Two!
20: astore_3
21: goto 27
24: ldc #16; //String Other
26: astore_3
27: aload_3
28: areturn
使用if_icmpne指令来判断两个int是否相等,进而决定是不是需要进行跳转。
我们如何知道编译器是否做了模式匹配的优化呢?Scala可以给类型表达式使用注解。@switch告诉编译器我想做tableswitch优化。如果编译器发现它做不了优化就会报错。比如下面的代码:
import annotation.switch
class Tableswitch{
def annotated(x: Int @switch) = x match {
case 1 => "One"
case 2 => "Two!"
case z => z + "?"
}
def notOptimised(x: Int) =
(x: @switch) match {
case 1 => "One"
case 2 => "Two!"
case i: Int => "Other"
}
}
在第11行会报错:
could not emit switch for @switch annotated match
(x: @switch) match {
^
@tailrec注解用来告诉编译器,请对尾递归进行优化。
核心规则8:对需要优化的尾递归,加上@tailrec注解,保证我们得到期望的性能优化。
Scala编译器进行尾递归优化也需要满足3个条件:
- 方法必须声明为final或private,不能是多态的。
- 方法必须有返回值注解。
- 方法必须在其一个返回分支上最后调用自己。
我们有一个普通递归和一个尾递归:
import annotation.tailrec
object TailRecursion{
def FibonacciRecursive(n: Int): Int = {
if(n < 2)
n
else
FibonacciRecursive(n-1)+FibonacciRecursive(n-2)
}
@tailrec
def FibonacciTailRecursive(n: Int, ret1: Int, ret2: Int): Int = {
if(n < 2)
ret1
else
FibonacciTailRecursive(n-1, ret2, ret1 + ret2)
}
}
它们的汇编代码:
public int FibonacciRecursive(int);
Code:
0: iload_1
1: iconst_2
2: if_icmpge 9
5: iload_1
6: goto 24
9: aload_0
10: iload_1
11: iconst_1
12: isub
13: invokevirtual #16; //Method FibonacciRecursive:(I)I
16: aload_0
17: iload_1
18: iconst_2
19: isub
20: invokevirtual #16; //Method FibonacciRecursive:(I)I
23: iadd
24: ireturn
public int FibonacciTailRecursive(int, int, int);
Code:
0: iload_1
1: iconst_2
2: if_icmpge 7
5: iload_2
6: ireturn
7: iload_1
8: iconst_1
9: isub
10: iload_3
11: iload_2
12: iload_3
13: iadd
14: istore_3
15: istore_2
16: istore_1
17: goto 0
可见Scala对后者进行了优化。使用的是goto循环。但是如果我们在FibonacciRecursive加上@tailrec注解,编译器就会报错:
could not optimize @tailrec annotated method FibonacciRecursive: it contains a recursive call not in tail position
FibonacciRecursive(n-1)+FibonacciRecursive(n-2)
^
优化注解并不是要求编译器去做优化,而是要求编译或发出警告。
【注1】尾递归指的是函数最后一句调用自身的函数。尾递归优化可以减少递归需要的栈空间。一般将其展开为循环,或者重复使用当前栈空间。尾递归会增加理解的难度,并且不少编译器是不提供尾递归优化的。
参考资料
来源于网络
- Scala Style Guide
- Programming in Scala , Martin Odersky
- Scala Cookbook , Alvin Alexander
- Effective Scala , Twitter
-
深入理解Scala-编码规范