Scala 入门介绍
1 基础
1.1 Scala 解释器
REPL — 交互式解释器环境
R(read)、E(evaluate)、P(print)、L(loop)
输入值,交互式解释器会读取输入内容并对它求值,再返回结果,并重复此过程。
1.2 声明 val(值) 和 var(变量)
val: 定义值(常量), 即不可改变引用的指向(不可对其进行赋值操作), 指向的对象能否可变取决于变量自身类型
var: 定义变量, 可以改变引用的指向
- 声明值或者变量的同时, 必须初始化;
- 可以不指定类型, Scala可以利用初始化值进行类型推断
- Scala中, 仅当同一行代码存在多条语句才需要用分号隔开
- 可以同时将多个值或者变量放在一起声明 val x, y = 10
1.3 数值类型
与 Java 一样 Scala也有8中数值类型:
Byte, Char, Short, Int, Long, Float , Double Boolean
这些数值类型都是类, 并且 Scala 通过隐式转换为这些类型提供了很多常用方法. 比如说 Scala 提供了 RichInt, RichChar, RichDouble 等, 分别为 Int, Char, Double 提供其所不具备的方法
比如说表达式 1.to(10), Int 值1首先被隐式转换为 RichInt 然后应用 RichInt 类的方法.
其他数值类型还有:
- BigInt 背后为 java.math.BigInteger
- Decimal 背后为 java.math.BIgDecimal
在 Scala 中, 我们使用方法而不是强制类型转换来做数值类型之间的转换 比如说 1.4.toInt得到 1, 99.toChar 得到 c
1.4 操作符
在 Scala 中, 操作符有一点特别, 操作符实际上都是方法; 说的更加激进点, 就是 Scala 中没有操作符, 一切都是方法调用, 不过为了表述方便, 我们还是称其为操作符, 不过心里要明白它们都是方法.
比说表达式 a + b 就是方法调用 a.+(b) 的简写
Scala的标识符命名极其灵活, 不会对方法名中出现非字母, 数字,下划线的做法有偏见, 你几乎可以使用任何符号为方法命名
下面来看看 Scala 一个很有用的技巧, 这会让我们的编程风格更加 Scala 化
a.方法(b) 可以简写为 a 方法 b
这里方法是一个带有两个参数的方法(一个隐式的和一个显示的)
+ - * / %
& | ^ >> <<
Scala 没有提供 ++
和 --
操作符, 使用 +=1 和 -= 1 就可以. 为何没有提供呢? 因为 Scala Int类是不可变的, 这样我们没法通过一个简单的方法实现 ++ . Scala的设计者认为不值得为了少按一个按键而额外增加一个特例
1.5 函数和方法
在介绍 Scala 的函数和方法之前先来简单看一下方法和函数有什么区别吧, 简单的说就是 方法和对象相关;函数和对象无关。
除了方法外, Scala 还提供了函数( Java 只有方法), 相比 Java, 在 Scala 中使用数学函数更加简单, 你不需要从某个类中调用它的静态方法.
Scala 的数学函数都在 Scala.math 包中定义. 通过下面语句引入
import scala.math._ 或者 import math._
sqrt(2)
pow(2, 3) // 8
min(3, Pi) // 3
Scala 中没有静态方法, 不过它有个类似的特性, 叫做单例对象, 通常一个类对应一个伴生对象, 伴生对象中定义的方法和 Java 中的静态方法一样
不带参数的 Scala 方法通常可以不使用圆括号
记住上面说的是方法而不是对象 😃
1.6 apply方法
当对一个对象使用类似方法调用的语法时, 就会触发 apply 方法, 比如 "Good"(2) 实际上就是方法调用 "Good".apply(2); 你可以把这种用法当做 () 操作符的重载
1.7 Scaladoc
Javadoc 按照字母顺序列出类的清单, 而Scaladoc 按照包(package)列出类的清单
// 前缀操作符 unary_-, 即负操作符 -x
scala> BigInt(1).unary_-
res24: scala.math.BigInt = -1
"Harry".patch(1, "ung", 2) // hungry
上面的图片, C 表示这是一个类, 我们可以看到图片上半圆和下半圆颜色不一样, 表示该类有伴生对象, 点击这个图标就能切换到伴生对象界面了.
点击 Source, 可以跳转到 Github 上的 Scala源码库去
2. 控制结构和函数
在介绍 Scala 中的条件表达式, 循环和函数之前, 先来看一组概念, 在 Java中, 表达式(如 3+4)和语句(如 if 语句) 是不同的东西, 那区别是什么呢? 表达式求值并返回值, 而语句执行动作, 但是不返回值; 在 Scala 中几乎所有构造出来的语法结构都有值, 比如 for循环语句也有返回值 Unit
scala> val l = for(i <- 1 to 2){i}
l: Unit = ()
介绍内容如下:
- if 表达式有值
- void 类型是 Unit
- for 循环返回 Unit
- 分号不是必须的(绝大多数情况下)
- 避免在函数中使用 return
- 函数定义一定不要漏掉了等于号
=
- Scala没有受检异常, 常使用模式匹配处理异常
2.1 条件表达式
在Scala中, if/else 是表达式, 所以有返回值, 又因为 Scala 中每个表达式都有一个类型, 所以如果 if /else 与句的两个分支类型相同的话, 表达式就是该类型, 如果两个分支类型不同的话, 返回值就是来个分支类型的公共超类型, 当然这种情况用 case 语句来处理是个更好的选择.
if/else 表达式如果 else 部分缺失了, 那么有可能表达式没有输出值, 但是 Scala 中所有表达式都应该有某种返回值, 解决方法就是引入 Unit 类, 写作 (). 不带 else 的 if 表达式等同于 if(x>0) 1 else ()
scala> val s1 = if (1 > 0) 1 else -1
s1: Int = 1
scala> val s2 = if (1 < 0) 1 else "H"
s2: Any = H
scala> val s3 = if (1 < 0) 1
s3: AnyVal = ()
Unit 可以看成 Java 中的 void, 该类型只有一个值, 技术上说, void 没有值而 Unit 有一个表示无值的值
2.2 语句终止
Scala中, 行尾的位置分号㔻必须的, 只要能从上下文判断出这里是语句的终止即可. 不过如果你想在单行写多个语句, 就需要将它们用分号分开
多行时, 语句终止编码风格:
- 同行左大括号
- 使用空悬操作符或者括号
空悬操作符: 一行代码的最后一个非空字符是一个操作符
2.3 块表达式与赋值
在 Scala 中, { } 块包含一系列值表达式, 其结果也是表达式. 块中最后一个表达式的值就是块的值, 即 {} 块的值取决于最后一个表达式
Scala中, 赋值动作本身没有值, 严格的说, 它们的值是 Unit 类型, 比如块 {r= r*n; n -= 1} 的值为 (). 由于赋值语句是 Unit 类型, 所以别将赋值语句串接起来 , 比如 想 x = y =1, 由于Scala 中大多都用的 val, val 定义的值没法被赋值, 所以基本上不会出现这个错误, 不过还是要注意, 不要把 Java 的习惯全盘带到 Scala 中.
最后关于块 {} 在多说一点, 当你使用一个块 {} 的时候, 你就创建了一个作用域, 而作用域是可以嵌套的, 外层作用域的实体(值, 变量, 对象等) 在内层作用域是可见的.
2.4 输入与输出
输出到控制台:
println("Hi " + "Jim")
printf("Hi, %s\n", "Jim")
从控制台读取输入:
scala> import scala.io.StdIn._
import scala.io.StdIn._
scala> read
readBoolean readFloat readShort readf3
readByte readInt readf
readChar readLine readf1
readDouble readLong readf2
// 与其他方法不同, readLine 可以带有一个参数作为提示字符串
scala> readLine("input>") // 输入 dog
input>res0: String = dog
// readf()可以接收任意数量的值,返回值为List[Any]类型
scala> val list = readf("{0}") // 输入 1
list: List[Any] = List(1)
//readf1()仅能接收一个值,返回接收的值
scala> val num = readf1("is {0}") // 输入 is me
num: Any = me
// readf2() , readf3() 分别接收两个, 三个值,返回值为 Tuple 类型
scala> val tuple = readf3("{0} + {1} + {2}") // 输入 c + a + t
tuple: (Any, Any, Any) = (c,a,t)
2.5 循环
在介绍循环前, 先介绍下 to 和 until 方法
1 to n: 返回 1 到 n 的区间, 包含 n
1 until n: 返回 1 到 n 的区间, 不包含 n
while循环:
while(循环变量) {
statements
}
一般在 Scala 中不会用 while 循环
for循环:
for (i<- 表达式) {
statements
}
让变量 i 遍历 表达式的所有值, 至于遍历具体如何执行取决于表达式的类型, 返回值为 Unit
Scala 中没有提供 break 和 continue 语句来退出循环, 如果需要 break, 可以这样做:
- 使用 Boolean 型的控制变量
- 使用 scala.util.control.Breaks 对象的 break 方法
- 函数中可以把 return 当做 break 使用
2.6 高级for循环和for推导式
可以是用 变量 <- 表达式
的形式提供多个生成器, 用分号将它们隔开; 每个生成器都可以带一个守卫, 以 if 开头的 Boolean 表达式; Scala中 有这样一个编程风格, 如果 () 中有多个表达式的话, 可以使用 {} 代替 () 来写一个块表达式
for {
i <- 1 to 3 if (i != 2) //注意 if 前也没有分号
j <- 1 to 3 if (i != j)
} {
print (i * 10 + j + " ")
}
12 13 31 32
从上面可以看出, 可以将生成器, 守卫(和定义) 包含在大括号中, 可以以换行的方式来隔开它们;
如果 for 循环的循环体以 yield 开始, 则该循环会构造出一个集合, 每次迭代生成集合中的一个, 这类循环叫做 for 推导式
for 推导式 生成的集合和它的第一个生成器是类型兼容的.
scala> for (c <- "Hi"; i <- 0 to 1) yield (c+i).toChar
res5: String = HIij
scala> for (i <- 0 to 1; c <- "Hi" ) yield (c+i).toChar
res6: scala.collection.immutable.IndexedSeq[Char] = Vector(H, i, I, j)
2.7 函数
Scala 除了方法之外还支持函数
定义函数需要给出函数名, 函数, 函数体; 你必须给出所有参数的类型, 只要函数不是递归的, 就不需要指定返回类型. Scala 可以通过等于号 = 右侧的表达式类型自动推断返回值类型.
// 递归必须制定返回值类型
def fac(n: Int): Int = if (n <= 0) 1 else n * fac(n -1)
如果函数体需要过个表达式完成, 使用代码块 { }, 块的最后一个表达式就是函数的返回值. 比如说:
def fac(n: Int) = {
var r = 1
for (i <- 1 to n) r = r * i
i
}
Scala 中一般不使用 return 来返回函数返回值, 一般讲 return当做 函数版的 break 语句
过程
Scala对于不返回值的函数有特殊的表示方法. 如果函数体包含在花括号当中但没有前面的等于号 = , 那么返回值类型就是 Unit, 这样的函数被称为 过程
. 过程不返回值, 我们调用仅仅为了它的副作用.
默认参数和带名参数
带有默认参数的函数, 在调用函数时, 我们可以不显式地给出所有的参数值
def foo (str: String, l: String = "(", r: String = ")") = l + str + r
调用 foo("smile"), 得到 (smile).
如果你给出的值不够, 给出的值从前往后依次匹配参数(没有带名参数的话), 对于还没有匹配到值的参数使用默认参数.
foo("smile", ":(") // :(smile)
函数调用 在提供参数值的时候指定带名参数, 正常参数按照位置匹配, 而带名参数不需要和参数列表的顺序完全一致, 来看一个例子
foo("smile", r = ":)") //(smile:)
变长参数
先来看一下变长参数的语法吧
def sum(plus: Int*) = {
var res = 0
for (p <- plus) res+= p
res
}
可以使用任意多的参数来调用该函数, 函数得到类型是 Seq 的参数. 但是如果传入单个参数, 参数必须是 Int 型, 而不可以是一个 Int 型集合要实现这个做法,你需要在数组参数后添加 : _*
, 这是编译器的一个 annotation(标记), 当编译器遇到这个标记时会把集合中的每个元素当作参数传递给 sum. 这个标记只在解决变长参数传递问题
sum(1) // 1
sum(1, 2) // 3
sum(1, 2, 3) // 6
sum(Seq(1, 2), _*) //3
sum(List(1, 2): _*) //3
2.8 懒值
当 val 被声明为 lazy, 它的初始化将被延迟, 直到我们首次对它求值
lazy val words = scala.io.Source.fromFile("/home/nowgood/happy.txt")
如果程序从不访问 words, 那么文件也不会被打开.
你可以吧懒值当做是介于 val 和 def 的中间状态
// words 被定义时即被取值
val words = scala.io.Source.fromFile("/home/nowgood/happy.txt")
// words 被首次使用时取值
lazy val words = scala.io.Source.fromFile("/home/nowgood/happy.txt")
// 每一次 words 被使用时取值
def words = scala.io.Source.fromFile("/home/nowgood/happy.txt")
懒值不是没有额外开销, 我们每次访问懒值, 都会有一个方法被调用, 这个方法以线程安全的方式检查值是否已被初始化
2.9 异常
Scala 异常的工作机制和 Java一样
throw new IllegalArgumentException(" have a exception")
和 Java一样, 抛出的对象必须是 java.lang.Throwable 的子类. 不过, 与 Java 不同的是, Scala没有受检异常, 即你不需要声明说函数或者方法可能会抛出某种异常
Throwable表达式类型为 Nothing, Nothing是 Scala 中一切其他类型的子类
也可以使用 try/catch/finally 语句:
try { ... } catch { ... } finally { ... }