Scala学习记录(1):语法入门

成元 2026-04-27 { 教程, 笔记 } [ Scala ]

前言

最近为了学习 Chisel,开始补一下 Scala 语法。

一开始我对 Scala 的印象其实很简单:语法上能看到一点 C/C++ 这类语言的影子,但整体写法又明显更灵活。真的上手之后才发现,它虽然也能写出很“传统”的代码,但很多地方的思路其实不太一样。比如 if 不只是流程控制语句,也可以直接当表达式返回值;函数可以像普通值一样传来传去;而且它默认鼓励你多用不可变数据。

这篇文章主要参考 Chisel Bootcamp 的 1_intro_to_scala.ipynb,把其中比较基础、后续写 Chisel 时又很常见的 Scala 语法整理一下。本文重点放在“语法入门”上,所以会尽量先把语法现象讲清楚,再配一些简单例子。

基础语法

这一部分先整理最基础、最先会碰到的语法:语句怎么结束、变量怎么定义、条件判断怎么写。

语句与分号

首先需要注意的是,与 C/C++ 不同,Scala 通常不需要在末尾添加分号。

Scala 会在换行时自动推断语句结束。例如下面这种写法是完全正常的:

val a = 1
val b = 2
val c = a + b

只有在你想把多条语句写到同一行时,才需要分号:

val a = 1; val b = 2

所以对日常开发来说,可以先把 Scala 当成一种“默认不写分号”的语言来看待。

变量与常量

Scala 里最常见的两个关键字是 varval

示例:

var numberOfKittens = 6
val kittensPerHouse = 101
val alphabet = "abcdefghijklmnopqrstuvwxyz"
var done = false

其中 numberOfKittensdone 后面可以重新赋值,而 kittensPerHousealphabet 一旦创建就不能改了。

numberOfKittens += 1
done = true

// kittensPerHouse = 202
// 这行会编译失败,因为 val 不能被重新赋值

从风格上说,Scala 更鼓励优先使用 val,只有确实需要修改时再用 var。这样写的好处很直接:变量状态更少,代码更不容易绕晕自己。

另外 Scala 也支持类型推断,所以很多时候不用手写类型:

val name = "Scala"
val version = 3

编译器会自动推断 nameStringversionInt。当然你也可以手动标注:

val name: String = "Scala"

条件判断

基础的 if / else 用法和其他语言差不多:

if (numberOfKittens > kittensPerHouse) {
  println("Too many kittens!")
}

也可以带 else if

if (done) {
  println("we are done")
} else if (numberOfKittens < kittensPerHouse) {
  println("more kittens!")
  numberOfKittens += 1
} else {
  done = true
}

如果分支里只有一行语句,也可以省略大括号:

if (done)
  println("we are done")
else
  numberOfKittens += 1

不过比起这些表面语法,更值得记住的是:Scala 里的 if 是有返回值的

例如:

val likelyCharactersSet =
  if (alphabet.length == 26) "english"
  else "not english"

这里 if 表达式的值,就是最终被赋给 likelyCharactersSet 的值。

这个特性在 Scala 里非常常见。很多时候它能帮你少写一个临时变量,也让代码更像“根据条件选择一个值”,而不是“先声明,再分支赋值”。

函数与代码块

Scala 的函数、代码块和表达式风格联系得非常紧。很多时候你会觉得自己不是在“按步骤写程序”,而是在“拼出一个结果”。这一部分把这些零散语法放到一起看,会顺很多。

函数定义:def

Scala 使用 def 定义函数:

def times2(x: Int): Int = 2 * x

这个定义可以拆开看:

复杂一点的函数可以写成代码块形式:

def distance(x: Int, y: Int, returnPositive: Boolean): Int = {
  val xy = x * y
  if (returnPositive) xy.abs else -xy.abs
}

这里最后一行 if (returnPositive) ... 的结果会自动作为整个函数的返回值。也就是说,Scala 很多代码块默认“最后一行就是返回值”

无参函数

Scala 里没有参数的函数不一定非得写空括号:

def currentValue: Int = 42
def printValue(): Unit = println(currentValue)

一般可以先这样理解这条习惯:

这里的“副作用”可以简单理解成打印、修改状态、读写文件之类会对外部产生影响的行为。

重载

Scala 也支持函数重载:

def times2(x: Int): Int = 2 * x
def times2(x: String): Int = 2 * x.toInt

调用时会根据参数类型选择对应版本:

times2(5)
times2("7")

不过重载虽然能用,但如果一个函数被你重载得太花,读代码的人通常不会很开心。

递归与嵌套函数

