事先说明

安装:https://docs.scala-lang.org/getting-started/install-scala.html

文档:https://scala-lang.org/

风格:https://docs.scala-lang.org/style/index.html

工具包:https://docs.scala-lang.org/toolkit/introduction.html

本文主要资料来源为:《Programming in Scala, 5th ed》

编写规范

语言风格

注意事项

语言要求

main

要运行一个 Scala 程序,必须提供一个独立对象的名称,而这个独立对象需要包含一个main方法。

该方法接收一个 Array[String] 作为参数,结果类型为 Unit 。任何带有满足正确签名的 main 方法的独立对象都能被用作应用程序的入口。

它也可以写脚本,只需要你写一个函数即可,例如:@main def main() = println("hello,world!")

速记

语法糖

  1. 如果函数是一个接受单个参数的语句,那就不用给出参数名和参数。例如 args.foreach(println),另外foreach内部可以直接使用case

  2. 如果一个方法只接收一个参数,则在调用它的时候,可以不使用英文句点或圆括号。例如 1.+(2)

  3. 无论是函数还是对象,Scala 都允许你在使用的时候直接定义一个,属于匿名。

代码结构

Scala 允许圆括号控制流结构,花括号代码块,分号断句,基本像 C 一样的情况;也允许 Python 一样的缩进语法,可以混用。

工具链

scalac 编译器

scala 官方repl

amm 更好的repl

sbt、mill 构建工具

scalafmt 代码格式化

metals 语言服务器

编译器

jvm

选项说明
-java-output-version 8/9/10/11指定生成兼容的 Java 版本字节码
-usejavacp使用 Java 的 classpath
-J<flag>传递参数给 JVM
-D<key=value>传递系统属性给 JVM

语言特性控制

