3. Scala特性

一、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 ,在方法体中每次用这个参数时都会触发计算,它有如下应用场景:

  1. 避免不需要的计算
  2. 包装计算,使得可以做到在计算前和计算后都做相应的操作
  3. 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

应用场景:

  1. 把要执行的参数包在try-catch中
  2. Setting some thread-local context while the argument is being evaluated???
  3. 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))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值