一、Case Classes and Sealed Traits
1.1 Case Classes
样例类和普通类很相似,但是样例类中只有数据,但是样例类中的所有属性都是公共的且不可变的,没有任何可变状态和封装。它的名字来源于模式匹配中的 case 关键字。
样例类在初始化的时候可以省略 new 关键字,它的构造方法和所有属性都是公共的。
@ case class Point(x: Int, y: Int)
defined class Point
@ val p = Point(1, 2)
p: Point = Point(1, 2)
@ p.x
res21: Int = 1
@ p.y
res22: Int = 2
类似于普通类,可以在样例类中定义实例方法或属性:
@ case class Point(x: Int, y: Int) {
def z = x + y
}
defined class Point
@ val p = Point(1, 2)
p: Point = Point(1, 2)
@ p.z
res25: Int = 3
1.2 Sealed Traits
trait也可以被sealed修饰,这种trait只能被固定的case class继承,在脚本中,需要定义在同一个{}中,在文件中,需要定义在同一个文件。
@ {
sealed trait Point
case class Point2D(x: Double, y: Double) extends Point
case class Point3D(x: Double, y: Double, z: Double) extends Point
}
defined trait Point
defined class Point2D
defined class Point3D
@ def hypotenuse(p: Point) = p match {
case Point2D(x, y) => math.sqrt(x * x + y * y)
case Point3D(x, y, z) => math.sqrt(x * x + y * y + z * z)
}
defined function hypotenuse
@ val points: Array[Point] = Array(Point2D(1, 2), Point3D(4, 5, 5))
points: Array[Point] = Array(Point2D(1.0, 2.0), Point3D(4.0, 5.0, 5.0))
@ for(p <- points) println(hypotenuse(p))
2.23606797749979
8.12403840463596
一般来说,sealed traits用于期望子类的数量是固定的,不希望被到处继承的场景,这样在修改trait中的方法时,不会导致需要修改很多代码。
二、Pattern Matching
2.1 Matching
match中用case _ => 来定义默认情况
2.1.1 Matching on primitives
@ def dayOfWeek(x: Int) = x match {
case 1 => "Mon"; case 2 => "Tue"
case 3 => "Wed"; case 4 => "Thu"
case 5 => "Fri"; case 6 => "Sat"
case 7 => "Sun"; case _ => "Unknown"
}
defined function dayOfWeek
@ dayOfWeek(5)
res31: String = "Fri"
@ dayOfWeek(-1)
res32: String = "Unknown"
2.1.2 Matching on tuple
@ for (i <- Range.inclusive(1, 100)) {
val s = (i % 3, i % 5) match {
case (0, 0) => "FizzBuzz"
case (0, _) => "Fizz"
case (_, 0) => "Buzz"
case _ => i
}
println(s)
}
1
2
Fizz
4
Buzz
Fizz
....
2.1.3 Matching on Case Classes
case class不仅可以像1.2中示例那样匹配到类型,还可以匹配具体的值
@ def direction(p: Point) = p match {
case Point(0, 0) => "origin"
case Point(_, 0) => "horizontal"
case Point(0, _) => "vertical"
case _ => "diagonal"
}
defined function direction
@ direction(Point(0, 0))
res38: String = "origin"
@ direction(Point(1, 1))
res39: String = "diagonal"
2.1.4 Matching on String Patterns(从scala2.13开始才能用)
def splitDate(s: String) = s match {
case s"$day-$month-$year" =>
s"day: $day, mon: $month, yr: $year"
case _ => "not a date"
}
2.1.5 嵌套匹配
2.1.5.1 在case class中应用字符串匹配
case class Person(name: String, title: String)
object NestPattern {
def greet(p: Person) = p match {
//在name上应用字符串匹配
case Person(s"$firstName $lastName", title) => println(s"Hello $title $lastName")
case Person(name, title) => println(s"Hello $title $name")
}
def main(args: Array[String]): Unit = {
greet(Person("Haoyi Li", "Mr")) //Hello Mr Li
greet(Person("Zhao", "Dr")) //Hello Dr Zhao
}
}
2.1.5.2 在tuple中应用case class和字符串匹配
object NestPattern {
def greet2(husband: Person, wife: Person) = (husband, wife) match {
//tuple中是case class, 在case中用字符串匹配
//注意:在模式匹配中,if要写在 => 左边
case (Person(s"$first1 $last1", _), Person(s"$first2 $last2", _)) if last1 == last2 => println(s"Hello Mr and Ms $last1")
case (Person(name1, _), Person(name2, _)) => println(s"Hello $name1 and $name2")
}
def main(args: Array[String]): Unit = {
greet2(Person("James Bond", "Mr"), Person("Jane Bond", "Ms")) //Hello Mr and Ms Bond
greet2(Person("James", "Mr"), Person("Jane Bond", "Ms")) //Hello James and Jane Bond
}
}
2.3 用模式匹配输出/计算表达式
trait Expr
case class BinOp(left: Expr, op: String, right: Expr) extends Expr
case class Literal(value: Int) extends Expr
case class Variable(name: String) extends Expr
object Simplify {
def evaluate(expr: Expr, values: Map[String, Int]): Int = {
expr match {
case BinOp(left, "+", right) => evaluate(left, values) + evaluate(right, values)
case BinOp(left, "-", right) => evaluate(left, values) - evaluate(right, values)
case BinOp(left, "*", right) => evaluate(left, values) * evaluate(right, values)
case Literal(value) => value
case Variable(name) => values(name)
}
}
def printExpr(expr: Expr): String = {
expr match {
case BinOp(left, op, right) => s"(${printExpr(left)} ${op} ${printExpr(right)})"
case Literal(value) => value.toString
case Variable(name) => name
case _ => "hh"
}
}
def simplifyExpr(expr: Expr): Expr = {
expr match {
case BinOp(Literal(left), "+", Literal(right)) => Literal(left + right)
case BinOp(Literal(left), "-", Literal(right)) => Literal(left - right)
case BinOp(Literal(left), "*", Literal(right)) => Literal(left * right)
case BinOp(left, "*", Literal(1)) => left
}
}
def main(args: Array[String]): Unit = {
val largeExpr = BinOp(
BinOp(Variable("x"), "+", Literal(1)),
"*",
BinOp(Variable("y"), "-", Literal(1))
)
val print_expr = printExpr(largeExpr)
println(print_expr) //((x + 1) * (y - 1))
val expr_res = evaluate(largeExpr, Map("x" -> 10, "y" -> 20))
println(expr_res) //209
}
}
三、By-Name Parameters
scala支持 “by-name” 方法参数,语法是 : => T ,在方法体中每次用这个参数时都会触发计算,它有如下应用场景:
- 避免不需要的计算
- 包装计算,使得可以做到在计算前和计算后都做相应的操作
- Repeating evaluation of the argument more than once?看不懂
3.1 避免不需要的计算
如下操作,如果level > logLevel 不成立,那就不需要构建 msg 信息,可以节省 CPU时间
@ var logLevel = 1
@ def log(level: Int, msg: => String) = {
if (level > logLevel) println(msg)
}
@ log(2, "Hello " + 123 + " World")
Hello 123 World
@ logLevel = 3
@ log(2, "Hello " + 123 + " World")
<no output>
通常情况下,一个方法不会同时使用到它的所有参数,类似于示例中的,如果msg不需要,那就不需要计算;我们可以在对性能严格的应用中通过这种方式减少CPU时间和对象分配时间来提升性能。
在Scala集合中的getOrElse和getOrElseUpdate方法中就是用的这种技术。
3.2 包装计算
通过使用 by-name parameters 可以使得在参数中的代码执行前和执行后都执行一些操作,如下代码,我们在参数计算前后都执行System.currentTimeMillis(),这种就可以得到参数执行了多久
@ def measureTime(f: => Unit) = {
val start = System.currentTimeMillis()
f
val end = System.currentTimeMillis()
println("Evaluate took " + (end - start) + " milliseconds")
}
defined function measureTime
@ measureTime(new Array[String](10 * 1000 * 1000).hashCode())
Evaluate took 5 milliseconds
@ measureTime(new Array[String](100 * 1000 * 1000).hashCode())
Evaluate took 165 milliseconds
应用场景:
- 把要执行的参数包在try-catch中
- Setting some thread-local context while the argument is being evaluated???
- valuating the argument in a Future so the logic runs asynchronously on another thread???
四、Implicit Parameters
隐式参数是在调用方法时可以自动填充的参数。
如下:
@ class Foo(val value: Int)
defined class Foo
@ def bar(implicit foo: Foo) = foo.value + 10
defined function bar
@ implicit val foo: Foo = new Foo(1)
foo: Foo = ammonite.$sess.cmd14$Foo@54463380
@ bar
res17: Int = 11
@ bar(foo)
res18: Int = 11
隐式参数类似于方法中的默认值,但是与默认值不同的是,默认值是硬编码在代码里写死的,而隐式参数是可以变化的
注意:如果在作用域内检测到两个同类型的隐式变量,那编译器就不知道该传递哪个了,程序就会报错
@ implicit val foo: Foo = new Foo(1)
foo: Foo = ammonite.$sess.cmd14$Foo@43ab0659
@ implicit val foo2: Foo = new Foo(1)
foo2: Foo = ammonite.$sess.cmd14$Foo@3516b881
@ bar
cmd21.sc:1: ambiguous implicit values:
both value foo in object cmd19 of type => ammonite.$sess.cmd14.Foo
and value foo2 in object cmd20 of type => ammonite.$sess.cmd14.Foo
match expected type ammonite.$sess.cmd14.Foo
val res21 = bar
^
Compilation Fail
通过隐式参数,可以让程序更简洁
五、Typeclass Inference(类型推断)
隐式参数的另一个作用是可以将变量值自动关联其类型。
举例:
如果我们想将给定的字符串转化成各种其他类型:Int、Boolean、Double,很容易想到的实现思路是用方法重写,如下:
trait StrParse[T] { def parse(s: String): T }
object ParseInt extends StrParser[Int] { override def parse(s: String): Int = s.toInt }
object ParseBoolean extends StrParser[Boolean] { override def parse(s: String): Boolean = s.toBoolean }
object ParseDouble extends StrParser[Double] { override def parse(s: String): Double = s.toDouble }
@ val args = Seq("123", "true", "7.5")
args: Seq[String] = List("123", "true", "7.5")
@ val myInt = ParseInt.parse(args(0))
myInt: Int = 123
@ val myBoolean = ParseBoolean.parse(args(1))
myBoolean: Boolean = true
@ val myDouble = ParseDouble.parse(args(2))
myDouble: Double = 7.5
但是,我们如果加了新需求,不仅想对给定好的字符串转换,还想对控制台输入的字符串进行转化,那实现方式就是将上面定义好的StrParse当作参数传入到parseFromConsole中,如下:
def parseFromConsole[T](parser: StrParse[T]): Unit = {
parser.parse(scala.Console.in.readLine())
}
parseFromConsole[Int](ParseInt)
parseFromConsole[Boolean](ParseBoolean)
parseFromConsole[Double](ParseDouble)
这种实现方式很好了,但是依然会像第四节提到的那样会使得代码冗余,需要在很多地方都写傻姑娘ParseFoo对象(Foo为Int、Boolean、Double),而且在很多情况下在作用域内,只会有一个 StrParser[Int](必须有这个前提),那我们其实可以用隐式参数来简化代码。
trait StrParse[T] { def parse(s: String): T }
object StrParse {
implicit object ParseInt extends StrParse[Int] { override def parse(s: String): Int = s.toInt }
implicit object ParseBoolean extends StrParse[Boolean] { override def parse(s: String): Boolean = s.toBoolean }
implicit object ParseDouble extends StrParse[Double] { override def parse(s: String): Double = s.toDouble }
}
如上代码,我们在 StrParse object 中定义了隐式对象 ParseInt、ParseBoolean、ParseDouble,StrParse与接口同名,我们称之为接口的伴生对象,伴生对象经常用于将隐式变量、静态方法、工厂方法与其他和接口或类有关的信息存放在一起,但是不会存放与实例相关的信息。
通过这种定义,我们就可以通过类型来匹配到要传入的parse了
def parseFromString[T](s: String)(implicit parser: StrParse[T]): T = {
parser.parse(s)
}
val args = Seq("123", "true", "7.5")
val myInt = parseFromString[Int](args(0))
val myBoolean = parseFromString[Boolean](args(1))
val myDouble = parseFromString[Double](args(2))
def parseFromConsole[T](implicit parser: StrParser[T]): Unit = {
parser.parse(scala.Console.in.readLine())
}
我们也可以通过反射来实现相同的功能,但是依赖反射会容易产生运行时错误或运行时难以发现的bug,而scala提供的这种语法糖是在编译期间实现的,如果有错误,我们可以及时发现。
六、Recursive Typeclass Inference
类型推断不仅可以用于基本数据类型,还可以用于复杂数据类型,如Seq[Int],(Int, Boolean)、Seq[(Int, Boolean)]
例1:解析Seq
implicit def ParseSeq[T](implicit p: StrParse[T]): StrParse[Seq[T]] = new StrParse[Seq[T]] {
override def parse(s: String): Seq[T] = s.split(",").toSeq.map(p.parse)
}
val args2 = "1,2,3,4"
parseFromString[Seq[Int]](args2)
注意:这里我们定义的是一个隐式函数,而不是隐式对象,因为依据于T的具体类型,我们需要不同的StrParse[Seq[T]]
所以隐式参数不仅可以传递object,还可以传递函数,只要函数的返回类型和需要的类型一致即可!
例2:解析Tuple
implicit def ParseTuple[T, K](implicit p1: StrParse[T], p2: StrParse[K]): StrParse[(T, K)] = new StrParse[(T, K)] {
override def parse(s: String): (T, K) = {
val Array(left, right) = s.split("=")
(p1.parse(left), p2.parse(right))
}
}
parseFromString[(Int, Boolean)]("123=true")
例3:通过上述定义的解析Seq和解析Tuple,我们现在就可以解析嵌套结构了
@ parseFromString[Seq[(Int, Boolean)]]("1=true,2=false,3=true,4=false")
res104: Seq[(Int, Boolean)] = ArraySeq((1, true), (2, false), (3, true), (4, false))
@ parseFromString[(Seq[Int], Seq[Boolean])]("1,2,3,4,5=true,false,true")
res105: (Seq[Int], Seq[Boolean]) = (ArraySeq(1, 2, 3, 4, 5), ArraySeq(true, false, true))