你好,游客 登录
背景:
阅读新闻

Scala新手教程:scala 从入门到入门+

[日期:2018-09-03] 来源:csdn  作者:lihancheng [字体: ]

新手向,面向刚从java过渡到scala的同学,目的是写出已已易于维护和阅读的代码.

从语句到表达式

语句(statement): 一段可执行的代码
表达式(expression): 一段可以被求值的代码

在Java中语句和表达式是有区分的,表达式必须在return或者等号右侧,而在scala中,一切都是表达式.

一个例子:
假设我们在公司的内网和外网要从不同的域名访问一样的机器

  1. //Java代码
  2. String urlString = null;
  3. String hostName = InetAddress.getLocalHost().getHostName();
  4. if (isInnerHost(hostName)) {
  5. urlString = "http://inner.host";
  6. } else {
  7. urlString = "http://outter.host";
  8. }

刚转到scala的人很可能这么写

  1. var urlString: String = null
  2. var hostName = InetAddress.getLocalHost.getHostName
  3. if (isInnerHost(hostName)) {
  4. urlString = "http://inner.host"
  5. } else {
  6. urlString = "http://outter.host"
  7. }

我们让它更像scala一点吧

  1. val hostName = InetAddress.getLocalHost.getHostName
  2. val urlString = if (isInnerHost(hostName)) {
  3. "http://inner.host"
  4. } else {
  5. "http://outter.host"
  6. }

这样做的好处都有啥?

  1. 代码简练,符合直觉
  2. urlString 是值而不是变量,有效防止 urlString 在后续的代码中被更改(编译时排错)

很多时候,我们编程时说的安全并不是指怕被黑客破坏掉,而是预防自己因为逗比而让程序崩了.

纯函数和非纯函数

纯函数(Pure Function)是这样一种函数——输入输出数据流全是显式(Explicit)的。
显式(Explicit)的意思是,函数与外界交换数据只有一个唯一渠道——参数和返回值;函数从函数外部接受的所有输入信息都通过参数传递到该函数内部;函数输出到函数外部的所有信息都通过返回值传递到该函数外部。

如果一个函数通过隐式(Implicit)方式,从外界获取数据,或者向外部输出数据,那么,该函数就不是纯函数,叫作非纯函数(Impure Function)。
隐式(Implicit)的意思是,函数通过参数和返回值以外的渠道,和外界进行数据交换。比如,读取全局变量,修改全局变量,都叫作以隐式的方式和外界进行数据交换;比如,利用I/O API(输入输出系统函数库)读取配置文件,或者输出到文件,打印到屏幕,都叫做隐式的方式和外界进行数据交换。

  1. //一些例子
  2. //纯函数
  3. def add(a:Int,b:Int)= a + b
  4. //非纯函数
  5. var a = 1
  6. def addA(b:Int)= a + b
  7.  
  8. def add(a:Int,b:Int)= {
  9. println(s"a:$a b:$b")
  10. a + b
  11. }
  12. def randInt()= Random.nextInt()

纯函数的好处(来自维基百科)

  • 无状态,线程安全,不需要线程同步.
  • 纯函数相互调用组装起来的函数,还是纯函数.
  • 应用程序或者运行环境(Runtime)可以对纯函数的运算结果进行缓存,运算加快速度.

纯函数的好处(来自我的经验)

  • 单元测试非常方便!
  • 分布式/并发环境下,断点调试的方式无以为继,你需要单元测试.

单元测试什么的,赶紧去 http://www.scalatest.org 试试吧

惰性求值/Call by name

维基百科中惰性求值的解释
惰性求值(Lazy Evaluation),又称惰性计算、懒惰求值,是一个计算机编程中的一个概念,它的目的是要最小化计算机要做的工作。它有两个相关而又有区别的含意,可以表示为“延迟求值”和“最小化求值”,本条目专注前者,后者请参见最小化计算条目。除可以得到性能的提升外,惰性计算的最重要的好处是它可以构造一个无限的数据类型。
惰性求值的相反是及早求值,这是一个大多数编程语言所拥有的普通计算方式。

