事先说明
安装:https://docs.scala-lang.org/getting-started/install-scala.html
风格:https://docs.scala-lang.org/style/index.html
工具包:https://docs.scala-lang.org/toolkit/introduction.html
本文主要资料来源为:《Programming in Scala, 5th ed》
编写规范
语言风格
- 包命名应该遵守 java 命名规范
- 常量应该用大驼峰,变量和值应该用小驼峰。这里的常量不是指
val,而是指类似object的成员这种 object,class,trait应该大驼峰- 方法和函数应该用小驼峰
- 方法带有括号代表改方法有副作用,即使没有参数
@volatile这种修饰应该用小驼峰
注意事项
- Scala 所有运算符本质上都是方法
- Scala 在每一个 Scala 源码文件中都隐式地引入了 java.lang 和 scala 包,不用申明
- Scala 不可变变量赋值,如果不是值语义的话,都是引用
语言要求
main
要运行一个 Scala 程序,必须提供一个独立对象的名称,而这个独立对象需要包含一个main方法。
该方法接收一个 Array[String] 作为参数,结果类型为 Unit 。任何带有满足正确签名的 main 方法的独立对象都能被用作应用程序的入口。
它也可以写脚本,只需要你写一个函数即可,例如:@main def main() = println("hello,world!")
速记
语法糖
如果函数是一个接受单个参数的语句,那就不用给出参数名和参数。例如
args.foreach(println),另外foreach内部可以直接使用case如果一个方法只接收一个参数,则在调用它的时候,可以不使用英文句点或圆括号。例如
1.+(2)无论是函数还是对象,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 |
scalaVersion | Scala 版本 | "3.8.1", "2.13.16" |
mvnDeps | Maven 依赖(支持 :: Scala 语法) | com.lihaoyi::scalatags:0.13.1 |
moduleDeps | 模块间依赖(同项目) | [bar], [build.bar] |
jvmVersion | 指定 JVM 版本运行 | zulu:21, system |
可编程配置
mill 用 scala 做配置语言。
Mill 的一切构建于两个概念之上:
- Tasks(任务):对应 Scala 中的
def方法 - Modules(模块):对应 Scala 中的
object对象
用到相关功能记录
包
用法
鍖?
package 123.123 // 在文件顶部声明package子句,意思就是把文件放在指定包
//or
package 123.123: // 或者当成命名空间一样使用
class xxx__root__ 包保证可以访问顶层包名,避免被遮挡。
不嵌套时,包之间不会自动互相可见。
嵌套包允许:
内层包访问外层包的名称(如果用缩进语法)
同一个包内的名称可直接使用。
内层包可用简单名称访问外层包的成员(仅在使用嵌套语法时有效)。
顶层包不会因名称遮挡而无法访问,可以用
__root__访问。一个包下不仅可放类/特质/对象,还可以放任意定义(如函数、类型别名)。
包对象可以包含包级别的函数及值。
能在整个包内被引入使用。
import 可以放在任意地方(局部或全局)。
可以引入包,也可以引入对象(包括普通对象)的成员。
支持选择性引入:
- 只引入指定成员
- 用
as重命名成员 - 用
as \_隐藏成员 - 用
*引入所有成员(最后位置)
引入选择器支持任意组合:选、重命名、隐藏、通配。
引入包名本身也是允许的(引入嵌套包以缩短路径)。
所有 Scala 文件默认包含三个包:
鐢ㄦ硶
import java.lang.*
import scala.*
import scala.Predef.*让组合(has‑a)像继承(is‑a)一样方便,通过自动生成“转调方法”。
export 对象.*会把该对象的方法以“同名方法”形式导出到当前类。- 导出会生成 final 重载方法(export alias)。
导出支持所有 import 的选择器语法:
- 指定导出
- 重命名
- 排除(as _)
- 通配
- 可用于隐藏某些方法,也可用于只暴露部分方法。
用途:
- 使用组合代替继承但保持同样的易用性。
定义在块里的变量只在该块中有效,内层块能访问外层变量,反之不行
同一块里不能重复定义同名变量,内层可以定义与外层同名变量 → 叫做“遮挡”(shadowing),Scala 允许遮挡,但 Java 不允许
REPL 每行都是新的隐式作用域
每次调用函数,局部变量都会重新创建,局部变量只属于当前调用,不共享
类型族是某个行为的“类型集合“
FromString 类型族可以转化字符串为T
类型后置,支持隐式转化,类型来自 java.lang。
可以直接多个变量赋值同一个
| 类型 | 位数 | 说明 | Java 对应 |
|---|---|---|---|
Byte | 8-bit | 有符号整数 | byte |
Short | 16-bit | 有符号整数 | short |
Int | 32-bit | 整数(默认整数类型) | int |
Long | 64-bit | 长整数 | long |
Float | 32-bit | 单精度浮点 | float |
Double | 64-bit | 双精度浮点(默认小数类型) | double |
Char | 16-bit | Unicode 字符 | char |
Boolean | 1-bit(逻辑) | true / false | boolean |
Unit | - | 类似 Java void | 无 |
Any 是所有类型的根类,提供通用方法(==、!=、equals、hashCode、toString)。
AnyVal:所有值类型(Int、Double、Boolean 等)及用户定义的值类。
AnyRef:所有引用类型的基类(在 JVM 上等同于 java.lang.Object)。
Null:所有引用类型的子类型(null 的类型),不能赋给值类型。
Nothing:所有类型的子类型,但没有值,用于表示不会正常返回的表达式(如 error)。
值类之间没有继承关系,且用隐式转换提供更多功能(如 Int → RichInt)。
可以定义自己的值类型(继承 AnyVal,只有一个 val 参数)。
A & B 表示对象同时是 A 和 B 的实例。
对交集类型可以访问两个类型的所有成员。
A | B 表示对象可能是 A 或 B 类型。
只能访问它们的共同父类型成员,额外成员要通过模式匹配访问。
并集类型与交集类型让 Scala 类型系统满足数学上的“格”结构。
用于“混入行为用的特质”,不希望它们出现在推断类型中。
用 transparent 修饰后,编译器不再推断为这些特质的交集类型。
仍可显式使用这些特质作为类型。
富包装类 = 通过隐式转换(或隐式类)给原类型“增强方法”的包装类
比如 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 的 String(java.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 AsssetionErrorfor 琛ㄨ揪寮?
参数 => 结果
(参数: 类型) => 结果 // 可省略括号,可省略类型,可用下划线代替变量
(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 + 1scala 的控制流结构基本都可以返回值
没有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 // 兜底
}- 顺序执行:从上到下尝试,匹配到第一个后停止(不会贯穿/Fall through)。
- 表达式性质:
match是表达式,会返回结果值。 - MatchError:如果没有匹配到任何 case,抛出
scala.MatchError异常。
这是模式匹配的核心,共有 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。 |
编译器根据首字母大小写区分变量模式和常量模式:
- 小写开头(如
pi):被视为变量模式(会匹配任何值并赋值给pi)。 - 大写开头(如
Pi):被视为常量模式(引用已有的常量Pi进行比较)。 - 强制指定:如果常量是小写(如
this.pi或局部变量),需使用反引号pi或限定名obj.pi来强制作为常量匹配。
在模式之后添加 if 进行逻辑判断。
鐗硅川
case BinOp("+", x, y) if x == y => // 仅当 x == y 时匹配用于限制类的继承结构,辅助编译器进行完备性检查。
- 语法:在父类前加
sealed。 - 限制:所有子类必须在同一个文件中定义。
- 作用:若
match未覆盖所有子类,编译器会发出警告。 - 压制警告:
(selector: @unchecked) match { ... }。
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
}- 原理:编译器将其翻译为
PartialFunction,包含apply和isDefinedAt方法。
閫嗗彉
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表示调用主构造函数仅在定义所在的类 / 对象内部可访问。
Scala 比 Java 更严格:外部类不能访问内部类的 private。
private[X]:可在“直到 X 的范围”访问,其中 X 可是包、类、对象。
Scala 比 Java 更严格:只能被子类访问(同包不行)。
protected[X]:可在子类 + X 范围内访问。
默认,不写修饰符即为 public。
public var 自动生成 getter 和 setter。getter:
hour,setter:hour_=(用于处理赋值)可以重写做检查和灵活设置读取
private[X] / protected[X] 中的 X 可以是:
包(实现包内可见)
类或对象(实现“内部类私有”等效果)
外层包的子包也能访问 private[外层包]
类可以访问伴生对象的私有成员
伴生对象可以访问类的私有成员
protected 对伴生对象没意义(伴生对象不能被继承)
鍖垮悕涓婁笅鏂囧弬鏁?
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关键字(如Var("x"))。 - 字段访问:参数自动成为公开的
val字段。 - 常用方法:自动实现
toString(结构化打印)、hashCode、equals(基于内容的结构化比较)。 - Copy 方法:
obj.copy(param = newValue),用于复制并修改部分属性。
工厂对象包含创建其他对象的方法,使用方用这些工厂方法来构建对象,而不是直接用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 特质可以写:
- 方法(已实现方法和抽象方法)
- 字段(变量)和 状态(var)和 抽象变量
- 构造代码
- 参数,如果父类已经给特质传了参数,子类不能再传
它解决了多重继承的问题。在典型的菱形调用情况下,如果要调用最开始的父类方法,那么特质会把所有父类和特质排成一条从左到右的队列(线性化),然后 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.Innertrait 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).tailOptionextension (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)
)它等价于:
- 一个
sealed class Direction - 四个
val实例 - 一个伴生对象
- 自动生成
values,枚举类的所有枚举值组成一个数组 - 自动生成
valueOf,字符串转枚举 - 自动生成
ordinal,编号
与 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 做的事情:
- 先算出 result
- 把 result 传给函数
- 如果函数返回 true → 正常返回
- 如果 false → 抛 AssertionError
要先new一个异常对象,然后throw
throw new RuntimeException("n must be even")
try
xxxxx
catch
case xx: 异常类型
case ...
finally // 无论是否异常,都会执行
xxxxtry-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
}
}