彭某的技术折腾笔记

彭某的技术折腾笔记

Scala 重点笔记

2024-08-07

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 定义的是一个过程块,不是一个对象。

表达式

defval 还能用于创建表达式,例如:

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 来使用,但实际使用时,一定要经过这个代理。

⚠️ 注意!

  1. 按名传递有点类似于传递了一个 Lambda 表达式或是匿名函数之类的东西,但也不完全一样,例如如果传递的是一个 () => Int 型的匿名函数或是 Lambda 表达式,那么调用时就不能只写 x,而应写 x() 了。
  2. 无参数的 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,而不是 ObjectObject 拥有截然不同的含义,是一个类名,是所有类的基类。

样例类场景

使用 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 所在类 Testunapply 方法,得到能构造出 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
  • 0