前言
Scala is yet another programming language which supports common programming paradigms. We chose to use it for several reasons:
Scala是一种支持常见编程范例的编程语言。我们选择使用它有几个原因:
- 它能更好的嵌入领域专用语言(DSL)。
- 它拥有强大而优雅的库,包含各种数据集合。
- 它具有严格的类型系统,有助于在开发周期的早期(编译时)捕获大量错误。
- 它具有强大的函数式编程方式。
- Chisel更好发音。
之后我们讨论Chisel时,以上这些要点都会慢慢显现出来。不过现在,我们将专注于阅读和编写基本的Scala代码。
变量和常量 - var 和 val
创建变量和常量的语句前面分别带有关键字var和val。通常的做法是尽可能使用val。这是为了防止之后给变量错误地重新赋值,从而使代码难以阅读。
var numberOfKittens = 6
val kittensPerHouse = 101
val alphabet = "abcdefghijklmnopqrstuvwxyz"
var done = false
numberOfKittens: Int = 6
kittensPerHouse: Int = 101
alphabet: String = "abcdefghijklmnopqrstuvwxyz"
done: Boolean = false
首先要注意的是,与Java和C不同,Scala语句结尾通常不需要分号。当有换行符时,Scala会自动判断是否语句结束。例如,当行上的最后一个符号是操作符的话,Scala通常可以判断单个语句是否分布在多行中。需要分号的唯一情况是您希望将多个语句放在一行上的时候。
上面的常量和变量很清楚。两个变量var
可以被重新赋值,而两个常量val
在被创建之后就是不可变的。
numberOfKittens += 1
// kittensPerHouse = kittensPerHouse * 2 // 这一句会出错; 常量kittensPerHouse不可更改
println(alphabet)
done = true
abcdefghijklmnopqrstuvwxyz
条件语句
Scala中的条件语句与其他编程语言类似。
// 一个简单的条件语句 (这是注释)
if (numberOfKittens > kittensPerHouse) {
println("Too many kittens!!!")
}
// 如果只有一行语句的话大括号不是必须的。但是,Scala代码规范
// 希望只有在"else"部分出现的时候才省略大括号。
// (最好不要像下面这样...)
if (numberOfKittens > kittensPerHouse)
println("Too many kittens!!!")
// 下面的if语句包含else部分,所以可以在这里省略大括号
if (done)
println("we are done")
else
numberOfKittens += 1
// 下面是“else if”部分
// 代码规范上,因为代码块超过一行,所以都使用了大括号
if (done) {
println("we are done")
}
else if (numberOfKittens < kittensPerHouse) {
println("more kittens!")
numberOfKittens += 1
}
else {
done = true
}
we are done
we are done
但在Scala中,“if
”语句会返回值。它的值由所选分支的最后一行语句决定。这非常强大,特别是在函数中或类中初始化值的时候。类似下面这样:
val likelyCharactersSet = if (alphabet.length == 26)
"english"
else
"not english"
println(likelyCharactersSet)
english
likelyCharactersSet: String = "english"
我们创建了常量likelyCharactersSet,并且它的值是在运行时才被确定。
方法(Methods),函数(Functions)
方法使用关键字def
定义。在这里,我们不予区分将它们也称为函数。函数的参数由逗号分隔,包含参数的名称,类型以及默认值(可选)。为清楚起见,应指定返回类型。
没有任何参数的Scala函数定义后面不需要空括号。这使得类的成员变量要改成函数,并来自于计算结果的情况下更加容易。按照惯例,不改变任何东西的无参数的函数(调用它们不会改变任何东西,它们只是返回一个值)不使用括号;而会给这个类带来改变的函数(也许它们会改变类里面的变量或打印出来东西)要使用括号。
简单的声明(Declarations)
// 简单的乘以一个系数, 例如,times2(3) 返回 6
// 代码块只有一行,所以省略了大括号
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
}
defined function times2
defined function distance
函数的重载
重载意味着可以使用相同的函数名称,而参数及其类型不同,这时候编译器会自动确定应该调用哪个版本的函数。但是应该尽量避免重载函数。
//In
// 函数重载
def times2(x: Int): Int = 2 * x
def times2(x: String): Int = 2 * x.toInt
times2(5)
times2("7")
//Out
defined function times2
defined function times2
res5_2: Int = 10
res5_3: Int = 14
递归和嵌套函数调用
大括号定义代码的范围。在一个函数的范围内可以存在递归或嵌套调用。在某个范围内定义的函数只能在该范围内访问。
//In
/** 打印一个由“X”组成的三角形
* (这是另一种注释的方式)
*/
def asciiTriangle(rows: Int) {
// 这里很有趣:将字符串"X"乘以一个整数会得到由许多重复"X"构成的字符串
def printRow(columns: Int): Unit = println("X" * columns)
if(rows > 0) {
printRow(rows)
asciiTriangle(rows - 1) // 这里是递归
}
}
// printRow(1) // 这样调用会出错,因为printRow不是定义在当前作用域中
asciiTriangle(6)
XXXXXX
XXXXX
XXXX
XXX
XX
X
//Out
defined function asciiTriangle
列表(Lists)
Scala实现了各种聚合或序列对象。列表list与数组array非常类似,但支持更多访问或追加元素的操作。
//In
val x = 7
val y = 14
val list1 = List(1, 2, 3)
val list2 = x :: y :: y :: Nil // 另一种列表的构造方法
val list3 = list1 ++ list2 // 连接两个列表
val m = list2.length
val s = list2.size
val headOfList = list1.head // 取列表的第一个元素
val restOfList = list1.tail // 得到去掉第一个元素的新的列表
val third = list1(2) // 列表的第三个元素(下标从0开始)
//Out
x: Int = 7
y: Int = 14
list1: List[Int] = List(1, 2, 3)
list2: List[Int] = List(7, 14, 14)
list3: List[Int] = List(1, 2, 3, 7, 14, 14)
m: Int = 3
s: Int = 3
headOfList: Int = 1
restOfList: List[Int] = List(2, 3)
third: Int = 3
for
语句
Scala有for
语句,完全类似于别的语言,可以用来进行遍历。
for (i <- 0 to 7) { print(i + " ") }
println()
0 1 2 3 4 5 6 7
如果上面用的是until
而不是to
的话,遍历就是从0到6(不包含7)。
for (i <- 0 until 7) { print(i + " ") }
println()
0 1 2 3 4 5 6
在for
语句加入by
表示步长。下面的代码会打印出从0到10之间的偶数。
for(i <- 0 to 10 by 2) { print(i + " ") }
println()
0 2 4 6 8 10
如果你有一个集合并想要访问所有元素,你可以使用for
来作为迭代器(iterator),类似于Java或者Python中一样。下面的代码里,我们列出4个随机整数,然后对它们求和。
val randomList = List(scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt())
var listSum = 0
for (value <- randomList) {
listSum += value
}
println("sum is " + listSum)
sum is -381288124
randomList: List[Int] = List(-348678184, 2118016949, 2118650181, 25690226)
listSum: Int = -381288124
Scala中的for
可以非常直观地用于遍历。但是还有一类称为comprehensions的函数,在许多场合下使用起来更为方便,例如可以更容易地对数组元素求和。这些将在后面的章节中予以介绍。
怎样读Scala代码
能够阅读Scala代码并理解常见的命名约定,设计模式和最佳实践是成为有效的Chisel程序员重要的一步。代码的重用是Chisel的优势之一,但这需要阅读并理解别人的代码。有效地解析别人的代码也可以更容易地寻求帮助,例如利用StackOverflow等资源。
接下来这一节会展示一些常见的代码模式。
包(Packages)和导入(Imports)
package mytools
class Tool1 { ... }
上面定义了一个包(Packages,当需要在别的代码中导入的时候,如下:
import mytools.Tool1
注意:包的名字必须和路径的层级一致。这不是强制的要求,但如果不遵守的话,可能会产生一些难以诊断的问题。按照约定,包的名称应该是小写的,不包含下划线之类的分隔符。这样的名称有时会变得阅读很困难,一种方法是添加另一层层次结构,例如package good.tools
。尽你所能遵循这些约定吧,但是Chisel本身也存在一些不符合这些规则的情况。
如上所示,import
语句用来告诉编译器您需要使用其他的库。在Chisel编程时常见导入如下面所示:
import chisel3._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
第一行导入的是chisel3包中的所有类和方法;下划线在这里用作通配符。第二行表示从chisel3.iotesters包中导入指定的类。
Scala是一个面向对象的编程语言
Scala是一个面向对象的编程语言,了解这一点很重要,这也是Scala和Chisel的一个很大的优势。
- 变量是对象。
- 运用
val
声明的常量也是对象。
- 甚至literal也是对象(例如,1,2,3,字符串等)。
- 函数本身也是对象。这之后会详细说明。
- 对象(Object)是类(class)的实例。
- 事实上,面向对象中的对象(object) 在Scala中被称为实例。
- 在定义一个类的时候,需要指定:
- 类中的数据(
val
, var
)
- 类的操作,称为方法(method)或者函数(function)。这些方法可以通过该类的实例来调用。
- 类可以从其他类继承。
- 被继承的类叫做父类,继承的类叫做子类。
- 在这种情况下,子类继承父类的所有数据成员和方法。
- 子类也可以扩展或重载从父类继承的这些方法。
- 类也可以继承自特征(traits)。Traits可以被看作是轻量级的类,它可以允许多继承。
- (Singleton)Object对象在Scala中是一个特殊的类。
- 它们不是上面所说的对象,上面那些我们称之为实例。
我们接下来会看怎么创建对象:
类的例子
下面是一个创建Scala类的例子:
// WrapCounter从0开始计数,一直到最大值然后归零
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
}
println(s"counter created with max value $max")
}
defined class WrapCounter
在上面的代码中:
-
class WrapCounter
--这是类WrapCounter的定义.
-
(counterBits: Int)
-- 创建一个WrapCounter实例需要一个整数类型的参数,名称为counterBits
。
- 大括号 ({}) 里面是这个类的代码块。大部分的类在这里面定义变量,常量和方法。
-
val max: Long =
-- 这个类包含一个叫max的成员常量, 类型是Long
,并且在创建类的实例时被初始化。
-
(1 << counterBits) - 1
计算counterBits这么多比特所能表示的最大值。max在这里由val
创建,所以它是常量,之后不能被改变。
- 变量 counter 在这里被创建出来,并且赋初值为 0L , “L” 在这里表示0是一个长整型,所以编译器也会推定 counter 为长整型。
-
max 和 counter通常被称为该类的 成员变量(或常量)。
- 该类定义了一个叫inc的方法,该方法不需要参数并且返回一个长整型 Long 的值。
- 方法 inc 包含如下代码:
-
counter = counter + 1
递增 counter.
-
if (counter > max) { counter = 0 }
检查counter是否大于最大值max,如果大于的话,重置成0。
-
counter
-- 最后这一行很重要
- 一个代码块的最后一行被认为是该代码块的返回值,调用者可以使用或忽略该返回值。
- 这是一个通用的办法;例如,在
if
和else
语句中,真假分支都可以是代码块,所以if...else
语句也可以返回值:例如val result = if (10 * 10 > 90) 'greater' else 'lesser'
的返回值就是'greater'。
- 所以在这里,方法inc的返回值就是counter。
-
println(s'counter created with max value $max')
在标准输出stdout中打印出一个字符串。由于println直接位于定义类的代码块中,所以它会在每一次创建该类的实例时都会被运行。
- 这里打印的字符串是一个被解析(interpolated)的 字符串。
- 在引号前面的 s 表示这是一个被解析的字符串。
- 被解析的字符串会在运行的时候才被处理。
- 字符串中的 $max 会被变量max的值所替换。
- 如果$ 后面跟的是任意Scala代码块的话,这个代码块可以包含任何Scala语句。
- 例如,
println(s'doubled max is ${max + max}')
.
- 这个代码块的返回值将会替换掉字符串中的
${...}
。
- 如果返回值不是字符串,则会被转换为1;理论上scala中几乎每个类型都可以隐式地转换为字符串)。
- 除非您正在调试,否则应该避免类似这样每次创建类的实例时都要打印的内容,以避免标准输出显示了太多东西。
创建类的实例
让我们使用上面的例子来创建一个类的实例。 Scala实例是通过关键字new
来创建的。
val x = new WrapCounter(2)
counter created with max value 3
x: WrapCounter = ammonite.$sess.cmd12$Helper$WrapCounter@8833366
您可能也会看到不使用关键字new
来创建的实例,例如val y = WrapCounter(6)
。
这样的情况经常发生,特别值得注意,但需要使用companion对象,稍后会对此进行详细说明。
下面的代码会使用刚刚创建出来的实例(试试运行两次下面的代码)。
x.inc() // 递增计数器
// 实例x的成员变量对外部可见(除非它们被声明为私有成员)
if(x.counter == x.max) {
println("counter is about to wrap")
}
x inc() // Scala允许函数调用的时候省略“.”。这对于嵌入的领域专用语言(DSL)很有用处
res14_0: Long = 1L
res14_2: Long = 2L
代码块
代码块由大括号分隔,可以包含零行或多行Scala代码。代码块的最后一行就是该代码块的返回值(返回值可以被调用者忽略)。没有任何代码的代码块会返回一个类似于null的特殊对象,名为Unit
。Scala中有非常多的代码块:例如类的定义,函数和方法的定义,也可以是if
语句的子句,或者构成for
语句主体,等等。
参数化的代码块
代码块也可以带参数。在定义类或者方法的时候,这些参数看起来与其他传统编程语言类似。在下面的例子中,c
和s
是代码块的参数:
// 只包含一条语句的代码块不需要大括号
def add1(c: Int): Int = c + 1
class RepeatString(s: String) {
val repeatedString = s + s
}
defined function add1
defined class RepeatString
重要:还有另一种方法可以参数化代码块。下面是例子:
val intList = List(1, 2, 3)
val stringList = intList.map { i =>
i.toString
}
intList: List[Int] = List(1, 2, 3)
stringList: List[String] = List("1", "2", "3")
代码块被传递给List类的map
方法。map
方法需要一个代码块的参数,从而为列表的每个成员调用该代码块。这里传入的代码块的意思是将元素转换为字符串表示。Scala在许多地方都可以用类似的语法,您可能在不同的地方以不同的方式看到它。这种类型的代码块被称为匿名函数,稍后的章节中会介绍有关匿名函数更详细的信息。
这里的目标是帮助您在遇到时能够识别它们。随着更多地使用Scala,这样的代码会看起来更加舒适和熟悉。作者更倾向于使用特定的风格,这里也有个人喜好的问题。使用一行表示的代码(One-liner)会使代码看起来更紧凑,而使用复杂的代码块通常看起来更容易懂。为了使沟通和协作更容易,可以阅读。
用参数名进行调用,以及默认参数
看下面的方法定义:
def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = { ... }
调用方法时,可以同时使用参数名和参数值:
myMethod(count = 10, wrap = false, wrapValue = 23)
使用参数名传递参数的话可以在调用时不在乎参数的顺序:
myMethod(wrapValue = 23, wrap = false, count = 10)
对于经常调用的方法,参数的顺序可能很明显。但是对于不太常用的方法,特别是布尔参数,调用时使用参数名可以使您的代码更具可读性。如果方法需要传递的参数许多都是相同的类型,使用参数名也可以减少出错的可能性。类的参数也一样(通常只是类的构造函数的参数)。
当特定的参数有默认值的时候, 调用者只需要(通过参数名)传入那些没有默认值的参数即可。例如在上面,参数wrapValue
的默认值是24。 所以下面的代码:
myMethod(wrap = false, count = 10)
会认为wrapValue
参数已经传入了24.