选项作用典型用途
noAutoTupling禁止自动元组化强制函数参数匹配更严格
dynamics允许继承 scala.Dynamic实现动态调用(如 DSL)
unsafeNulls启用不安全 null(弱化 null 安全)与 Java 互操作
postfixOps允许后缀操作符(如 x toString兼容旧代码(不推荐)
strictEquality启用严格相等类型检查提高类型安全
implicitConversions允许隐式转换(无警告)旧库兼容
adhocExtensions允许临时扩展方法扩展已有类型
experimental.namedTypeArguments允许命名类型参数提高泛型可读性
experimental.genericNumberLiterals允许泛型数字字面量数值库开发
experimental.macros允许 Scala 2 宏兼容旧宏
experimental.dependent启用依赖方法类型高级类型编程
experimental.erasedDefinitions允许 erased 定义性能优化
deprecated.symbolLiterals允许 'symbol 语法旧 Scala 2 代码(已弃用)
experimental.saferExceptions更安全异常机制类型安全异常
experimental.pureFunctions启用纯函数检查捕获检查
experimental.captureChecking启用捕获检查资源安全
experimental.separationChecking启用分离检查(需 captureChecking)更严格安全模型
experimental.into允许 into 参数修饰符类型转换控制
experimental.modularity启用模块化实验特性模块系统探索
experimental.packageObjectValues允许 package object 作为值高级元编程

推荐开启:strictEquality,adhocExtensions

Scala3 和 2

选项说明
-new-syntax强制使用 Scala 3 新语法
-old-syntax强制使用旧语法
-indent使用缩进语法
-no-indent使用传统 {} 语法

版本和代码来源

选项作用
-source 3.x指定语法版本,注意feture
-source 3.x-migration升级提示模式
-rewrite自动改代码,配合-migration
-sourcepath指定源码目录
-sourceroot指定项目根目录

去符号

TASTy = Typed Abstract Syntax Tree(带类型的抽象语法树),它是 Scala 3 编译器保存的“带完整类型信息的程序结构”。Scala 3 会把 带完整类型信息的 AST 保存到 class 文件中

-from-tasty 可以从 .tasty 文件重新编译。

输出

选项说明
-explain更详细的错误解释
-explain-cyclic更详细地解释循环引用错误
-print-lines显示源码行号
-print-tasty打印 tasty 信息
-color彩色输出(always / never)
@file从文件读取参数
-feature当使用需要显式导入的语言特性时,发出警告并显示位置。

神秘用法

scalac 可以开启实验性语言特性

scalac 可以把 scala 编译成 js

mill

声明式配置 (YAML)

简单项目,仅需设置依赖和版本

# build.mill.yaml
= build.mill.yaml
extends: ScalaModule
scalaVersion: 3.8.1
mvnDeps:
  - com.lihaoyi::scalatags:0.13.1  # :: 表示 Scala 依赖
  - com.lihaoyi::mainargs:0.7.8

# 子模块
# test/package.mill.yaml
= 瀛愭ā鍧?// # test/package.mill.yaml
extends: [build.ScalaTests, TestModule.Utest]
mvnDeps:
  - com.lihaoyi::utest:0.9.1
配置键说明示例
extends继承的模块类型ScalaModule, SbtModule, ScalaTests, TestModule.Utest
scalaVersionScala 版本"3.8.1", "2.13.16"
mvnDepsMaven 依赖(支持 :: Scala 语法)com.lihaoyi::scalatags:0.13.1
moduleDeps模块间依赖(同项目)[bar], [build.bar]
jvmVersion指定 JVM 版本运行zulu:21, system

可编程配置

mill 用 scala 做配置语言。

Mill 的一切构建于两个概念之上:

  1. Tasks(任务):对应 Scala 中的 def 方法
  2. Modules(模块):对应 Scala 中的 object 对象

用到相关功能记录

用法

鍖?

package 123.123 // 在文件顶部声明package子句,意思就是把文件放在指定包
//or
package 123.123: // 或者当成命名空间一样使用
  class xxx

__root__ 包保证可以访问顶层包名,避免被遮挡。

不嵌套时,包之间不会自动互相可见。

嵌套包允许:

所有 Scala 文件默认包含三个包:

鐢ㄦ硶

import java.lang.*
import scala.*
import scala.Predef.*

让组合(has‑a)像继承(is‑a)一样方便,通过自动生成“转调方法”。

用途:

  1. 定义在块里的变量只在该块中有效,内层块能访问外层变量,反之不行

  2. 同一块里不能重复定义同名变量,内层可以定义与外层同名变量 → 叫做“遮挡”(shadowing),Scala 允许遮挡,但 Java 不允许

  3. REPL 每行都是新的隐式作用域

  4. 每次调用函数,局部变量都会重新创建,局部变量只属于当前调用,不共享

类型族是某个行为的“类型集合“

FromString 类型族可以转化字符串为T

类型后置,支持隐式转化,类型来自 java.lang。

可以直接多个变量赋值同一个

类型位数说明Java 对应
Byte8-bit有符号整数byte
Short16-bit有符号整数short
Int32-bit整数(默认整数类型)int
Long64-bit长整数long
Float32-bit单精度浮点float
Double64-bit双精度浮点(默认小数类型)double
Char16-bitUnicode 字符char
Boolean1-bit(逻辑)true / falseboolean
Unit-类似 Java void

富包装类 = 通过隐式转换(或隐式类)给原类型“增强方法”的包装类

比如 Int 和 Rich Int

ArrayBuffer

int x = 1; // java中int没有方法
1.to(10)   // scala却可以调用方法

// 因为标准库写了富包装类
extension (x: Int)
  def double: Int = x * 2

类型:34L 31l

进制:0x,0X 1_000_000_000

科学记数法:1.234F 3e5f 3e5D

unicode:

原生字符串:三个双引号,| 和 stripMargin 换行

转义字符:

写法含义
\n换行
\t制表符
\r回车
\\反斜杠
\"双引号
\'单引号
\b退格
\f换页

字符串插值:

鍑芥暟鏍煎紡

// s 插值
val name = "Tom"
val age = 20
println(s"My name is $name, I am $age years old")

// f 插值,类似 C 的 printf
val height = 1.756
println(f"My height is $height%.2f meters")

// raw 插值,不转义
println(raw"Hello\nWorld")

链表结构,不可变,单类型

鍛藉悕

val oneTwo = List(1, 2)
val twoThree = List(2, 3)
val oneTwoTwoThree = oneTwo ::: twoThree  // 列表拼接,O(长度左侧)
val oneTwoThree = 1 :: twoThree           // cons,O(1),在头部添加

// :: 是右操作元方法,实际上为  twoThree.::(1)
// 建议:需要尾部添加时,先逆序构建再 reverse,或用 ListBuffer
特性/方法说明
:::List 特有的拼接操作(比 ++ 更特化)
:: (cons)在头部添加元素,O(1)
+:, :+在头部/尾部添加元素(:+ 需要遍历,O(n))
addString追加到 StringBuilder
方法说明示例
List(a, b, c)创建列表List(1, 2, 3)
List.range(start, end)数值区间(不含 end)List.range(1, 5)List(1,2,3,4)
List.range(start, end, step)带步长List.range(1, 10, 2)
List.fill(n)(elem)重复元素List.fill(3)("a")List("a","a","a")
List.fill(n, m)(elem)多维List.fill(2,3)(0)
List.tabulate(n)(f)按函数生成List.tabulate(5)(n => n * n)
List.concat(xs, ys, zs)拼接多个列表-
List.empty[A]空列表-
方法说明
+=尾部追加,O(1)
+=:头部追加,O(1)
++=追加集合
++=:头部追加集合
insert(idx, elems)指定位置插入
remove(idx) / remove(idx, count)移除元素
trimStart(n) / trimEnd(n)去掉前/后 n 个
clear清空
clone复制
result返回不可变集合

JVM 数组包装,可变,单类型,长度固定

scala
// 创建方式
val arr1 = new Array[Int](3)           // 3个0
val arr2 = Array("zero", "one")        // 调用 Array.apply
val arr3 = Array.apply("zero", "one")  // 同上

// 访问与修改
arr2(0) = 1        // 语法糖,实际调用 arr2.update(0, 1)
arr2(0)            // 语法糖,实际调用 arr2.apply(0)
特性/方法说明
update(i, x)修改索引 i 处的值,O(1)
apply(i)获取索引 i 处的值,O(1)
clone浅拷贝数组
方法说明示例
Array.apply(elems)变参创建Array(1, 2, 3)
Array.empty[T]空数组Array.empty[Int]
Array.fill(n)(elem)重复元素Array.fill(3)("a")
Array.fill(n, m)(elem)多维重复Array.fill(2,3)(0)
Array.tabulate(n)(f)按函数生成Array.tabulate(5)(i => i*i)
Array.range(start, end)数值区间Array.range(1, 5)
Array.range(start, end, step)带步长区间Array.range(1, 10, 2)
Array.concat(arrays)拼接多个数组Array.concat(arr1, arr2)

鍙傛暟

import scala.collection.mutable.ArrayBuffer

val buf = ArrayBuffer[Int]()
buf += 1          // 追加元素
buf += (2, 3)     // 追加多个
buf ++= Array(4, 5)  // 追加集合
buf.insert(0, 0)  // 在位置 0 插入
buf.remove(0)     // 移除位置 0
buf.toArray       // 转为 Array
方法说明
+=追加单个元素
+=:在头部添加
++=追加集合
insert(idx, elems)在指定位置插入
remove(idx) / remove(idx, count)移除元素
trimStart(n) / trimEnd(n)去掉前/后 n 个
clear清空
result返回不可变集合(构建器模式)

固定长度,不可变多类型,最多 22 个元素(Scala 2)/ 任意长度(Scala 3)

鍙彉鍙傛暟

val pair = (1, "xyz")
val triple = (1, 3.14, "Fred")

无序集合,元素唯一,有可变和不可变两种版本

鎸囧畾鍙傛暟鍜岄粯璁ゅ弬鏁?

// 不可变(默认)
val jetSet = Set("Boeing", "Airbus")

// 可变
import scala.collection.mutable
val movieSet = mutable.Set("Spotlight", "Moonlight")

特殊类型:

类型说明
SortedSet有序集合(基于树)
LinkedHashSet保持插入顺序
BitSet位集合,存储非负整数,空间高效
HashSet哈希实现,默认

键值对集合,键唯一,有可变和不可变两种版本

鍖垮悕鍙傛暟

// 不可变
val scores = Map("Alice" -> 10, "Bob" -> 3)

// 可变
import scala.collection.mutable
val mutableScores = mutable.Map("Alice" -> 10)

特殊类型:

类型说明
SortedMap按键排序(仅不可变)
LinkedHashMap保持插入顺序(可变)
TreeMap红黑树实现
HashMap哈希实现,默认
WeakHashMap弱引用键
ConcurrentMap线程安全(并发包)

Scala 的 String 就是 Java 的 Stringjava.lang.String),但 Scala 通过隐式转换提供了 StringOps,让 String 拥有集合操作能力。

