Scala 重点笔记
Scala 重点笔记
2024年8月7日
符号标志
和其他语言只能用字母数字作为标志符不同,Scala 还支持符号标志符,例如 :=
,::::
之类的,至少两个符号组成,可以用来创造运算符。
特殊类型
Scala 有一些特殊类型:
val optionValue: Option[String] = Some("I am here")
val eitherValue: Either[String, Int] = Right(42)
val tryValue: Try[Int] = Try(10 / 2)
val unitValue: Unit = ()
val nullValue: String = null
val nothingValue: Nothing = throw new RuntimeException("Nothing value")
空了可以再研究一下。
基本类型隐式 Builder
Scala 有一些隐式的 Builder,例如 IntBuilder
,可以在基本类型调用别的函数时,把基本类型隐式转换成对应的 Builder 对象,例如:
128.bits
等同于:
IntToBuilder(128).bits
可以将 128
转换为 BitCount
对象,内部实现如下:
implicit def IntToBuilder(value: Int): IntBuilder = new IntBuilder(value)
class IntBuilder(val i: Int) extends AnyVal {
def bits = new BitCount(i)
}
循环与生成器
For 循环
基本
for( var x <- Range ) {
statement(s);
}
闭区间
for( a <- 1 to 10) {
println( "Value of a: " + a );
}
左闭右开
for( a <- 1 until 10) {
println( "Value of a: " + a );
}
循环过滤
for( var x <- List
if condition1; if condition2...
) {
statement(s);
}
生成器:
var retVal = for{ var x <- List
if condition1; if condition2...
} yield x
方法相关
返回值
可以使用 return
提前返回,否则按照函数式的规则,最后一个语句会作为返回值。
方法调用方式
点和空格
使用对象调用方法时,常规调用方式是:
object.method(parameter)
也可以使用空格调用:
object method parameter
但是,有且仅有对象能够通过空格调用方法,单纯的 method parameter
是错误的语法,只能 method(parameter)
无参数和空参数
对于使用 def
定义的函数,如果不需要接收参数,则有无参数(parameterless)和空参数(empty-paran)。
空参数
如果按照空参数定义:
def print_line(): Unit = {
println("-----")
}
则调用时,可以使用:
print_line()
print_line
但是最好带上括号,否则容易混淆。
无参数
如果按照无参数定义,例如:
def date: String = {
// Get current date string
val date = java.time.LocalDate.now()
date.toString
}
则调用时只能:
println(date)
加括号就会报错。
使用场景
一般来讲,空参数用来执行某个任务,而无参数用来获取某个属性,类似于 getter
这个概念。
def,匿名函数与表达式
def
def
一般来讲,就是一个最通俗,最常规的函数定义,和其他语言无异,例如:
def adder(a: Int, b: Int): Int = {
a + b
}
对于有参数的 def
,我们不能直接访问函数名,会直接报错,例如:
println(adder)
如果某个高阶函数明确接收一个函数作为输入参数,则可以将函数名传入。例如:
def calculator(a: Int, b: Int, operation: (Int, Int) => Int): Int = {
operation(a, b)
}
println(calculator(2, 3, function))
此外,如过一个函数定义没有 =
,则没有返回值,或者说返回值是 Unit
:
def no_return() {
1
}
以上函数并不会返回 1
。
匿名函数
val
定义函数时,类似于定义了一个匿名函数(Lambda),然后将这个匿名函数取了一个名字,使这个变量存储了一个 Lambda 表达式:
val adder: (Int, Int) => Int = (a: Int, b: Int) => {
a + b
}
访问时,可以直接访问函数名,例如:
println(adder)
// Examples$$$Lambda$16/0x0000000800091040@727803de
同样,也可以作为高阶函数输入。
val
定义的函数,其名称是一个对象,因此可以直接访问名称,而def
定义的是一个过程块,不是一个对象。
表达式
def
和 val
还能用于创建表达式,例如:
def dice = { new scala.util.Random().nextInt(6) + 1 }
val once = { new scala.util.Random().nextInt(6) + 1 }
println(dice + " <-> " + once) // 5 <-> 6
println(dice + " <-> " + once) // 4 <-> 6
println(dice + " <-> " + once) // 3 <-> 6
println(dice + " <-> " + once) // 1 <-> 6
val
表达式和无参数的 def
区别在于:
- 每次调用
def
表达式(无参数def
),都是调用后面的代码块,会重新计算 - 无参数的
val
表达式会在第一次被调用时计算值并缓存,后续直接使用
⚠️ 注意!
def dice = { new scala.util.Random().nextInt(6) + 1 } def dice = () => { new scala.util.Random().nextInt(6) + 1 }
两种方法有本质区别,第一个是一个表达式,第二个是空参数函数,调用时要:
println(dice())
传名调用及意义
对于通过 def
定义的,带返回值的函数,在作为参数被其他函数调用时,有两种传递方式:按值传递和按名传递。
按值传递
常规的按值传递如下:
def something(): Int = {
println("calling something")
1 // return value
}
def callByValue(x: Int) = {
println("x1=" + x)
println("x2=" + x)
}
callByValue(something())
// calling something
// x1=1
// x2=1
在传参时,something()
就已经完成调用并计算,得到了值,因此实际传入 callByValue
函数的就只有最后的值。
按名传递
Scala 在函数传参时,还提供了另一种传递方式,按名传递:
def something(): Int = {
println("calling something")
1 // return value
}
def callByName(x: => Int) = {
println("x1=" + x)
println("x2=" + x)
}
callByName(something())
// calling something
// x1=1
// calling something
// x2=1
写法上的区别在于参数列表从 x: Int
变成了 x: => Int
,代表着传入的是一个 x
计算方式,推迟了参数传递时 something()
的计算,后续每次调用 x
时,再现场计算一次。有点类似于将 something()
这句代码本身传给了 x
。
换一种方法理解,=> Int
并不是一种实际的类型,而是一个 Int
的代理,他可以在函数内部完全当作 Int
来使用,但实际使用时,一定要经过这个代理。
⚠️ 注意!
- 按名传递有点类似于传递了一个 Lambda 表达式或是匿名函数之类的东西,但也不完全一样,例如如果传递的是一个
() => Int
型的匿名函数或是 Lambda 表达式,那么调用时就不能只写x
,而应写x()
了。- 无参数的
val
表达式作为参数无法按名传递,因为在声明时已经完成了计算。如果有参数的val
,那就成为了上一条所描述的匿名函数。
可变参数
一个例子足以说明语法:
object Test {
def main(args: Array[String]) {
printStrings("Runoob", "Scala", "Python");
}
def printStrings(args: String*) = {
var i: Int = 0;
for (arg <- args) {
println("Arg value[" + i + "] = " + arg);
i = i + 1;
}
}
}
偏应用和柯里化
Scala 中,如果一个函数有多个参数,可以先给出部分参数,生成一个需要剩下参数的函数,等待后续调用。要实现此功能,可以使用偏应用函数或是柯里化函数。
偏应用函数
语法如下:
import java.util.Date
object Test {
def main(args: Array[String]) {
val date = new Date
val logWithDateBound = log(date, _: String)
logWithDateBound("message1")
}
def log(date: Date, message: String) = {
println(date + "----" + message)
}
}
柯里化函数
偏应用函数在任何多参数函数中都可以使用,而柯里化函数在声明时需要显示声明:
object Test {
def main(args: Array[String]) {
val middle_func = add_curry(3)_
// Or val middle_func = add_curry(3)(_)
println(middle_func(4))
}
def add_curry(a: Int)(b: Int) = {
a + b
}
}
其中,依然需要一个 Place Holder,柯里化还有一个不同是只能从前向后提供参数,不能先提供后面的。
如果最后一个参数列表只接受一个参数,则可以用大括号而不是小括号:
def whileLoop(condition: => Boolean)(body: => Unit): Unit = {
if (condition) {
body
whileLoop(condition)(body)
}
}
var i = 2
whileLoop (i > 0) {
println(i)
i -= 1
} // prints 2 1
另一种不需要 Place Holder 的柯里化函数定义方式是借用 Lambda 表达式:
def add_curry_two(a: Int) = (b: Int) => {
a + b
}
val middle_func = add_curry_two(3)
println(middle_func(4))
当然,也可以定义具有更多参数的柯里化函数:
def add_curry_three(a: Int) = (b: Int) => (c: Int) => {
a + b + c
}
单例对象
单例对象类似于 Java 中的 static class
,在 Scala 中叫 object
,其本身就是一个实例,且无法再实例化出其他实例。一个 object
和同名的 class
共享属性和访问权限,这也叫做伴生类。
⚠️ 注意!
一定是小写的
object
,而不是Object
,Object
拥有截然不同的含义,是一个类名,是所有类的基类。
样例类场景
使用 case class
定义的类叫做样例类,其宗旨是为了存储某种数据结构,而不是业务逻辑,可以类比于 C 中的 struct
,但是功能更多。
样例类的特性如下:
- 按值传递而不是按引用传递;
- 构造器的每个参数都成为val,除非显式被声明为var,但是并不推荐这么做;
- 在伴生对象中提供了apply方法,所以可以不使用new关键字就可构建对象;
- 提供unapply方法使模式匹配可以工作;
- 生成toString、equals、hashCode和copy方法,除非显示给出这些方法的定义。
普通类实例化:
class MyClass(x: Int) {
}
val inst = new MyClass(2)
样例类实例化:
object Test {
def main(args: Array[String]): Unit = {
case class Person(name: String, age: Int)
val person = Person("Alice", 30)
}
}
当然,样例类的定义也可以放在全局作用域。
提取器
首先需要铺垫一个背景,任何一个对象,只要内部定义了 apply
方法,都可以被当作函数调用,例如一个最简单的函数对象:
val func = (x: Int) => {
x + 1
}
println(func.apply(2))
// Equivalent to func(2)
如果是一个类也可以:
object Foo {
var y = 5
def apply (x: Int) = x + y
}
Foo (1)
class MyAdder(x: Int) {
def apply(y: Int) = x + y
}
val adder = new MyAdder(2)
val result = adder(4)
在 C++ 中,也可以实现类似的操作:
class X {
int operator()(int param1, int param2, int param3) {
// do something
}
};
apply
还可以实现 case class
不用 new
就能实例化的操作:
class MyClass(val value: Int) {
}
object MyClass {
// 伴生对象中的 apply 方法,提供一个实例来调用函数
def apply(value: Int): MyClass = new MyClass(value)
}
object Test {
def main(args: Array[String]): Unit = {
val x: MyClass = MyClass(10)
println(x.value)
}
}
现在,可以介绍提取器 unapply
了。
很多情况下,apply
用于构造一个对象,而 unapply
则用于从一个对象中,拆出原始构造参数:
class A(a: Int, b: Int) {
def num1 = a
def num2 = b
override def toString(): String = {
return s"[Class A]: a=$a b=$b";
}
}
object A {
def apply(a: Int, b: Int): A = new A(a, b)
def unapply(input: A): Option[(Int, Int)] = Some((input.num1, input.num2))
}
object Test {
def main(args: Array[String]): Unit = {
val a = A(1, 2)
println(A.unapply(a)); // Some((1,2))
}
}
在 match
模式匹配中,也会自动调用 unapply
:
object Test {
def main(args: Array[String]) {
val x = Test(5)
println(x)
x match {
case Test(num) => println(x + " 是 " + num + " 的两倍!")
//unapply 被调用
case _ => println("无法计算")
}
}
def apply(x: Int) = x * 2
def unapply(z: Int): Option[Int] = if (z % 2 == 0) Some(z / 2) else None
}
此处,match
会自动调用 x
所在类 Test
的 unapply
方法,得到能构造出 x
的参数 num
的值,如果存在,则匹配成功。
SBT 结构
用于构建 Scala 的 SBT,需要项目根目录下有一个名为 project
的目录,里面存放 SBT 的配置,例如编译器版本等,用于构建出 build.sbt
之类的文件,后续项目本身才是用 build.sbt
的配置构建。
project
目录像是 CMakeLists.txt
,而 build.sbt
则像是 Makefile
。
简易程序入口
正常情况下,object
中有一个 main
函数才可作为程序入口执行,例如:
object Test {
def main(args: Array[String]): Unit = {
println("Hello, world!")
}
}
但是 Scala 还提供了一种简易入口,可以不需要 main
,只需要继承 App
:
object Test extends App{
println("Hello, world!")
}
当然,如果直接在一个 Scala 文件中写:
println("Hello, world!")
也可以通过:
scala test.scala
来运行,因为自动调用的是 Scala 解释器,而只有前两种完整的程序结构,才可以通过编译:
scalac code.scala