函数内部还可以继续定义函数:

def asciiTriangle(rows: Int): Unit = {
  def printRow(columns: Int): Unit = println("X" * columns)

  if (rows > 0) {
    printRow(rows)
    asciiTriangle(rows - 1)
  }
}

这里的 printRow 就是定义在 asciiTriangle 内部的局部函数,只能在当前作用域里使用。

这段代码还顺便展示了递归。asciiTriangle(rows - 1) 会不断调用自己,直到 rows 变成 0 为止。

代码块

Scala 里大括号 {} 的存在感很强,因为它不只是“包住几行代码”这么简单。

一个代码块通常有两个关键点:

比如:

val result = {
  val a = 3
  val b = 4
  a + b
}

最终 result 的值就是 7

其实前面看到的很多东西,本质上都和代码块有关:

所以理解“最后一行会作为结果”这件事,对读 Scala 很重要。

匿名函数

后面如果接触 Chisel 或更偏函数式一点的 Scala 代码,匿名函数会非常常见。

例如:

val intList = List(1, 2, 3)
val stringList = intList.map { i =>
  i.toString
}

这里的 { i => i.toString } 就是一个匿名函数。

可以把它理解成:

所以上面这段代码执行后,stringList 会变成:

List("1", "2", "3")

这类写法第一次看会觉得有点抽象,但适应之后会发现它很适合描述“对一组数据逐个做同样的处理”。

命名参数与默认参数

Scala 支持在调用函数时写出参数名,这一点我感觉挺实用,特别是参数一多、或者出现多个布尔参数的时候,可读性会好不少。

例如定义一个函数:

def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = {
  println(count, wrap, wrapValue)
}

调用时可以这样写:

myMethod(count = 10, wrap = false, wrapValue = 23)

甚至可以交换顺序:

myMethod(wrapValue = 23, wrap = false, count = 10)

因为 wrapValue 有默认值,所以也可以省略:

myMethod(wrap = false, count = 10)

这时 Scala 会自动把 wrapValue 当成 24

这个语法的好处在于:当函数参数很多时,调用点本身就像一份说明文档。你不用倒回去数“第三个 false 到底是什么意思”。

集合与遍历

Scala 的集合库很常用,后面读 Chisel 代码时也会不断看到各种集合操作。这里先记最基础的 Listfor

List

Scala 自带很丰富的集合库,List 是最常见的顺序集合之一。

创建列表:

val list1 = List(1, 2, 3)
val x = 7
val y = 14
val list2 = x :: y :: y :: Nil

其中:

所以:

val list2 = 7 :: 14 :: 14 :: Nil

本质上就是构造出一个包含三个元素的列表。

列表常见操作:

val list3 = list1 ++ list2
val headOfList = list1.head
val restOfList = list1.tail
val third = list1(2)

它们分别表示:

如果你后面要看 Chisel 代码,那 ListSeqMap 这些集合写法基本迟早都会见到,所以这里最好先熟悉一下。

for 循环

Scala 也有 for

for (i <- 0 to 7) {
  print(i + " ")
}

这里的 0 to 7 表示从 0 遍历到 7右边界会包含进去

如果不想包含右边界,可以用 until

for (i <- 0 until 7) {
  print(i + " ")
}

这个循环实际会遍历 06

也可以指定步长:

for (i <- 0 to 10 by 2) {
  print(i + " ")
}

这会输出 0 2 4 6 8 10

除了遍历数字范围,也可以直接遍历集合:

val randomList = List(3, 5, 8, 13)
var listSum = 0

for (value <- randomList) {
  listSum += value
}

这和 C 里按下标访问数组不太一样,更像“把集合中的元素依次取出来使用”。

面向对象

前面的内容更多是在说“Scala 语句怎么写”。这一部分则是从面向对象的角度整理 Scala。这样把相关概念放在一处,读起来会比零散记笔记顺很多。

值、对象与实例

Scala 是一门面向对象语言,所以类语法也很重要。

不过这里的“面向对象”不能只理解成“能写 class”。更准确地说,Scala 里很多你平时看起来像“普通值”的东西,本质上也都可以当作对象来看

例如下面这些东西,在 Scala 的语境里都可以看作对象:

也就是说,下面这些都不只是“裸数据”:

val a = 3
val name = "scala"
val flag = true

它们都带有自己的类型,也都能调用相应的方法。例如:

val name = "scala"
println(name.toUpperCase)
println(a.toString)

如果你之前主要写 C/C++,这里会有一点不太一样。C 里整数更像“原始值”;而在 Scala 里,很多值都统一放进了对象这套思路里,所以各种操作看起来会更一致。