StringBuilder 是可变字符串

閮ㄥ垎搴旂敤鍑芥暟

val s = "Hello"

// 这些操作来自 StringOps,不是 String 本身的方法
s.map(_.toUpper)        // "HELLO"
s.filter(_.isLetter)    // "Hello"
s.head                  // 'H'
s.tail                  // "ello"
s.reverse               // "olleH"
s.take(3)               // "Hel"
s.drop(2)               // "llo"

具体看书

所有运算符,都是方法的实现,通过语法糖实现表达式形式

可以用方法写重载

表格:TODO

函数的尾递归只能优化自己的这个函数,不能两个函数相互优化,这是因为jvm字节码无法实现导致的

方法和函数都允许重载

函数内部可定义函数

鏌噷鍖?

def mux(x: Int, y: Int): Int = if x > y then x else y // 结果推断可以去掉类型,但是有些时候不行,比如递归
// 函数即使不写return也可以,默认最后一行结果当返回值,官方推荐这个办法,进而让函数变成小函数

妯″紡鍖归厤

def echo(args: String*) = for s <- args do println(s) // 类型后面有星号 * 表示“这个参数可以重复出现
echo("a", "b", "c", "d") // args 在函数内部的类型其实是 Seq[String](可以当成 List)
val list = List("a", "b", "c")
echo(list: _*) // 传入时要解包