惰性求值不是新鲜事

  1. import scala.io.Source.fromFile
  2. val iter: Iterator[String] =
  3. fromFile("sampleFile")
  4. .getLines()

文件迭代器就用到了惰性求值.
用户可以完全像操作内存中的数据一样操作文件,然而文件只有一小部分传入了内存中.

用lazy关键词指定惰性求值

  1. lazy val firstLazy = {
  2. println("first lazy")
  3. 1
  4. }
  5. lazy val secondLazy = {
  6. println("second lazy")
  7. 2
  8. def add(a:Int,b:Int)= {
  9. a+b
  10. }
  1. //在 scala repl 中的结果
  2. scala> add(secondLazy,firstLazy)
  3. second lazy
  4. first lazy
  5. res0: Int = 3
  6.  
  7. res0: Int = 3

second lazy 先于 first lazy输出了

Call by value 就是函数参数的惰性求值

  1. deffirstLazy = {
  2. println("first lazy")
  3. 1
  4. }
  5. defsecondLazy = {
  6. println("second lazy")
  7. 2
  8. }
  9. defchooseOne(first:Boolean, a:Int, b:Int) = {
  10. if (first) a else b
  11. }
  12. defchooseOneLazy(first:Boolean, a: => Int, b: => Int) = {
  13. if (first) a else b
  14. }
  1. chooseOne(first = true, secondLazy, firstLazy)
  2. //second lazy
  3. //first lazy
  4. //res0: Int = 2
  5. chooseOneLazy(first = true, secondLazy, firstLazy)
  6. //second lazy
  7. //res1: Int = 2

对于非纯函数,惰性求值会产生和立即求值产生不一样的结果.

一个例子,假设你要建立一个本地缓存

  1. //需要查询mysql等,可能来自于一个第三方jar包
  2. def itemIdToShopId: Int => Int  
  3. var cache = Map.empty[Int, Int]
  4. def cachedItemIdToShopId(itemId: Int):Int = {
  5. cache.get(itemId) match {
  6. case Some(shopId) => shopId
  7. case None =>
  8. val shopId = itemIdToShopId(itemId)
  9. cache += itemId -> shopId
  10. shopId
  11. }
  12. }
  • 罗辑没什么问题,但测试的时候不方便连mysql怎么办?
  • 如果第三方jar包发生了改变,cachedItemIdToShopId也要发生改变.
  1. //用你的本地mock来测试程序
  2. def mockItemIdToSHopId: Int => Int
  3. def cachedItemIdToShopId(itemId: Int): Int ={
  4. cache.get(itemId) match {
  5. caseSome(shopId)=> shopId
  6. case None =>
  7. val shopId = mockItemIdToSHopId(itemId)
  8. cache += itemId -> shopId
  9. shopId
  10. }
  11. }
  • 在测试的时候用mock,提交前要换成线上的,反复测试的话要反复改动,非常令人沮丧.
  • 手工操作容易忙中出错.
  1. //将远程请求的结果作为函数的一个参数
  2. def cachedItemIdToShopId(itemId: Int, remoteShopId: Int): Int = {
  3. cache.get(itemId) match {
  4. caseSome(shopId)=> shopId
  5. case None =>
  6. val shopId = remoteShopId
  7. cache += itemId -> shopId
  8. shopId
  9. }
  10. }
  11. //调用这个函数
  12. cachedItemIdToShopId(itemId,itemIdToShopId(itemId))
  • 函数对mysql的依赖没有了
  • 不需要在测试和提交时切换代码
  • 貌似引入了新问题?

没错,cache根本没有起应有的作用,函数每次执行的时候都调用了itemIdToShopId从远程取数据

  1. //改成call by name就没有这个问题啦
  2. def cachedItemIdToShopId(itemId: Int, remoteShopId: =>Int): Int = {
  3. cache.get(itemId) match {
  4. case Some(shopId) => shopId
  5. case None =>
  6. val shopId = remoteShopId
  7. cache += itemId -> shopId
  8. shopId
  9. }
  10. }
  11. //调用这个函数
  12. cachedItemIdToShopId(itemId,itemIdToShopId(itemId))
  13.  
  • 函数对mysql的依赖没有了
  • 不需要在测试和提交时切换代码
  • 只在需要的时候查询远程库

Tuple/case class/模式匹配

Tuple为编程提供许多便利

  • 函数可以通过tuple返回多个值
  • tuple可以存储在容器类中,代替java bean
  • 可以一次为多个变量赋值

使用tuple的例子

  1. val (one, two) = (1, 2)
  2. one //res0: Int = 1
  3. two //res1: Int = 2
  4. defsellerAndItemId(orderId: Int): (Int, Int) =
  5. orderId match {
  6. case 0 => (1, 2)
  7. }
  8. val (sellerId, itemId) = sellerAndItemId(0)
  9. sellerId // sellerId: Int = 1
  10. itemId // itemId: Int = 2
  11. val sellerItem = sellerAndItemId(0)
  12. sellerItem._1 //res4: Int = 1
  13. sellerItem._2 //res5: Int = 2

用模式匹配增加tuple可读性

  1. val sampleList = List((1, 2, 3), (4, 5, 6), (7, 8, 9))
  2. sampleList.map(x => s"${x._1}_${x._2}_${x._3}")
  3. //res0: List[String] = List(1_2_3, 4_5_6, 7_8_9)
  4. sampleList.map {
  5. case (orderId, shopId, itemId) =>
  6. s"${orderId}_${shopId}_$itemId"
  7. }
  8. //res1: List[String] = List(1_2_3, 4_5_6, 7_8_9)

上下两个map做了同样的事情,但下一个map为tuple中的三个值都给了名字,增加了代码的可读性.

match和java和switch很像,但有区别

  1. match是表达式,会返回值
  2. match不需要”break”
  3. 如果没有任何符合要求的case,match会抛异常,因为是表达式
  4. match可以匹配任何东西,switch只能匹配数字或字符串常量
  1. //case如果是常量,就在值相等时匹配.
  2. //如果是变量,就匹配任何值.
  3. def describe(x: Any) = x match {
  4. case 5 => "five"
  5. case true => "truth"
  6. case "hello" => "hi!"
  7. case Nil => "the empty list"
  8. case somethingElse => "something else " + somethingElse
  9. }

case class,tuple以及列表都可以在匹配的同时捕获内部的内容.

  1. case class Sample(a:String,b:String,c:String,d:String,e:String)
  2. def showContent(x: Any) =
  3. x match {
  4. case Sample(a,b,c,d,e) =>
  5. s"Sample $a.$b.$c.$d.$e"
  6. case (a,b,c,d,e) =>
  7. s"tuple $a,$b,$c,$d,$e"
  8. case head::second::rest =>
  9. s"list head:$head second:$second rest:$rest"
  10. }

Case class

  1. 模式匹配过程中其实调用了类的unapply方法
  2. Case class 是为模式匹配(以及其他一些方面)提供了特别的便利的类
  3. Case class 还是普通的class,但是它自动为你实现了apply,unapply,toString等方法
  4. 其实tuple就是泛型的case class

用 option 代替 null

null 的问题

  1. Map<String, String> map = ???
  2. String valFor2014 = map.get(“1024”); // null
  3.  
  4. if (valFor1024 == null)
  5. abadon();
  6. else doSomething();
  • null到底代表key找不到还是说1024对应的值就是null?
  • 某年某月某日,我把为null则abandon这段代码写了100遍.

option介绍

  • option可以看作是一个容器,容器的size是1或0
  • Size为1的时候就是一个Some[A](x: A),size为0的时候就是一个None

看看scala的map

  1. defget(key: A): Option[B]
  2.  
  3. defgetOrElse[B1 >: B](key: A, default: => B1): B1 = get(key) match {
  4. case Some(v) => v
  5. case None => default
  6. }
  • 可以区分Map中到底又没有这个key.
  • 我见过许多java项目自己实现了getOrElse这个方法并放在一个叫做MapUtils的类里.
  • 为什么java经过这么多代演进,Map仍然没有默认包含这个方法,一直想不通.
    (写完这段突然发现java8开始包含getOrDefault了)

好像没有太大区别?

确实能够区分Map是无值还是值为null了.
但是if(为null) 则 abandon 要写一百遍.
case Some(v) => v
case None => default
似乎也得写一百遍.

不,不是这样的
不要忘了option是个容器
http://www.scala-lang.org/api/2.11.7/index.html#scala.Option

试试容器里的各种方法

  1. val a: Option[String] = Some("1024")
  2. val b: Option[String] = None
  3. a.map(_.toInt)
  4. //res0: Option[Int] = Some(1024)
  5. b.map(_.toInt)
  6. //res1: Option[Int] = None,不会甩exception
  7. a.filter(_ == "2048")
  8. //res2: Option[String] = None
  9. b.filter(_ == "2048")
  10. //res3: Option[String] = None
  11. a.getOrElse("2048")
  12. //res4: String = 1024
  13. b.getOrElse("2048")
  14. //res5: String = 2048
  15. a.map(_.toInt)
  16. .map(_ + 1)
  17. .map(_ / 5)
  18. .map(_ / 2 == 0) //res6: Option[Boolean] = Some(false)
  19. //如果是null,恐怕要一连check abandon四遍了

option配合其他容器使用

  1. val a: Seq[String] =
  2. Seq("1", "2", "3", null, "4")
  3. val b: Seq[Option[String]] =
  4. Seq(Some("1"), Some("2"), Some("3"), None, Some("4"))
  5.  
  6. a.filter(_ != null).map(_.toInt)
  7. //res0: Seq[Int] = List(1, 2, 3, 4)
  8. //如果你忘了检查,编译器是看不出来的,只能在跑崩的时候抛异常
  9. b.flatMap(_.map(_.toInt))
  10. //res1: Seq[Int] = List(1, 2, 3, 4)
  • option帮助你把错误扼杀在编译阶段
  • flatMap则可以在过滤空值的同时将option恢复为原始数据.

scala原生容器类都对option有良好支持

  1. Seq(1,2,3).headOption
  2. //res0: Option[Int] = Some(1)
  3.  
  4. Seq(1,2,3).find(_ == 5)
  5. //res1: Option[Int] = None
  6.  
  7. Seq(1,2,3).lastOption
  8. //res2: Option[Int] = Some(3)
  9.  
  10. Vector(1,2,3).reduceLeft(_ + _)
  11. //res3: Int = 6
  12.  
  13. Vector(1,2,3).reduceLeftOption(_ + _)
  14. //res4: Option[Int] = Some(6)
  15. //在vector为空的时候也能用
  16.  
  17. Seq("a", "b", "c", null, "d").map(Option(_))
  18. //res0: Seq[Option[String]] =
  19. // List(Some(a), Some(b), Some(c), None, Some(d))
  20. //原始数据转换成option也很方便
  21.  

用Try类保存异常

传统异常处理的局限性

  1. try {
  2. 1024 / 0
  3. } catch {
  4. case e: Throwable => e.printStackTrace()
  5. }

用try-catch的模式,异常必须在抛出的时候马上处理.
然而在分布式计算中,我们很可能希望将异常集中到一起处理,来避免需要到每台机器上单独看错误日志的窘态.

  1. val seq = Seq(0, 1, 2, 3, 4)
  2. //seq: Seq[Int] = List(0, 1, 2, 3, 4)
  3.  
  4. val seqTry = seq.map(x => Try {
  5. 20 / x
  6. })
  7. //seqTry: Seq[scala.util.Try[Int]] = List(Failure(java.lang.ArithmeticException: devide by zero),Success(20), Success(10), Success(6), Success(5))
  8.  
  9. val succSeq = seqTry.flatMap(_.toOption)
  10. //succSeq: Seq[Int] = List(20, 10, 6, 5) Try可以转换成Option
  11. val succSeq2 = seqTry.collect {
  12. case Success(x) => x
  13. }
  14. //succSeq2: Seq[Int] = List(20, 10, 6, 5) 和上一个是一样的
  15. val failSeq: Seq[Throwable] = seqTry.collect {
  16. case Failure(e) => e
  17. }
  18. //failSeq: Seq[Throwable] = List(java.lang.ArithmeticException: devide by zero)

Try实例可以序列化,并且在机器间传送.

函数是一等公民

一个需求

  • 假设我们需要检查许多的数字是否符合某一范围
  • 范围存储在外部系统中,并且可能随时更改
  • 数字范围像这样存储着”>= 3,< 7”

一个java版本

  1. List<String> params = new LinkedList<>();
  2. List<Integer> nums = new LinkedList<>();
  3. List<String> marks = new LinkedList<>();
  4.  
  5. publicJavaRangeMatcher(List<String> params){
  6. this.params = params;
  7. for (String param : params) {
  8. String[] markNum = param.split(" ");
  9. marks.add(markNum[0]);
  10. nums.add(Integer.parseInt(markNum[1]));
  11. }
  12. }
  13.  
  14. public boolean check(int input){
  15. for (int i = 0; i < marks.size(); i++) {
  16. int num = nums.get(i);
  17. String mark = marks.get(i);
  18. if (mark.equals(">") && input <= num) return false;
  19. if (mark.equals(">=") && input < num) return false;
  20. if (mark.equals("<") && input >= num) return false;
  21. if (mark.equals("<=") && input > num) return false;
  22. }
  23. return true;
  24. }
  25.  
  26. List<String> paramsList = new LinkedList<String>() {{
  27. add(“>= 3”);
  28. add(“< 7”);
  29. }};
  30. JavaRangeMatcher matcher = new JavaRangeMatcher(paramsList);
  31. int[] inputs = new int[]{1, 3, 5, 7, 9};
  32. for (int input : inputs) {
  33. System.out.println(matcher.check(input));
  34. }
  35. //给自己有限的时间,想想又没有性能优化的余地
  36. //我们一起来跑跑看

一个 scala 版本

  1. def exprToInt(expr: String): Int => Boolean = {
  2. val Array(mark, num, _*) = expr.split(" ")
  3. val numInt = num.toInt
  4. mark match {
  5. case "<" => numInt.>
  6. case ">" => numInt.<
  7. case ">=" => numInt.<=
  8. case "<=" => numInt.>=
  9. } //返回函数的函数
  10. }
  11.  
  12. case class RangeMatcher(range: Seq[String]) {
  13. val rangeFunc: Seq[(Int) => Boolean] = range.map(exprToInt)
  14.  
  15. def check(input: Int) = rangeFunc.forall(_(input))
  16. }
  17.  
  18. def main(args: Array[String]) {
  19. val requirements = Seq(">= 3", "< 7")
  20. val rangeMatcher = RangeMatcher(requirements)
  21. val results = Seq(1, 3, 5, 7, 9).map(rangeMatcher.check)
  22. println(results.mkString(","))
  23. //false,true,true,false,false
  24. }
  25.  

关于性能

这里有一个性能测试网站

我对于网站测试的结果,我总结的情况就是两点.
1. 排在后面的基本都是动态类型语言,静态类型语言相对容易优化到性能差不多的结果.
2. 同一个语言代码写得好差产生的性能差异,远远比各种语言最好的代码性能差异大.

总的来说,程序员越自由,程序性能就越差

不过也有反例,我们之前那个程序就是.

  1. //java版本
  2. publicstaticvoidmain(String[] args){
  3. List<String> paramsList = new LinkedList<String>() {{
  4. add(">= 3");
  5. add("< 7");
  6. }};
  7. JavaRangeMatcher matcher = new JavaRangeMatcher(paramsList);
  8. Random random = new Random();
  9. long timeBegin = System.currentTimeMillis();
  10. for (int i = 0; i < 100000000; i++) {
  11. int input = random.nextInt() % 10;
  12. matcher.check(input);
  13. }
  14. long timeEnd = System.currentTimeMillis();
  15. System.out.println("java 消耗时间: " + (timeEnd - timeBegin) + " 毫秒");
  16. //java 消耗时间: 3263 毫秒
  17. }
 
收藏 推荐 打印 | 阅读: