为Play初学者准备的Scala基础知识

欢迎来访PlayScala社区(http://www.playscala.cn/)

1 前言

本文的主要目的是为了让Play Framework的初学者快速了解Scala语言,算是一篇Play Framework的入门前传吧。
使用PlayFramework可以极大的提高开发效率,但是需要注意,PlayJava入门很简单,我之前带过一个实习小姑娘,有一点编程经验,但从来没有接触过PlayJava,然而一周入门,一个月独立完成项目。但是PlayScala没那么简单,虽然后者的开发效率更高,但是由于Scala程序员匮乏,PlayScala只适合团队较小(10人以下)并且较稳定的情况下使用。其实有很多人怀疑,Scala到底能提高多少开发效率,这里有一行Scala代码,大家可以先体会一下:

Source.fromFile("D:/f.txt", "UTF-8").getLines().toList.distinct.sortBy(s => (s.charAt(0), s.length)).foreach( println _)

虽然只有一行代码,但是却做了很多事情:以UTF-8编码读取文件所有行 -> 去重 -> 按首字符排序,首字符相同按长度排序 -> 打印结果。各位脑补一下Java的实现。更多的一行代码请查看酷炫的一行代码 - Scala就是这么任性!。下面我们进入正题,先看Scala语言简介。

2 Scala简介

提到编程语言,大家的第一反应通常是面向对象编程(OOP), 然而随着硬件服务器CPU核数和个数越来越多,函数式编程(FP)语言又重新回到了人们的视线。两种编程语言都各有特点,面向对象编程符合人类对世界的认知,更容易理解;函数式编程的语法更接近人类语言,简洁高效。两种语言都让人无法取舍。而Scala将这两种编程语言完美的融合到一起,形成一门更加强大的JVM语言,同时Scala修正了Java很多不合理的设计,新增了更多高级特性,学习Scala的同时也是对Java的一次深度回顾,让你对编程语言的理解更加地深刻。与Java相比,Scala的设计更加一致:

  • 一切都是对象
1.toDouble  //可以直接调用基本类型上的方法
"1".toInt //将字符串转换成整型
  • 一切都是方法
"a" * 3 //等价于: "a".*(3)
2 - 1   //等价于: 2.-(1)
  • 一切都是表达式
val i = if(true){ 1 } else { 0 } // i = 1

Scala拥有一套强大的类型推导系统,所以你可以像动态类型语言那样编码,降低代码冗余度,减少无意义击键次数同时,代码也显得更加清晰易懂。
另外Java和Scala对待程序员的态度也很有意思,这里只是开个玩笑,大家别太在意。Java认为他所面对的程序员是一帮小白,容易犯错误,所以想方设法的限制你,避免你犯错;而Scala则认为他所面对的程序员是一帮天才,所以尽可能的向他敞开编程语言宝库,给他更大的自由度去想象和创作。

3 基本语法规则

3.1 变量声明

val用于定义不可变变量,var用于定义可变变量,这里的"可变"指的是引用的可变性。val定义的变量类似于Java的final变量,即变量只能赋一次值:

val msg = "hello" // 等价于:val msg: String = "hello"
var i = 1         // 等价于:var   i: Int = 1 
i = i + 1

变量后面的类型声明可以省略,每行代码末尾的分号";"也可以省略。

3.2 函数声明

def用于定义函数:

def max(x: Int, y: Int): Int = {
    if (x > y) { x } else { y }
}
val maxVal = max(1, 2) // 2

Scala是函数式语言,所以你可以像基本类型那样把函数赋给一个变量:

val max = (x: Int, y: Int) => {
    if (x > y) { x } else { y }
}
val maxVal = max(1, 2) // 2

等号"="右边是一个匿名函数,也就是我们常说的Lambda函数,匿名函数由参数和函数体两部分组成,中间用"=>"隔开,这里省略了max变量的类型,因为编译器可以自动推断出来,完整的写法如下:

val max: (Int, Int) => Int = (x: Int, y: Int) => {
    if (x > y) { x } else { y }
}

max的类型是(Int, Int) => Int,即接受两个Int参数,产生一个Int返回值的函数类型。

3.3 class

Scala的class定义和Java很相似:

class Counter {
    private var value = 0 //你必须初始化字段
    def increment() { value += 1} //方法默认public
    def current() = value 
}

Scala的源文件中可以定义多个类,并且默认都是public,所以外界都可以看见。class的使用也很简单:

val myCounter = new Counter //或new Counter()
myCounter.increment()
println(myCounter.current)  //或myCounter.current()

Scala中如果对象方法或类的构造器没有参数,则括号"()"可以省略。

3.4 object

Scala没有静态方法和静态字段,而是提供了object对象,也就是Java中的单例对象,即全局只有一个实例。

object Accounts {
    private var lastNumber = 0
    def newUniqueNumber() = { lastNumber += 1; lastNumber }
}

因为Accounts是一个单例对象,可以直接使用而无需初始化:

val uniqueNumber = Accounts.newUniqueNumber

object的另一个用法是作为类的伴生对象, 类似于Java类上的静态方法,只不过Scala将Java类上的静态功能全交给object实现了。object作为伴生对象时必须和类在同一个源文件中定义,并且可以相互访问私有属性。

3.5 apply方法

如果某个对象obj上定义了apply方法,则我们可以这样调用:

obj(arg1, ... , argn)

是的,你猜对了,伴生对象上的apply方法立马就派上用场了,例如List类有一个同名的伴生对象List,那么你可以这样初始化一个列表:

val list = List("a", "b", "c")

想想下面的Java版本,是不是感觉幸福感油然而生:

List list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");

3.6 块表达式

在Scala中一切都是表达式,如果表达式含有多条语句,则使用大括号"{}"括起来,形成一个块表达式,块表达式的最后一条语句的值作为整个块的返回值。

val r = {
    val i = 1
    val j = 2
    i + j
} // r = 3

4 case class和模式匹配

在Scala中接触到新概念不要害怕,了解之后你会发现它帮你解决了很多实际问题,就如我们这里要聊的case class和模式匹配。定义一个case class的代码如下:

case class Currency(value: Double, unit: String)

当你定义了一个case class之后,编译器会自动帮你做如下事情:

  • 自动创建伴生对象
  • 为该类添加toString,hashCode和euqals方法,用于模式匹配时的结构化比较
  • 为该类添加copy方法,用于快速拷贝对象

好了,下面我们来看一下模式匹配的威力:

abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
val amount = Currency(100.0, "EUR")
val amountStr = 
    amount match {
        case Dollar(v) => "$" + v
        case Currency(v, u) => "I got " + v + u
        case _ => ""
    }

在Scala中,类、函数、方法和object可以像变量一样在任何地方定义。

Scala中默认使用的类都是不可变的,所以如果你想改变value的值需要借助copy方法:

val newAmound = amount.copy(value = 1000.0)

Scala中的模式匹配还可以实现更复杂的匹配,详见"Programming in Scala, 3nd Edition"。如果说Java中的switch是一把手枪,那么Scala中的模式匹配是一架当之无愧的战头机。

5 map和flatMap

可能有很多人就是因为这两个方法才迷恋上Scala的。map和flatMap是两个高阶函数,所谓高阶函数就是接受函数作为参数的函数。这两个方法各自接受一个一元函数(即只有一个参数的函数,类型为:(A) => B),利用这个一元函数,你可以对数据流中的每一个元素进行一些操作或转换,最终得到一个全新的数据流。
map方法接受的一元函数类型为:(A) => B:

List(1, 2, 3).map((i: Int) => { i + 1 }) // List(2, 3, 4)

也可以简写如下两种形式:

List(1, 2, 3).map(i => i + 1 )
List(1, 2, 3).map(_ + 1 ) 

你可以把第2种形式中的下划线理解成每个元素的占位符,其实这只是编译器的语法糖,编译后的结果和前两种写法相同。使用这个语法糖的前提是下划线"_"在函数体内只能出现一次。

在上面的例子里,map方法接受的一元函数类型是:(Int) => Int,元素的类型没有发生改变,我们可以尝试改变元素类型:

List(1, 2, 3).map(i => i.toString * i) // List(1, 22, 333)

这次传入的一元函数类型是: (Int) => String,将原List从List[Int]类型转换成了List[String]类型,完成一次数据流类型转换。

flatMap方法接受的一元函数类型为:(A) => List[B],我们发现该一元函数返回的类型也是一个List,flatMap方法会自动将由每个元素A转换成的小List[B]展平成一个大的List[B],这也是flatMap中的"flat"所要表达的意思:

List(1, 2, 3).flatMap(i => List(i, i)) // List(1, 1, 2, 2, 3, 3)

这里我们只在List上演示了map和flatMap的基本用法,Scala中所有的容器类型(例如Option, Either, Future, Set, ...)都内置了这两个方法。除了map和flatMap,Scala的容器类型上还有很多类似的方法,例如filter, find, sortBy等等,详见"Programming in Scala, 3nd Edition"。

6 常用类介绍

6.1 String

在Scala中,String更加方便好用:

//原始字符串一对三引号`"""`括起来,可包含多行字符串,内容不需要转义
"""Welcome here.
   Type "HELP" for help!"""
   
//类型转换
"100.0".toDouble

//判断字符串相等直接用"==",而不需要使用equals方法
val s1 = new String("a")
s1 == "a" // true

//字符串去重
"aabbcc".distinct // "abc"

//取前n个字符,如果n大于字符串长度返回原字符串
"abcd".take(10) // "abcd"

//字符串排序
"bcad".sorted // "abcd"

//过滤特定字符
"bcad".filter(_ != 'a') // "bcd"

//字符串插值, 以s开头的字符串内部可以直接插入变量,方便字符串构造
val i = 100
s"i=${i}" // "i=100"

Scala中没有受检异常(checked exception),所以你没有必要声明受检异常,如果真的发生异常,则会在运行时抛出。

6.2 Option

Scala用Option类型表示一个值是否存在,用来避免Java的NullPointerException。它有两个子类:Some和None。Some类型表示值存在,None类型则表示值不存在。
常用操作:

val opt: Option[String] = Some("hello")
//判断是否为None
opt.isEmpty // false
//如果为None,则返回默认值"default",否则返回opt持有的值
opt.getOrElse("default") 
//如果为None则返回"DEFAULT",否则将字符转为大写
opt.fold("DEFAULT"){ value => value.toUpperCase } // "HELLO"
//功能同上
opt match {
  case Some(v) => v.toUpperCase
  case None => "DEFAULT"
}

6.3 List

在Scala中,List要么是Nil(空列表),要么就是由head和tail组成的递归结构。 head是首元素,tail是剩下的List。所以你可以这样构建List:

val list = 1 :: Nil // 等价于:val list = List(1)

连续的两个冒号"::"就像是胶水,将List的head和tail粘在一起。
常用操作:

val list = List(1, 3, 2)
//获取第1个元素
list.headOption.getOrElse(0) // 1
//查找
list.find(_ % 2 == 0).getOrElse(0) // 2
//过滤
list.filter(_ % 2 == 1) // List(1, 3)
//排序
list.sorted // List(1, 2, 3)
//最小值/最大值/求和
list.min // 1
list.max // 3
list.sum // 6
//转化成字符串
list.mkString(",") // "1, 3, 2"

Scala提供的List基本可以实现SQL查询的所有功能,这也是Spark为什么基于Scala开发的原因。更多功能请参考官方文档。

在Scala中默认的集合类例如List,Set,Map,Tuple等都是不可变的,所以调用其修改方法会返回一个新的实例。如果要使用可变集合,请使用scala.collection.mutable包下相应的类。不可变类型在编写并发代码时很有用。

6.4 Tuple

Tuple(元组)Tuple可以容纳不同类型的元素,最简单的形态是二元组,即由两个元素构成的Tuple, 可以使用_1, _2等方法访问其元素:

val t = ("a", 1) // 等价于:val t: Tuple2[String, Int] = ("a", 1)
t._1 // "a"
t._2 // 1

也可以使用模式匹配利用Tuple同时初始化一组变量:

val t = ("a", 1)
val (v1, v2) = t
v1 // "a"
v2 // 1

6.5 Map

Map其实是二元组的集合:

val map = Map("a" -> 1, "b" -> 2)

"->"其实是String类型上的方法,返回一个二元组:

"a" -> 1 //等价于: ("a", 1)

所以你也可以这样构建Map:

val map = Map(("a", 1), ("b", 2))

常用操作:

val map = Map("a" -> 1, "b" -> 2)
//读取
map("a") // 1
//写入或添加键值
map("a") = 0
//删除键值
map - "a" // Map(b -> 2)

7 控制结构

7.1 if

if语句同样是表达式,拥有返回值:

val i = 1
val r = if(i > 0){ 1 } else { 0 } // r = 1

7.2 for

Scala中for语句功能比Java要丰富很多,你可以使用for遍历一个List:

val list = List(1, 2, 3)
for(i <- list){
    println(i)
}

你也可以使用模式匹配遍历一个Map:

val map = Map(("a", 1), ("b", 2))
for((k, v) <- map){
  println(k + ": " + v)
}

如果循环体以yield开始,for语句会返回一个新的集合:

val newList1 = for(i <- List(1, 2, 3)) yield i * 2 // List(2, 4, 6)
val newList2 =
    for{
       i <- List(1, 2)
       j <- List(3, 4)
    } yield i + j //List(4, 5, 5, 6)

如果有多个集合需要遍历,则for语句后面的圆括号"()"要换成大括号"{}"。

8 Future和Promise

Future和Promise是Scala提供的最吸引人的特性之一,借助Future和Promise你可以轻松地编写完全异步非阻塞的代码,这在多处理器时代显得格外重要。

8.1 Future

Future用于获取异步任务的返回结果。Future有两种状态:完成(completed)和未完成(not completed)。处于完成状态的Future可能包含两种情况的信息,一种是异步任务执行成功了,Future中包含异步任务执行成功的返回结果;另一种是异步任务执行失败了,Future中包含了相应的Exception信息。Future的独特之处在于它的值只能被写入一次,之后就会变为一个不可变值,其中包含成功或失败信息。你可以在Future上注册一个回调函数,以便在任务执行完成后得到通知:

import scala.concurrent.ExecutionContext.Implicits.global
val f = Future{ 1 + 2 }
f.onComplete{ t =>
  t match{
    case Success(v) => println("success: " + v)
    case Failure(t) => println("failed: " + t.getMessage)
  }
}
//等待任务结束
Await.ready(f, 10 seconds)

onComplete方法接受一个一元函数,类型为:Try[T] => U。Try类型和Option类型很像,也有两个子类SuccessFailure,前者表示任务执行成功,后者表示任务执行失败。

第1行import语句导入了一个隐式的ExecutionContext,你可以把它理解成是一个线程池,Future类在需要时会自动使用其上的线程。在Scala中你不需要直接和线程打交道。

由于Future也是一个容器类,所以可以使用for语句取回它的值:

val f = Future{ 1 + 2 }
for(v <- f) {
    println(v) // 3
}

也可以使用map方法对任务结果进行转换:

val f1 = Future{ 1 + 2 }
val f2 = f1.map(v => v % 2)
for(v <- f2) {
    println(v) // 1
}

利用for语句可以等待多个Future的返回结果:

val f1 = Future{ 1 + 2 }
val f2 = Future{ 3 + 4 }
for{
    v1 <- f1
    v2 <- f2
} {
    println(v1 + v2) // 10
}

结合yield可以返回一个新的Future:

val f1 = Future{ 1 + 2 }
val f2 = Future{ 3 + 4 }
val f3 =  
    for{
        v1 <- f1
        v2 <- f2
    } yield {
        v1 + v2
    }

8.2 Promise

有时我们需要精细地控制Future的完成时机和返回结果,也就是说我们需要一个控制Future的开关,没错,这个开关就是Promise。每个Promise实例都会有一个唯一的Future与之相关联:

val p = Promise[Int]()
val f = p.future
for(v <- f) { println(v) }

//3秒钟之后返回3
Thread.sleep(3000)
p.success(3)

//等待任务结束
Await.ready(f, 10 seconds)

9 小结

Scala在刚入门的时候确实有点难度,各种奇怪的语法、符号漫天飞,看的云里雾里。但是在你入门之后会发现,这些奇怪的地方其实是合理的,是一种有意的设计。例如允许方法名包含特殊符号,你可以写出下面的代码:

"a" * 3 // "aaa"
val map = Map("a" -> 1, "b" -> 2)

"*"和"->"其实是字符串上的两个方法,允许符号作为方法名使得代码直观易懂。由于Scala赋予程序员对代码很高的控制力,如果滥用就会导致天书般的代码,这需要团队内部进行协调,控制代码的复杂度。Scala之父Martin Odersky也曾经表示会在2016简化Scala语言,降低初学者的门槛。到时会有更多的人加入这个社区,一起分享编程的乐趣。

10 参考

  • "Programming in Scala, 3nd Edition"
  • "快学Scala"

11 附录

11.1 开发工具推荐

IntelliJ IDEA + Scala插件

11.2 转载声明

转载请注明作者joymufeng

你可能感兴趣的:(为Play初学者准备的Scala基础知识)