鍩烘湰缁撴瀯

def speed(time: Int = 10, distance: Int = 100)
speed(time = 10, distance = 100) // 如果用命名参数,顺序可以打乱,没有命名的按顺序走

同时这也是推荐使用的断言方法,因为比起简单的布尔判断,它在断言禁用时后续不会调用比较逻辑,性能更好。

鍙橀噺瀹氫箟 (瑙f瀯璧嬪€?

assetionsEnabled = true
def byNameAssert(predicate: ()  => Boolean) =
  if assertionsEnabled && ! predicate() then
    throw new AssetionError
// 我们肯定希望少写点东西,但是下面语句是不合法的
byNameAssert(5 > 3)
// 所以需要改写
def byNameAsset(predicate: => Boolean ) = // predicate: => Boolea 就是传名参数,意思就是把 “5>3” 不计算直接传入,等到用到再计算
  if assettionsEnabled && !predicate then
    throw new AsssetionError

for 琛ㄨ揪寮?

参数 => 结果
(参数: 类型) => 结果 // 可省略括号,可省略类型,可用下划线代替变量
(x: Int) => x > 0

_ > 0 // _本身就是一个占位符号,代表参数,所以可以直接去掉参数本身

(a, b) => a + b
_ + _ // 第一个代表第一个变量,第二个代表第二个变量

闭包 = 函数字面量 + 它捕获的自由变量

绫诲拰瀵硅薄

var more = 1
val add = (x: Int) => x + more // 捕获的是变量本身的引用,不是值。如果 more 发生变化,闭包能看到
val addmore = (x: Int) => more += x // 闭包也可以修改外层变量(尤其 var)

鏋勯€?

def sum(a: Int, b: Int, c: Int) = a + b + c // 完整函数
val f = sum _ // 方法转函数,等于 f = (a: Int, b: Int, c: Int) => sum(a,b,c)
val g = sum(1, _, 3) // 部分应用参数

缁勫悎鍜岀户鎵?

def plainOldSum(x: Int, y: Int) = x + y
def currideSum(x: Int)(y:Int) = x + y
curriedSum(1)(2)
def first = currideSum(1)
first(2)

如果只有一个抽象方法的接口(Single Abstract Method,SAM),可以直接用函数填写

鎶借薄绫?

trait Increaser:
  def increase(x: Int): Int
val inc: Increaser = x => x + 1

scala 的控制流结构基本都可以返回值

没有continue和break,更推荐用递归实现函数,另外如果递归发生函数末尾,会触发尾递归优化,也就是编译器会转化为循环,而不是跳到一个新的函数

鎵╁睍绫?

if 条件 then 结果1 else 结果2
while 条件 do 结果
for 条件 do 结果 // 用来执行副作用
for 条件 if 条件 ... do 结果 // 中间也可以创造新量,准确来说,if 只是添加过滤器
for 条件 yield 结果 // 如同x.map(),事实上本质就是这个方法
xxx match
  case xxx => xxx // scala 没有 break,在这里是隐式的

构造数值范围需要说明

缁熶竴璁块棶鍘熷垯

1 to 4
1 until 4 // 不包含上界

鏅€氱被

selector match {
  case pattern1 => expression1
  case pattern2 => expression2
  case _ => defaultExpression // 兜底
}

这是模式匹配的核心,共有 8 种主要形式。

模式名称语法示例说明与注意点
通配模式case _ =>匹配任何对象,不绑定变量。常用于忽略不关心的部分或默认分支。
常量模式case 5 =>case "hello" =>case Nil =>case Pi =>匹配值相等 (==) 的字面量、单例对象或常量。注意:必须是大写字母开头(如 Pi)或是字面量。
变量模式case x =>匹配任何对象,并将对象绑定给变量 x注意:必须是小写字母开头。
构造方法模式case BinOp("+", e, Num(0)) =>深度匹配。检查对象类型及其构造参数。支持嵌套(如 Num(0) 在内部)。
序列模式case List(0, _, _) =>case List(0, _*) =>匹配 List 或 Array。_ 匹配单个元素;_* 匹配任意数量(0到多个)的剩余元素。
元组模式case (a, b, c) =>匹配元组。常用于同时处理多个值。
带类型的模式case s: String =>case m: Map[_, _] =>替代 instanceof重要限制(类型擦除):除了数组(Array),泛型参数在运行时被擦除。Map[Int, Int]Map[String, String] 在运行时是一样的,只能匹配到 Map
变量绑定模式case e @ UnOp("abs", _) =>语法:变量名 @ 模式。既匹配模式,又将整个匹配对象赋值给变量 e

编译器根据首字母大小写区分变量模式和常量模式:

在模式之后添加 if 进行逻辑判断。

鐗硅川

case BinOp("+", x, y) if x == y => // 仅当 x == y 时匹配

用于限制类的继承结构,辅助编译器进行完备性检查。

Scala 标准的判空方式:

浣跨敤

optionVal match {
  case Some(x) => println(x) // 有值,解包
  case None    => println("无值")
}

除了 match 表达式,模式匹配还可用在以下地方:

绫诲瀷鍙傛暟鍖?

val (number, string) = (1, "abc") // 拆解元组
val BinOp(op, left, right) = expr // 拆解样例类

用花括号包围的一组 case,可作为函数字面量:

鍗忓彉

val withDefault: Option[Int] => Int = {
  case Some(x) => x
  case None => 0
}

閫嗗彉

for ((country, city) <- capitals) // 遍历 Map 时直接解构成元组,遍历

val results = List(Some("apple"), None, Some("banana"))
for (Some(fruit) <- results)      // 只匹配 Some(x)

娉涘瀷涓婁笅鏂囧弬鏁?

// 可以不带 new
class Person(name: String, age: Int) // 圆括号传参数,参数化不用new。但是这个传入的参数只是一个临时变量,而非类的字段,无法像访问字段一样访问这个参数。它的作用域是只有位于类体内可以访问。
class Person(val name: String, var age: Int) // 就可以当成字段了

class Person(val name: String, val age: Int):
  require(age >= 0, "Age must be non-negative") // 使用require(条件)方法,可以用作构造的检查。条件为假抛出错误IllegalArgumentException

def this(n: Int) = this(n, 1) // 如果使用this(...) 表示“调用同一个类的另一个构造方法”,第二个this表示调用主构造函数

private[X] / protected[X] 中的 X 可以是:

鍖垮悕涓婁笅鏂囧弬鏁?

new RationalTrait {
  val numerArg = 1
  val denomArg = 2
}

涓婁笅鏂囩晫瀹?

abstract class Element:
  def contents: Vector[String]
  def height: Int = contents.length
  def width: Int = if height == 0 then 0 else contents(0).length

抽象类是不能实例化的父类,用来定义“模板结构”,并强制子类实现部分行为。

鎶借薄鎴愬憳

class VectorElement(conts: Vector[String]) extends Element:
  def contents: Vector[String] = conts

继承只继承公有成员,另外如果重写了相同函数,也不会继承

可以用final修饰避免继承相应方法,变量和类

this 可以直接指代类本身

鍩虹鐢ㄦ硶

abstract class Element:
  def contents: Vector[String]
  def height: Int = contents.length
  def width: Int = if height == 0 then 0 else contents(0).length

这种无参方法(parameterless method)在Scala中很常见。与此对应,那些用空的圆括号定义的方法,如def height(): Int,被称作空圆括号方法(empty-paren method)。

推荐的做法是对于没有参数且只通过读取所在对象字段的方式访问其状态(确切地说,并不改变状态)的情况,尽量使用无参方法。这样的做法支持所谓的统一访问原则(uniform access principle):使用方代码不应受到某个属性是用字段还是用方法实现的影响。

唯一的区别是字段访问可能比方法调用的速度快一些,因为字段值在类初始化时就被预先计算好,而不是在每次方法调用时都重新计算。另一方面,字段需要每个Element对象为其分配额外的内存空间。因此属性实现为字段好还是方法好,这个问题取决于类的用法,而用法是可以随着时间变化而变化的。核心点在于Element类的使用方不应该被内部实现的变化所影响。

闄愬埗

// [] 为可选
class GenshinImpact:
  val version [= 1.1.1] // 可以直接在类定义写值,初始化等于
  private/protect var fuck // 默认为public,显式添加为私有
  def xxxx(): xxxx // 定义方法,官方建议写出返回值
  override def xxxx // 重写继承对象的方法或者变量

替换 class 关键字为 object,实现静态变量。常用在存放工具方法和一次性共享数据。

object 定义的单例对象在第一次被访问时初始化,且在整个程序中只有一个实例。

当单例对象与某个类共用同一个名称时,它被称作这个类的伴生对象。必须在同一个源码文件中定义类和类的伴生对象。同时,类又叫作这个单例对象的伴生类。

类和它的伴生对象可以互相访问对方的私有成员。

class/object 左边加个 case,编译器会自动生成模板代码,这个类还能实现模式匹配,因为它隐式实现了unapply方法

TODO

生成工厂方法:

生成同参数名变量:

生成方法:

可自行覆盖

编译器自动生成的特性(免费午餐):

工厂对象包含创建其他对象的方法,使用方用这些工厂方法来构建对象,而不是直接用new来构建对象。

这种做法的好处是对象创建逻辑可以被集中起来,而对象是如何用具体的类表示的可以被隐藏起来。这样既可以让你的类库更容易被使用方理解,因为暴露的细节更少,又提供了更多的机会,可以让你在未来不破坏使用方代码的前提下改变类库的实现。

鏀硅壇绫诲瀷

object Element:
  def elem(contents: Vector[String])
  def elem ...
  def elem ...

val e1 = Element.elem("hello")   // 可能内部用的是 LineElement
val e2 = Element.elem(Vector("a", "b"))  // 内部是 VectorElement
val e3 = Element.elem('x', 3, 5)         // 内部是 UniformElement

璺緞渚濊禆

trait Pp:
  def ppi = "zxcvqwer"
class Frog extends Pp

class Animal
trait Pet extends Animal // 特质可以继承超类,这样只有同样继承该超类的类才能使用该特质
class Peet extends Animal with Pet // 如果想要将特质混入一个显式继承自某个超类的类,则可以用extends关键字来给出这个超类,并用逗号(或with关键字)来混入特质。

Scala 特质可以写:

它解决了多重继承的问题。在典型的菱形调用情况下,如果要调用最开始的父类方法,那么特质会把所有父类和特质排成一条从左到右的队列(线性化),然后 super 就去找右边下一个。

特质还能叠加效果

鍒濆鍖?

// 原始队列类(BasicIntQueue)有一个方法
def put(x: Int) = buffer += x
// 把 x 变成 2*x 再放进去。
trait Doubling extends IntQueue {
  abstract override def put(x: Int) = super.put(2 * x)
}

trait Incrementing extends IntQueue {
  abstract override def put(x: Int) = super.put(x + 1)
}

trait Filtering extends IntQueue {
  abstract override def put(x: Int) =
    if x >= 0 then super.put(x)
}

// 效果依次叠加
new BasicIntQueue with Filtering with Incrementing

给类自动加一堆方便方法(富接口),也就是说有几个没有实现的方法,但是有好多个基于他们实现的方法,你只要实现了没有实现的方法,就可以获得好多方法。

鎵╁睍鏂规硶

trait IntQueue:
  def put(x: Int)
  def get(): Int

trait RichIntQueue extends IntQueue:
  def putTwice(x: Int) =
    put(x)
    put(x)
// 实现了IntQueue,就可以用RichIntQueue了

鍩虹鐢ㄦ硶

class Box[Content](private val value: Content) { // 此处 Content是参数模板名
  def get: Content = value
}

class Animal
class Dog extends Animal // Dog 是 Animal 的子对象

val dogBox = new Box[Dog](new Dog)
val animalBox: Box[Animal] = dogBox  // error

正常情况为不变,意思就是Box[Dog]和Box[Animal]不同类型,所以不合法

娉涘瀷

class Box[+Content]

val dogBox = new Box[Dog](new Dog)
val animalBox: Box[Animal] = dogBox  // 合法

协变就可以合法实现。

鍏勫紵鍑芥暟

class Printer[-Input]

val animalPrinter = new Printer[Animal]
val dogPrinter: Printer[Dog] = animalPrinter  // 合法

与协变相反,父类型的类型参数化变成子类型的类型参数化的子类型

协变如果参数被用在方法里会出问题,比如在前面的例子如果 animalBox.put(new Cat),这样实际上是 Dog 类型存放了 Cat 类型

娉涘寲浠f暟鏁版嵁绫诲瀷锛圙ADT锛?

class Queue[+Element] {
  def add[Super >: Element](item: Super): Queue[Super]
}

这样编译器就会找 Dog 和 Cat 的超集,避免出现问题,超集也就是 Animal。

下界也同理,符号是 <:

为了避免多次传递模板变量或者函数,添加了上下文参数的这个机制

鏂█

given prompt: PreferedPrompt = PreferePrompt("your wish>")

def greet(name:String)(using prompt: PreferedPrompt) =
  println(s"Welcome, $name, prompt.perference)

greet("123")

只要在相同作用域用 given 关键字给一个类型的模板,后续函数用 using 就可以使用模板,编译器会找相同类型,如果类型不同要显式声明

鍩烘湰鐢ㄦ硶

def isort[T](xs: List[T])(using ord: Ord[T])

编译器根据 T 查找 Ord[T] 实例。

ensuring

val dog: Animal =
  new Animal:
    def speak() = "wang"

given Animal with
  def speak() = "wang"

def isort[T](xs: List[T])(using Ordering[T])
== 娴嬭瘯

AnyFlatSpec锛圔DD椋庢牸锛?

def maxList[T: Ordering](elements: List[T])
def maxList[T](elements: List[T])(using Ordering[T]) // 等价于

summon[Ordering[T]] // 自动获取given实例,隐式获取

类型族

普通 import 不会导入 given,下面的才是正确用法

AnyWordSpec

import TomsPrefs.given // 全部引入
import TomsPrefs.given Ordering[?] // 按类型引入
import TomsPrefs.prefPromptOrd // 单独引入

AnyFeatureSpec锛堥渶姹傞┍鍔級

trait Abstract {
  type T
  def transform(x: T): T
  val initial: T
  var current: T
}

class Concrete extends Abstract:
  type T = String

如果类或者特质这样写,那这些就是抽象成员,子类继承要申明类型

Matchers DSL锛堟洿鑷劧璇█锛?

class Abstract:
  type SuitableFood <: Food

寮傚父鎶涘嚭

class Animal {
  type SuitableFood <: Food
} // 普通写法

Animal { type SuitableFood = Grass } // 改良写法

ScalaCheck

class Outer {
  class Inner
}

val o1 = new Outer
val o2 = new Outer

o1.Inner  ≠  o2.Inner // 它们是不同的类型

val x: Outer#Inner = new o1.Inner // 这样就可以
val y: Outer#Inner = new o2.Inner
trait RationalTrait {
  val numerArg: Int
  val denomArg: Int

  require(denomArg != 0)
}

scala 初始化是先初始化特质本身,再初始化子类里的 val

也就是说require(denomArg != 0)初始化的时候,denomArg还没有赋值,默认为0,直接失败

解决办法为

trait RationalTrait(val numerArg: Int, val denomArg: Int) {
  require(denomArg != 0)
}

// or
trait RationalTrait {
  val numerArg: Int
  val denomArg: Int

  lazy val g = { // 不在初始化时计算,在第一次使用时才计算
    require(denomArg != 0)
    gcd(numerArg, denomArg)
  }
}
extension (s: String)
  def singleSpace: String =
    s.trim.split("\\s+").mkString(" ")

" hello   world ".singleSpace

让函数调用更符合面向对象习惯,但实际上是外部函数

extension [T](xs: List[T])
  def tailOption: Option[List[T]] =
    if xs.isEmpty then None else Some(xs.tail)

List(1,2,3).tailOption
extension (n: Int)
  def isMinValue: Boolean =
    n == Int.MinValue

  def absOption: Option[Int] =
    if isMinValue then None else Some(n.abs)

编译器会改写为:

if n.isMinValue then ...

EDT(Enumerated Data Type)就是:取值只能是固定几个命名值之一。也就是通常理解的枚举

enum Direction:
  case North, East, South, West

// 可以带参数
enum Direction(val degrees: Int):
  case North extends Direction(0)
  case East  extends Direction(90)
  case South extends Direction(180)
  case West  extends Direction(270)

// 可以带方法
  def invert: Direction =
    this match
      case North => South
      case South => North
      case East  => West
      case West  => East

// 可以自定义伴生对象
object Direction:
  def nearestTo(deg: Int): Direction =
    Direction.values.minBy(d =>
      Math.abs(d.degrees - deg)
    )

它等价于:

与 Java 枚举兼容,不能为单个 case 定义专属方法

ADT = 一组有限 case + 至少有一个 case 带参数。

也就是把case变成方法,强制带数值,而且编译器在模式匹配时有检查

enum Eastwood[+G, +B]:
    case Good(value: G)
    case Bad(value: B)
    case Ugly(exception: Exception)
// 只能在 enum 本身定义方法。
    def map[H](f: G => H): Eastwood[H, B] =
      this match
        case Good(v) => Good(f(v))
        case Bad(b)  => Bad(b)
        case Ugly(e) => Ugly(e)

每个带参数的 case 会生成:

enum Literal[T]:
  case IntLit(value: Int) extends Literal[Int]
  case BooleanLit(value: Boolean) extends Literal[Boolean]

断言是用来检查“程序员自己认为一定成立”的条件,处理代码逻辑错误

测试是在外部验证代码是否符合预期,大规模测试

错误处理是用来处理“正常可能发生的错误“,处理用户错误

assert(condition) // 如果 condition 为 false,会抛出:java.lang.AssertionError
assert(condition, explanation) // AssertionError: explanation内容

断言可以在运行时开关:

参数作用
-ea启用断言
-da禁用断言
def double(x: Int): Int = {
  val result = x * 2
  assert(result >= x)
  result
}

// 优雅写法
def double(x: Int): Int =
  (x * 2) ensuring (_ >= x)

// 真实原理
result ensuring (r => r >= x)

ensuring 做的事情:

  1. 先算出 result
  2. 把 result 传给函数
  3. 如果函数返回 true → 正常返回
  4. 如果 false → 抛 AssertionError

要先new一个异常对象,然后throw

throw new RuntimeException("n must be even")

try
  xxxxx
catch
  case xx: 异常类型
  case ...
finally // 无论是否异常,都会执行
  xxxx

try-catch-finally 最后会返回一个值

Unit Test        → 测函数
Integration Test → 测模块
API Test         → 测接口
Property Test    → 测规律
Performance Test → 测性能

一个类对应一个测试类

测试文件名一般加 Test 或 Spec

每个 public 方法都应该被测试

传统测试

class MyTest extends AnyFunSuite {

  test("1 + 1 should equal 2") {
    assert(1 + 1 == 2)
  }

}
class MySpec extends AnyFlatSpec {

  "A calculator" should "add numbers correctly" in {
    assert(1 + 1 == 2)
  }

}
class MySpec extends AnyWordSpec {

  "Calculator" should {
    "add numbers" in {
      assert(1 + 1 == 2)
    }
  }
}
class TVSpec extends AnyFeatureSpec {

  Feature("TV power button") {

    Scenario("Turn on TV") {
      Given("TV is off")
      When("Press power button")
      Then("TV should turn on")
    }

  }
}
import org.scalatest.matchers.should.Matchers

1 + 1 should be (2)

an [IllegalArgumentException] should be thrownBy {
  divide(1, 0)
}
assertThrows[IllegalArgumentException] {
  divide(1, 0)
}

期待抛出这个异常

forAll { (a: Int, b: Int) =>
  a + b == b + a
}

forAll { (x: Int) =>
  whenever(x > 0) {
    double(x) > x
  }
}