类与实例

在面向对象里,可以先记住一个最基本的关系:

所以平时说“创建一个对象”,很多时候其实就是“创建一个类的实例”。

来看一个简单例子:

class WrapCounter(counterBits: Int) {
  val max: Long = (1 << counterBits) - 1
  var counter = 0L

  def inc(): Long = {
    counter = counter + 1
    if (counter > max) {
      counter = 0
    }
    counter
  }
}

这个类可以这样理解:

也就是说,类主要描述两类东西

这和 C++ 里的类其实很像,只是 Scala 的写法通常更紧凑一些。

创建实例时通常要用 new

val x = new WrapCounter(2)

然后就可以调用方法:

x.inc()

这里的 x 就是 WrapCounter 这个类的一个实例。x 内部有自己的 maxcounter,也可以调用自己的 inc() 方法。

有一个 Scala 里经常出现、第一次看可能有点怪的写法是:

x inc()

也就是点号有时可以省略。这种写法在一些 DSL 里会显得比较自然,不过对入门来说,先老老实实写 x.inc() 会更清楚。

继承与 trait

Scala 里的类也可以继承别的类:

class Counter {
  var value = 0

  def inc(): Unit = {
    value += 1
  }
}

class FastCounter extends Counter {
  override def inc(): Unit = {
    value += 2
  }
}

这里:

对你当前这个阶段来说,只要先记住:extends 表示“在已有类型基础上继续扩展” 就够了。后面看别人封装工具类、基础组件时会经常遇到。

Scala 里还有一个很常见的概念叫 trait

可以先把它粗略理解成“比类更轻一点、常用来复用一部分能力的结构”:

trait Printable {
  def printName(name: String): Unit = {
    println(name)
  }
}

class User extends Printable

这样 User 就拥有了 printName() 这个方法。

如果只从入门角度理解,我觉得可以先这样记:

后面看 Scala 或 Chisel 代码时,trait 的出场率很高,所以至少要先认得这个关键字。

object

Scala 里还有一个容易和“对象”这个概念搞混的语法,就是 object

这里一定要分清楚:

例如:

object MathTools {
  def add(a: Int, b: Int): Int = a + b
}

使用时不需要 new

MathTools.add(1, 2)

因为 object 本身就只会有一份,很适合放一些工具函数、常量,或者某些全局只需要一个实例的逻辑。

如果你有 C/C++ 背景,也可以暂时把它类比成“带命名空间效果的一组静态成员”,虽然这个类比不完全准确,但入门时够用了。

类体、主构造器与初始化

Scala 类体中不只是能写字段和方法,也能直接写普通语句:

class WrapCounter(counterBits: Int) {
  val max: Long = (1 << counterBits) - 1
  println(s"counter created with max value $max")
}

每次创建对象时,这行 println 都会执行。

如果从 C++ 的角度去理解,可以把 类体里那些“不是字段定义、也不是方法定义”的可执行语句,看成有点像构造对象时自动执行的初始化逻辑。

不过要注意,整个类代码块本身不能直接等同于“初始化方法”,因为它同时还承担了“定义成员变量和成员方法”的作用。更准确地说,class Xxx(...) { ... } 这整块内容可以理解成“类定义 + 主构造器主体”。

顺便这里又出现了一个常见语法:字符串插值

val name = "Scala"
println(s"hello, $name")
println(s"1 + 2 = ${1 + 2}")

前面的 s 表示这是插值字符串,$name 会被替换成变量值,${...} 里则可以放表达式。

代码组织:packageimport

这一部分不算最核心的语法,但实际写代码时肯定会见到。

例如:

package mytools

class Tool1 {
}

如果别的文件要使用这个类,可以导入它:

import mytools.Tool1

也可以用通配形式导入整个包里的内容:

import chisel3._

这里的 _ 很像其他语言里的 *

如果后面开始写 Chisel,这类导入语句几乎是天天见的,所以即使现在不深究,也要先认得。

总结

对刚开始接触 Scala 的人来说,我觉得它最容易让人不适应的地方不是“太难”,而是很多地方虽然都能类比到别的语言,但细节思路经常不一样

这篇文章先记录了后面最容易反复见到的几类基础语法:

如果只是为了后面继续学 Chisel,我感觉先掌握到这个程度已经够用了。至少再看到别人的 Scala 代码时,不会只觉得“每一行都认识,但连起来不知道在干什么”。

下一篇大概就可以开始继续记录更贴近 Chisel 使用场景的内容了。

资料来源

本文主要参考以下资料: