以下内容均摘自:https://blog.csdn.net/qq_34291505
一、变量定义与基本类型 1、Scala基本数据类型Scala是静态语言,在编译期间会检查每个对象的类型。对于类型不匹配的非法操作,在编译时就能被发现。对于动态语言而言,这种非法操作需要等到运行时才能被发现,此时可能造成严重错误。所以,静态语言相比诸如Python这样的动态语言在某些方面是有优势的。对于Chisel而言,我们就需要这种优势。因为Chisel需要编译成Verilog,我们不能产生非法的Verilog语句并且等到模块运行时才去发现它。
Scala标准库定义了一些基本类型,如下表所示。除了“String”类型是属于java.lang包之外,其余都在Scala的包里。
Byte8-bit有符号整数,补码表示,范围是 -2^{7} 到 2^{7}-1Short16-bit有符号整数,补码表示,范围是 -2^{15} 到 2^{15}-1Int32-bit有符号整数,补码表示,范围是 -2^{31} 到 2^{31}-1Long64-bit有符号整数,补码表示,范围是 -2^{63} 到 2^{63}-1Char16-bit字符,Unicode编码,范围是 0 到 2^{16}-1String字符串Float32-bit单精度浮点数,符合IEEE 754标准Double64-bit双精度浮点数,符合IEEE 754标准Boolean布尔值,其值为true或者false事实上,在定义变量时,应该指明变量的类型,只不过Scala的编译器具有自动推断类型的功能,可以根据赋给变量的对象的类型,来自动推断出变量的类型。如果要显式声明变量的类型,或者无法推断时,则只需在变量名后面加上一个冒号“ : ”,然后在等号与冒号之间写出类型名即可。
2、字符与字符串字面量字符字面量是以单引号’ ‘包起来的一个字符,采用Unicode编码。也可以用’\u编码号’的方式来构造一个字符,而且Unicode编码可以出现在代码的任何地方,甚至是名称命名。此外,还支持转义字符。例如:
scala> val a = 'A'
a: Char = A
scala> val b = '\u0041'
b: Char = A
scala> val c = '\u0042'
c: Char = B
scala> val \u0041\u0042 = 1
AB: Int = 1
scala> val d = '\\'
d: Char = \
字符串就是用双引号" “包起来的字符序列,长度任意,允许掺杂转义字符。此外,也可以用前后各三个双引号”"" “”"包起来,这样字符串里也能出现双引号,而且转义字符不会被解读。例如:
scala> val a = "\\\\\\"
a: String = \\\
scala> val b = """So long \u0041 String \\\'\"!"""
b: String = So long A String \\\'\"!
3、字符串插值
Scala包括了一个灵活的机制来支持字符串插值,这使得表达式可以被嵌入在字符串字面量中并被求值。第一种形式是s插值器,即在字符串的双引号前加一个s,形如s“…${表达式}…”。s插值器会对内嵌的每个表达式求值,对求值结果调用内置的toString方法,替换掉字面量中的表达式。从美元符号开始到首个非标识符字符(字母、数字、下划线和操作符的组合称为标识符,以及反引号对包起来的字符串)的部分会被当作表达式,如果有非标识符字符,就必须放在花括号里,且左花括号要紧跟美元符号。第二种形式是raw插值器,它与s插值器类似,只不过不识别转义字符。第三种形式是f插值器,允许给内嵌的表达式加上printf风格的指令,指令放在表达式之后并以百分号开始。指令语法来自java.util.Formatter。如:
scala> val name = "ABC"
name: String = ABC
scala> println(s"$name DEFG")
ABC DEFG
scala> s"Sum = ${1 + 10}"
res0: String = Sum = 11
scala> s"\\\\"
res1: String = \\
scala> raw"\\\\"
res2: String = \\\\
scala> printf(f"${math.Pi}%.5f")
3.14159
二、函数相关
1、函数字面量
函数式编程有两个主要思想,其中之一就是:函数是一等(first-class)的值。换句话说,一个函数的地位与一个Int值、一个String值等等,是一样的。既然一个Int值可以成为函数的参数、函数的返回值、定义在函数体里、存储在变量里,那么,作为地位相同的函数,也可以这样。你可以把一个函数当参数传递给另一个函数,也可以让一个函数返回一个函数,亦可以把函数赋给一个变量,又或者像定义一个值那样在函数里定义别的函数(即前述的嵌套函数)。就像写一个整数字面量“1”那样,Scala也可以定义函数的字面量。函数字面量是一种匿名函数的形式,它可以存储在变量里、成为函数参数或者当作函数返回值,其定义形式为:
(参数1: 参数1类型, 参数2: 参数2类型, …) => { 函数体 }
通常,函数字面量会赋给一个变量,这样就能通过“变量名(参数)”的形式来使用函数字面量。在参数类型可以被推断的情况下,可以省略类型,并且参数只有一个时,圆括号也可以省略。
函数字面量的形式可以更精简,即只保留函数体,并用下划线“_”作为占位符来代替参数。在参数类型不明确时,需要在下划线后面显式声明其类型。多个占位符代表多个参数,即第一个占位符是第一个参数,第二个占位符是第二个参数……因此不能重复使用某个参数。例如:
scala> val f = (_: Int) + (_: Int)
f: (Int, Int) => Int = $$Lambda$1072/1534177037@fb42c1c
scala> f(1, 2)
res0: Int = 3
无论是用“def”定义的函数,还是函数字面量,它们的函数体都可以把一个函数字面量作为一个返回结果,这样就成为了返回函数的函数;它们的参数变量的类型也可以是一个函数,这样调用时给的入参就可以是一个函数字面量。类型为函数的变量,其冒号后面的类型写法是“(参数1类型, 参数2类型,...) => 返回结果的类型”。
例如:
scala> val add = (x: Int) => { (y: Int) => x + y }
add: Int => (Int => Int) = $$Lambda$1192/1767705308@55456711
scala> add(1)(10)
res0: Int = 11
scala> def aFunc(f: Int => Int) = f(1) + 1
aFunc: (f: Int => Int)Int
scala> aFunc(x => x + 1)
res1: Int = 3
在第一个例子中,变量add被赋予了一个返回函数的函数字面量。在调用时,第一个括号里的“1”是传递给参数x,第二个括号里的“10”是传递给参数y。如果没有第二个括号,得到的就不是11,而是“(y: Int) => 1 + y”这个函数字面量。
在第二个例子中,函数aFunc的参数f是一个函数,并且该函数要求是一个入参为Int类型、返回结果也是Int类型的函数。在调用时,给出了函数字面量“x => x + 1”。这里没有显式声明x的类型,因为可以通过f的类型来推断出x必须是一个Int类型。在执行时,首先求值f(1),结合参数“1”和函数字面量,可以算出结果是2。那么,“f(1) + 1”就等于3了。
2、柯里化对大多数编程语言来说,函数只能有一个参数列表,但是列表里可以有若干个用逗号间隔的参数。Scala有一个独特的语法——柯里化,也就是一个函数可以有任意个参数列表。柯里化往往与另一个语法结合使用:当参数列表里只有一个参数时,在调用该函数时允许单个参数不用圆括号包起来,改用花括号也是可行的。这样,在自定义类库时,自定义方法就好像“if(...) {...}”、“while(...) {...}”、“for(...) {...}”
等内建控制结构一样,让人看上去以为是内建控制,丝毫看不出是自定义语法。例如:
scala> def add(x: Int, y: Int, z: Int) = x + y + z
add: (x: Int, y: Int, z: Int)Int
scala> add(1, 2, 3)
res0: Int = 6
scala> def addCurry(x: Int)(y: Int)(z: Int) = x + y + z
addCurry: (x: Int)(y: Int)(z: Int)Int
scala> addCurry(1)(2) {3}
res1: Int = 6
3、传名参数
第四点介绍了函数字面量如何作为函数的参数进行传递,以及如何表示类型为函数时参数的类型。如果某个函数的入参类型是一个无参函数,那么通常的类型表示法是“() => 函数的返回类型”。在调用这个函数时,给出的参数就必须写成形如“() => 函数体”这样的函数字面量。
为了让代码看起来更舒服,也为了让自定义控制结构更像内建结构,Scala又提供了一个特殊语法——传名参数。也就是类型是一个无参函数的函数入参,传名参数的类型表示法是“=> 函数的返回类型”,即相对常规表示法去掉了前面的空括号。在调用该函数时,传递进去的函数字面量则可以只写“函数体”,去掉了“() =>”。例如:
var assertionEnabled = false
// predicate是类型为无参函数的函数入参
def myAssert(predicate: () => Boolean) =
if(assertionEnabled && !predicate())
throw new AssertionError
// 常规版本的调用
myAssert(() => 5 > 3)
// 传名参数的用法,注意因为去掉了空括号,所以调用predicate时不能有括号
def byNameAssert(predicate: => Boolean) =
if(assertionEnabled && !predicate)
throw new AssertionError
// 传名参数版本的调用,看上去更自然
byNameAssert(5 > 3)
可以看到,传名参数使得代码更加简洁、自然,而常规写法则很别扭。事实上,predicate的类型可以改成Boolean,而不必是一个返回布尔值的函数,这样调用函数时与传名参数是一致的。例如:
// 使用布尔型参数的版本
def boolAssert(predicate: Boolean) =
if(assertionEnabled && !predicate)
throw new AssertionError
// 布尔型参数版本的调用
boolAssert(5 > 3)
尽管byNameAssert和boolAssert在调用形式上是一样的,但是两者的运行机制却不完全一样。如果给函数的实参是一个表达式,比如“5 > 3”这样的表达式,那么boolAssert在运行之前会先对表达式求值,然后把求得的值传递给函数去运行。而myAssert和byNameAssert则不会一开始就对表达式求值,它们是直接运行函数,直到函数调用入参时才会对表达式求值,也就是例子中的代码运行到“!predicate”时才会求“5 > 3”的值。
4、偏函数在Scala里,万物皆对象。函数是一等值,与整数、浮点数、字符串等等相同,所以函数也是一种对象。既然函数也是一个对象,那么必然属于某一种类型。为了标记函数的类型,Scala提供了一系列特质:Function0、Function1、Function2……Function22来表示参数为0、1、2……22个的函数。与元组很像,因此函数的参数最多只能有22个。当然也可以自定义含有更多参数的FunctionX,但是Scala标准库没有提供,也没有必要。
除此之外,还有一个特殊的函数特质:偏函数PartialFunction。偏函数的作用在于划分一个输入参数的可行域,在可行域内对入参执行一种操作,在可行域之外对入参执行其他操作。偏函数有两个抽象方法需要实现:apply和isDefinedAt。其中,isDefinedAt用于判断入参是否在可行域内,是的话就返回true,否则返回false;apply是偏函数的函数体,用于对入参执行操作。使用偏函数之前,应该先用isDefinedAt判断入参是否合法,否则可能会出现异常。
定义偏函数的一种简便方法就是使用case语句组。广义上讲,case语句就是一个偏函数,所以才可以用于模式匹配。一个case语句就是函数的一个入口,多个case语句就有多个入口,每个case语句又可以有自己的参数列表和函数体。例如:
val isInt1: PartialFunction[Any, String] = {
case x: Int => x + " is a Int."
}
// 相当于
val isInt2 = new PartialFunction[Any, String] {
def apply(x: Any) = x.asInstanceOf[Int] + " is a Int."
def isDefinedAt(x: Any) = x.isInstanceOf[Int]
}
注意apply方法可以隐式调用。x.isInstanceOf[T]判断x是不是T类型(及其超类)的对象,是的话就返回true。x.asInstanceOf[T]则把x转换成T类型的对象,如果不能转换则会报错。
偏函数PartialFunction[Any, Any]是Function1[Any, Any]的子特质,因为case语句只有一个参数。[Any, Any]中的第一个Any是输入参数的类型,第二个Any是返回结果的类型。如果确实需要输入多个参数,则可以用元组、列表或数组等把多个参数变成一个集合。
在用case语句定义偏函数时,前述的各种模式类型、模式守卫都可以使用。最后的通配模式可有可无,但是没有时,要保证运行不会出错。
上述代码运行如下:
scala> isInt1(1)
res0: String = 1 is a Int.
scala> isInt2(1)
res1: String = 1 is a Int.
scala> isInt1.isDefinedAt('1')
res2: Boolean = false
scala> isInt2.isDefinedAt('1')
res3: Boolean = false
scala> isInt1('1')
scala.MatchError: 1 (of class java.lang.Character)
at scala.PartialFunction$ $anon$1.apply(PartialFunction.scala:255)
at scala.PartialFunction$ $anon$1.apply(PartialFunction.scala:253)
at $anonfun$1.applyOrElse(:12)
at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:34)
... 28 elided
scala> isInt2('1')
java.lang.ClassCastException: java.lang.Character cannot be cast to java.lang.Integer
at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:101)
at $anon$1.apply(:13)
at $anon$1.apply(:12)
... 28 elided
三、类和对象相关
1、单例对象与伴生对象
在Scala里,除了用new可以构造一个对象,也可以用“object”开头定义一个对象。它类似于类的定义,只不过不能像类那样有参数,也没有构造方法。因此,不能用new来实例化一个object的定义,因为它已经是一个对象了。这种对象和用new实例化出来的对象没有什么区别,只不过new实例的对象是以类为蓝本构建的,并且数量没限制,而object定义的对象只能有这一个,故而得名“单例对象”。
如果某个单例对象和某个类同名,那么单例对象称为这个类的“伴生对象”,同样,类称为这个单例对象的“伴生类”。伴生类和伴生对象必须在同一个文件里,而且两者可以互访对方所有成员。在C++、Java等oop语言里,类内部可以定义静态变量。这些静态变量不属于任何一个用new实例化的对象,而是它们的公有部分。Scala追求纯粹的面向对象属性,即所有的事物都是类或对象,但是静态变量这种不属于类也不属于对象的事物显然违背了Scala的理念。所以,Scala的做法是把类内所有的静态变量从类里移除,转而集中定义在伴生对象里,让静态变量属于伴生对象这个独一无二的对象。
既然单例对象和new实例的对象一样,那么类内可以定义的代码,单例对象同样可以拥有。例如,单例对象里面可以定义字段和方法。Scala允许在类里定义别的类和单例对象,所以单例对象也可以包含别的类和单例对象的定义。因此,单例对象除了用作伴生对象,通常也可以用于打包某方面功能的函数系列成为一个工具集,或者包含主函数成为程序的入口。
“object”后面定义的单例对象名可以认为是这个单例对象的名称标签,因此可以通过句点符号访问单例对象的成员——“单例对象名.成员”,也可以赋给一个变量——“val 变量 = 单例对象名”,就像用new实例的对象那样。例如:
scala> class A { val a = 10 }
defined class A
scala> val x = new A
x: A = A@7e5831c4
scala> x.a
res0: Int = 10
scala> (new A).a
res1: Int = 10
scala> object B { val b = "a singleton object" }
defined object B
scala> B.b
res2: String = a singleton object
scala> val y = B
y: B.type = B$@4489b853
scala> y.b
res3: String = a singleton object
前面说过,定义一个类,就是定义了一种类型。从抽象层面讲,定义单例对象却并没有定义一种类型。实际上每个单例对象有自己独特的类型,即object.type。可以认为新类型出现了,只不过这个类型并不能用来归类某个对象集合,等同于没有定义新类型。即使是伴生对象也没有定义类型,而是由伴生类定义了同名的类型。后续章节将讲到,单例对象可以继承自超类或混入特质,这样它就能出现在需要超类对象的地方。例如下面的例子中,可以明确看到X.type和Y.type两种新类型出现,并且是不一样的:
scala> object X
defined object X
scala> object Y
defined object Y
scala> var x = X
x: X.type = X$@630bb67
scala> x = Y
:17: error: type mismatch;
found : Y.type
required: X.type
x = Y
^
2、工厂对象与工厂方法
如果定义一个方法专门用来构造某一个类的对象,那么这种方法就称为“工厂方法”。包含这些工厂方法集合的单例对象,也就叫“工厂对象” 。通常,工厂方法会定义在伴生对象里。尤其是当一系列类存在继承关系时,可以在基类的伴生对象里定义一系列对应的工厂方法。使用工厂方法的好处是可以不用直接使用new来实例化对象,改用方法调用,而且方法名可以是任意的,这样对外隐藏了类的实现细节。例如:
// students.scala
class Students(val name: String, var score: Int) {
def exam(s: Int) = score = s
override def toString = name + "'s score is " + score + "."
}
object Students {
def registerStu(name: String, score: Int) = new Students(name, score)
}
将文件students.scala编译后,并在解释器里用“import Students._”导入单例对象后,就能这样使用:
scala> import Students._
import Students._
scala> val stu = registerStu("Tim", 100)
stu: Students = Tim's score is 100.
3、apply方法
有一个特殊的方法名——apply,如果定义了这个方法,那么既可以显式调用——“对象.apply(参数)” ,也可以隐式调用——“对象(参数)”。隐式调用时,编译器会自动插入缺失的“.apply”。如果apply是无参方法,应该写出空括号,否则无法隐式调用。无论是类还是单例对象,都能定义这样的apply方法。
通常,在伴生对象里定义名为apply的工厂方法,就能通过“伴生对象名(参数)”来构造一个对象。也常常在类里定义一个与类相关的、具有特定行为的apply方法,让使用者可以隐式调用,进而隐藏相应的实现细节。例如:
// students2.scala
class Students2(val name: String, var score: Int) {
def apply(s: Int) = score = s
def display() = println("Current score is " + score + ".")
override def toString = name + "'s score is " + score + "."
}
object Students2 {
def apply(name: String, score: Int) = new Students2(name, score)
}
将文件students2.scala编译后,就能在解释器里这样使用:
scala> val stu2 = Students2("Jack", 60)
stu2: Students2 = Jack's score is 60.
scala> stu2(80)
scala> stu2.display
Current score is 80.
其中,“Students2(“Jack”, 60)”被翻译成“Students2.apply(“Jack”, 60)” ,也就是调用了伴生对象里的工厂方法,所以构造了一个Students2的对象并赋给变量stu2。“stu2(80)”被翻译成“stu2.apply(80)” ,也就是更新了字段score的数据。
4、主函数主函数是Scala程序唯一的入口,即程序是从主函数开始运行的。要提供这样的入口,则必须在某个单例对象里定义一个名为“main”的函数,而且该函数只有一个参数,类型为字符串数组Array[String],函数的返回类型是Unit。任何符合条件的单例对象都能成为程序的入口。例如:
// students2.scala
class Students2(val name: String, var score: Int) {
def apply(s: Int) = score = s
def display() = println("Current score is " + score + ".")
override def toString = name + "'s score is " + score + "."
}
object Students2 {
def apply(name: String, score: Int) = new Students2(name, score)
}
// main.scala
object Start {
def main(args: Array[String]) = {
try {
val score = args(1).toInt
val s = Students2(args(0), score)
println(s.toString)
} catch {
case ex: ArrayIndexOutOfBoundsException => println("Arguments are deficient!")
case ex: NumberFormatException => println("Second argument must be a Int!")
}
}
}
使用命令“scalac students2.scala main.scala”将两个文件编译后,就能用命令“scala Start 参数1 参数2”来运行程序。命令里的“Start”就是包含主函数的单例对象的名字,后面可以输入若干个用空格间隔的参数。这些参数被打包成字符串数组供主函数使用,也就是代码里的args(0)、args(1)。例如:
PS E:\Microsoft VS\Scala> scala Start Tom
Arguments are deficient!
PS E:\Microsoft VS\Scala> scala Start Tom aaa
Second argument must be a Int!
PS E:\Microsoft VS\Scala> scala Start Tom 100
Tom's score is 100.
主函数的一种简化写法是让单例对象混入“App”特质(特质在后续章节讲解),这样就只要在单例对象里编写主函数的函数体。例如:
// main2.scala
object Start2 extends App {
try {
var sum = 0
for(arg println("Arguments must be Int!")
}
}
将文件编译后,就可以如下使用:
PS E:\Microsoft VS\Scala> scala Start2 10 -8 20 AAA
Arguments must be Int!
PS E:\Microsoft VS\Scala> scala Start2 10 -8 20 8
sum = 30
四、操作符即方法
1、操作符在Scala里的解释
在诸如C++、Java等oop语言里,定义了像byte、short、int、char、float之类的基本类型,但是这些基本类型不属于面向对象的范畴。就好比C语言也有这些类型,但是C语言根本没有面向对象的概念。 比如只能说“1”是一个int类型的常量,却不能说它是一个int类型的对象。与之对应的,这些语言还定义了与基本类型相关的操作符。例如,有算术操作符加法“+”,它可以连接左、右两个操作数,然后算出相应的总和。
前面提到,Scala追求纯粹的面向对象,像这种不属于面向对象范畴的基本类型及其操作符都是有违宗旨的。那么,Scala如何实现这些基本类型呢?实际在Scala标准库里定义了“class Byte”、“class Short”、“class Char”、“class Int”、“class Long”、“class Float”、“class Double”、“class Boolean”和“class Unit”九种值类,只不过这些类是抽象的、不可继承的,因此不能通过“new Int”这种语句来构造一个Int对象,也不能编写它们的子类,它们的对象都是由字面量来表示。例如,整数字面量“1”就是一个Int的对象。在运行时,前八种值类会被转换成对应的Java基本类型。第九个Unit类对应Java的“void”类型,即表示空值,这样就能理解返回值类型为Unit的、有副作用的函数其实是空函数。Unit类的对象由一个空括号作为字面量来表示。
简而言之,Scala做到了真正的“万物皆对象”。
还有,与基本类型相关的操作符该如何处理呢?严格来讲,Scala并不存在操作符的概念,这些所谓的操作符,例如算术运算的加减乘除,逻辑运算的与或非,比较运算的大于小于等等,其实都是定义在“class Int”、“class Double”等类里的成员方法。也就是说,在Scala里,操作符即方法。例如,Int类定义了一个名为“+”的方法,那么表达式“1 + 2”的真正形式应该是“1.+(2)”。它的释义是:Int对象“1”调用了它的成员方法“+”,并把Int对象“2”当作参数传递给了该方法,最后这个方法会返回一个新的Int对象“3”。
推而广之,“操作符即方法”的概念不仅仅限于九种值类的操作符,Scala里任何类定义的成员方法都是操作符,而且方法调用都能写成操作符的形式:去掉句点符号,并且方法参数只有一个时可以省略圆括号。例如:
scala> class Students3(val name: String, var score: Int) {
| def exam(s: Int) = score = s
| def friends(n: String, s: Int) = println("My friend " + n + " gets " + s + ".")
| override def toString = name + "'s score is " + score + "."
| }
defined class Students3
scala> val stu3 = new Students3("Alice", 80)
stu3: Students3 = Alice's score is 80.
scala> stu3 exam 100
scala> stu3.score
res0: Int = 100
scala> stu3 friends ("Bob", 70)
My friend Bob gets 70.
2、三种操作符
- 前缀操作符
写在操作数前面的操作符称为前缀操作符,并且操作数只有一个。前缀操作符对应一个无参方法,操作数是调用该方法的对象。前缀操作符只有“+”、“-”、“!”和“~”四个,相对应的方法名分别是 “unary_+”、“unary_-”、“unary_!”和“unary_~”。如果自定义的方法名是 “unary_”加上这四个操作符之外的操作符,那么就不能写成前缀操作符的形式。假设定义了方法“unary_”,那么写成 “p”的形式让人误以为这是一个指针,实际Scala并不存在指针,因此只能写成“p.unary_”或后缀操作符“p unary_”的形式。例如:
scala> class MyInt(val x: Int) {
| def unary_! = -x
| def unary_* = x * 2
| }
defined class MyInt
scala> val mi = new MyInt(10)
mi: MyInt = MyInt@2aac87ab
scala> !mi
res0: Int = -10
scala> *mi
:12: error: not found: value *
*mi
^
:12: warning: postfix operator mi should be enabled
by making the implicit value scala.language.postfixOps visible.
This can be achieved by adding the import clause 'import scala.language.postfixOps'
or by setting the compiler option -language:postfixOps.
See the Scaladoc for value scala.language.postfixOps for a discussion
why the feature should be explicitly enabled.
*mi
^
scala> mi.unary_*
res2: Int = 20
- 中缀操作符
中缀操作符的左右两边都接收操作数,它对应普通的有参方法。两个操作数中的一个是调用该方法的对象,一个是传入该方法的参数,参数那一边没有数量限制,只是多个参数需要放在圆括号里。Scala规定,以冒号“ : ”结尾的操作符,其右操作数是调用该方法的对象,其余操作符都是把左操作数当调用该方法的对象。 例如:
scala> class MyInt2(val x: Int) {
| def +*(y: Int) = (x + y) * y
| def +:(y: Int) = x + y
| }
defined class MyInt2
scala> val mi2 = new MyInt2(10)
mi2: MyInt2 = MyInt2@216c6825
scala> mi2 +* 10
res7: Int = 200
scala> mi2 +: 10
:13: error: value +: is not a member of Int
mi2 +: 10
^
scala> 10 +: mi2
res9: Int = 20
对于系统打印函数“print”、“printf”和“println”,其实也是中缀操作符,不过左侧的操作数是调用对象——控制台Console,右侧是要打印的内容。例如:
scala> Console println "Hello, world!"
Hello, world!
- 后缀操作符
写在操作数后面的操作符称为后缀操作符,并且操作数只有一个,即调用该方法的对象。后缀操作符也对应一个无参方法,但是要注意方法名如果构成前缀操作符的条件,那么既可以写成前缀操作符,也可以把完整的方法名写成后缀操作符。例如:
scala> class MyInt3(val x: Int) {
| def display() = println("The value is " + x + ".")
| }
defined class MyInt3
scala> val mi3 = new MyInt3(10)
mi3: MyInt3 = MyInt3@2670435
scala> import scala.language.postfixOps
import scala.language.postfixOps
scala> mi3 display
The value is 10.
五、特质
1、什么是特质
因为Scala没有多重继承,为了提高代码复用率,故而创造了新的编程概念——特质。
特质是用关键字“trait”为开头来定义的,它与单例对象很像,两者都不能有入参。但是,单例对象天生就是具体的,特质天生就是抽象的,不过不需要用“abstract”来说明。所以,特质可以包含抽象成员,而单例对象却不行。另外,两者都不能用new来实例化,因为特质是抽象的,而单例对象已经是具体的对象。类、单例对象和特质三者一样,内部可以包含字段和方法,甚至包含其他类、单例对象、特质的定义。
特质可以被其它类、单例对象和特质“混入”。这里使用术语“混入”而不是“继承”,是因为特质在超类方法调用上采用线性化机制,与多重继承有很大的区别。其它方面,“混入”和“继承”其实是一样的。例如,某个类混入一个特质后,就包含了特质的所有公有成员,而且也可以用“override”来重写特质的成员。
Scala只允许继承自一个类,但是对特质的混入数量却没有限制,故而可用于替代多重继承语法。要混入一个特质,可以使用关键字“extends”。但如果“extends”已经被占用了,比如已经拿去继承一个类或混入一个特质,那么后续则通过关键字“with”来混入其他特质。例如:
scala> class A {
| val a = "Class A"
| }
defined class A
scala> trait B {
| val b = "Trait B"
| }
defined trait B
scala> trait C {
| def c = "Trait C"
| }
defined trait C
scala> object D extends A with B with C
defined object D
scala> D.a
res0: String = Class A
scala> D.b
res1: String = Trait B
scala> D.c
res2: String = Trait C
特质也定义了一个类型,而且类型为该特质的变量,可以指向混入该特质的对象。例如:
scala> trait A
defined trait A
scala> class B extends A
defined class B
scala> val x: A = new B
x: A = B@7cc1f72c
2、特质的层次
特质也可以继承自其他类,或混入任意个特质,这样该特质就是关键字“extends”引入的那个类/特质的子特质。如果没有继承和混入,那么这个特质就是AnyRef类的子特质。前面讲过AnyRef类是所有非值类和特质的超类。当某个类、单例对象或特质用关键字“extends”混入一个特质时,会隐式继承自这个特质的超类。也就是说,类/单例对象/特质的超类,都是由“extends”引入的类或特质决定的。
特质对混入有一个限制条件:那就是要混入该特质的类/单例对象/特质,它的超类必须是待混入特质的超类,或者是待混入特质的超类的子类。因为特质是多重继承的替代品,那就有“继承”的意思。既然是继承,混入特质的类/单例对象/特质的层次,就必须比待混入特质的层次要低。例如:
scala> class A
defined class A
scala> class B extends A
defined class B
scala> class C
defined class C
scala> trait D extends A
defined trait D
scala> trait E extends B
defined trait E
scala> class Test1 extends D
defined class Test1
scala> class Test2 extends A with D
defined class Test2
scala> class Test3 extends B with D
defined class Test3
scala> class Test4 extends C with D
:13: error: illegal inheritance; superclass C
is not a subclass of the superclass A
of the mixin trait D
class Test4 extends C with D
^
scala> class Test5 extends A with E
:13: error: illegal inheritance; superclass A
is not a subclass of the superclass B
of the mixin trait E
class Test5 extends A with E
上例中,类Test1直接混入特质D,这样隐式继承自D的超类——类A,所以合法。类Test2和Test3分别继承自类A和A的子类,所以也允许混入特质D。类Test4的超类是C,而C与A没有任何关系,所以非法。类Test5的超类是A,特质E的超类是B,尽管类A是类B的超类,这也仍然是非法的。从提示的错误信息也可以看出,混入特质的类/单例对象/特质,其超类必须是待混入特质的超类或超类的子类。
3、混入特质的简便方法如果想快速构造一个混入某些特质的实例,可以使用如下语法:
new Trait1 with Trait2 ... { definition }
这其实是定义了一个匿名类,这个匿名类混入了这些特质,并且花括号内是该匿名类的定义。然后使用new构造了这个匿名类的一个对象,其等效的代码就是:
class AnonymousClass extends Trait1 with Trait2 ... { definition }
new AnonymousClass
例如:
scala> trait T {
| val tt = "T__T"
| }
defined trait T
scala> trait X {
| val xx = "X__X"
| }
defined trait X
scala> val a = new T with X
a: T with X = $anon$1@4c1fed69
scala> a.tt
res0: String = T__T
scala> a.xx
res1: String = X__X
除此之外,还可以在最前面加上一个想要继承的超类:
new SuperClass with Trait1 with Trait2 ... { definition }
六、包-package
1、包 介绍
当代码过于庞大时,为了让整个系统层次分明,各个功能部分划分明显,常常需要把整体划分成若干独立的模块。与Java一样,Scala把代码以“包”的形式划分。
包是以关键字“package”
为开头来定义的。可以用花括号把包的范围包起来,这种风格类似C++和C#的命名空间,而且这种方法使得一个文件可以包含多个不同的包。也可以不用花括号标注范围,但包的声明必须在文件最前面,这样使得整个文件的内容都属于这个包,这种风格类似Java。对于包的命名方式,推荐使用Java的反转域名法,即“com.xxx.xxx”
的形式。
在包里,可以定义class、object和trait
(注意,意思就是不能直接定义变量或者函数),也可以定义别的package
。如果编译一个包文件,那么会在当前路径下生成一个与包名相同的文件夹,文件夹里是包内class、object和trait
编译后生成的文件,或者是包内层的包生成的更深一层文件夹。如果多个文件的顶层包的包名相同,那么编译后的文件会放在同一个文件夹内。也就是说,一个包的定义可以由多个文件的源代码组成。
2、包的层次和精确代码访问
因为包里还可以定义包,所以包也有层次结构。包不仅便于人们按模块阅读,同时也告诉编译器这些代码存在某些层次联系。像访问对象的成员一样,包也可以通过句点符号来按路径层次访问。如果包名中就出现了句点,那么编译器也会按层次编译。例如:
因为包里还可以定义包,所以包也有层次结构。包不仅便于人们按模块阅读,同时也告诉编译器这些代码存在某些层次联系。像访问对象的成员一样,包也可以通过句点符号来按路径层次访问。如果包名中就出现了句点,那么编译器也会按层次编译。例如:
package one.two
等效于:
package one
package two
这两种写法都会先编译出一个名为one的文件夹,然后在里面又编译出一个名为two的文件夹。如果一个包仅仅是包含了其他的包,没有额外的class、object和trait定义,那么建议写出第一种形式,这样内部代码省去了一次缩进。
Scala的包是嵌套的,而不像Java那样只是分级的。这体现在Java访问包内的内容必须从最顶层的包开始把全部路径写齐,而Scala则可以按照一定的规则书写更简短的形式。例如:
package bobsrockets {
package navigation {
class Navigator {
// 不需要写成bobsrockets.navigation.StarMap
val map = new StarMap
}
class StarMap
}
class Ship {
// 不需要写成bobsrockets.navigation.Navigator
val nav = new navigation.Navigator
}
package fleets {
class Fleet {
// 不需要写成bobsrockets.Ship
def addShip() = { new Ship }
}
}
}
第一,访问同一个包内的class、object和trait不需要增加路径前缀
。因为“new StarMap”和“class StarMap”都位于bobsrockets.navigation包内,所以这条代码能够通过编译。
第二,访问同一个包内更深一层的包所含的class、object和trait
,只需要写出那层更深的包。因为“class Ship”和“package navigation”都位于bobsrockets包内,所以要访问navigation包内的class、object和trait只需要增加“navigation.”,而不是完整的路径。
第三,当使用花括号显式表明包的作用范围时,本层包外所有可访问的class、object和trait在包内也可以直接访问
。
- 这里的包外,既可以是本层包外(也即上层包内),也可以是上层乃至上上层的包外,所以上层的东西基本上都可以直接访问到。参考上例的
def addShip() = { new Ship }
- 这里说的只是本层包外的
class、object和trait
可以直接访问,如果想访问本层包外的package
里的内容,那么至少要在想访问的本层包外的package
的前面加上这个package的父包
,可以参考“_root_”
例子中的val booster2 = new bobsrockets.launch.Booster2
和val booster3 = new _root_.launch.Booster3
; - 如果是包内的
package
,那么只需要带上该package
即可,参考val booster1 = new launch.Booster1
。
因为“package fleets”位于外层包bobsrockets,所以bobsrockets包内、fleets包外的所有class、object和trait可以直接访问,故而“new Ship”不需要完整路径也能通过编译。
以上规则在同一个文件内显式嵌套时可以生效。如果把包分散在多个文件内,并通过包名带句点来嵌套,则不会生效。例如下面的代码就不能通过编译:
// bobsrockets.scala
package bobsrockets {
class Ship
}
// fleets.scala
package bobsrockets.fleets {
class Fleet {
// 无法编译,Ship不在作用域内
def addShip() = { new Ship }
}
}
即使把这两个文件合并,也无法编译。但是当第二个文件把每个包分开声明时,上述规则又能生效。例如下面的代码是合法的:
package bobsrockets
class Ship
package bobsrockets
package fleets
class Fleet {
// 可以编译
def addShip() = { new Ship }
}
为了访问不同文件最顶层包的内容,Scala定义了一个隐式的顶层包“_root_”
,所有自定义的包其实都包含在这个包里。例如:
package launch {
class Booster3
}
package bobsrockets {
package navigation {
package launch {
class Booster1
}
class MissionControl {
val booster1 = new launch.Booster1
val booster2 = new bobsrockets.launch.Booster2
val booster3 = new _root_.launch.Booster3
}
}
package launch {
class Booster2
}
}
七、集合
数组Array
是定长的,但每个元素可以修改;可以包含不同类型元素;不可添加或者删除元素;从下标(0)
开始索引;列表List
也是定长的,并且每个元素不可修改;可以包含不同类型元素;虽然可以在头部或尾部添加元素或者合并列表,但这是创建了一个新的列表,并没有在之前的列表上直接进行修改(千万注意);从下标(0)
开始索引;数组缓冲和列表缓冲ArrayBuffer/ListBuffer
是不定长的,也即可变的,可以在头部添加,也可以在尾部添加或删除第一个符合的元素;并且耗时是固定的;最后转换成数组或者列表即可,但原来的缓冲仍然存在;- 元组也是定长的,每个元素不可修改;元组的特点是可以包含不同类型的对象,其字面量写法是在圆括号里编写用逗号间隔的元素;因为它可以包含不同类型的元素,所以不可遍历,也就无法通过下标来索引,只能通过
“_1”、“_2”......
这样来访问每个元素,注意第一个元素就是“_1”,不是“_0”;实际上,元组并不是一个类,而是一系列类:Tuple1、Tuple2、Tuple3…Tuple22,这些类都是具体的,因此除了通过字面量的写法构造元组,也可以显式地通过“new TupleX(元组元素)
”来构造;其中,每个数字代表元组包含的元素数量,也就是说元组最多只能包含22个元素,除非自定义Tuple23、Tuple24…不过这没有意义,因为元组可以嵌套元组,并不妨碍元组包含任意数量的元素; 映射Map
是包含一系列“键-值”对的集合,键和值的类型可以是任意的;键-值对的写法是“键 -> 值”;实际上,映射并不是一个类,而是一个特质,所以无法用new构建映射对象,只能通过伴生对象里的apply工厂方法来构造映射类型的对象;表达式“object1 -> object2”实际就是一个对偶(二元组)
,因此键-值对也可以写成对偶的形式;映射的apply方法通过接收一个键作为参数,返回对应的值;默认情况下,使用的是scala.collection.immutable
包里的不可变映射,也即定长并且元素不可修改;当然,也可以导入scala.collection.mutable
包里的可变映射,这样就能动态地增加、删除键-值对;可变映射的名字也叫“Map”,因此要注意使用import导入可变映射时,是否把不可变映射覆盖了;集Set
和映射一样,也是一个特质,也只能通过apply工厂方法构建对象;集只能包含字面值不相同的类型元素,可以包含不同类型元素;当构建时传入了重复参数,那么会过滤掉多余的,只取一个;集的apply方法是测试是否包含传入的参数,返回true或false,而不是通过下标来索引元素;默认情况下,使用的也是不可变集,scala.collection.mutable
包里也有同名的可变集;序列Seq
也是一个特质,数组和列表都混入了这个特质;序列可遍历、可迭代,也就是能用从0开始的下标索引,也可用于循环;序列也是包含一组相同或者不同类型的元素,并且不可变;其构造方法也是通过apply工厂方法;
要实现循环,在Scala里推荐使用for表达式。不过,Scala的for表达式是函数式风格的,没有引入指令式风格的“for(i = 0; i < N; i++)”。一个Scala的for表达式的一般形式如下:
for( seq ) yield expression
整个for表达式算一个语句。在这里,seq代表一个序列。换句话说,能放进for表达式里的对象,必须是一个可迭代的集合。比如常用的列表(List)、数组(Array)、映射(Map)、区间(Range)、迭代器(Iterator)、流(Stream)和所有的集(Set),它们都混入了特质Iterable。可迭代的集合对象能生成一个迭代器,用该迭代器可以逐个交出集合中的所有元素,进而构成了for表达式所需的序列。关键字“yield”是“产生”的意思,也就是把前面序列里符合条件的元素拿出来,逐个应用到后面的“expression”,得到的所有结果按顺序产生一个新的集合对象。
如果把seq展开来,其形式如下:
for {
p def something(x: String) = x match {
| case "Apple" => println("Fruit!")
| case "Tomato" => println("Vegetable!")
| case "Cola" => println("Beverage!")
| case _ => println("Huh?")
| }
something: (x: String)Unit
scala> something("Cola")
Beverage!
scala> something("Toy")
Huh?
十、隐式类与隐式参数
1、隐式类
隐式类是一个以关键字“implicit”开头的类,用于简化富包装类的编写。它不能是样例类,并且主构造方法有且仅有一个参数。此外,隐式类只能位于某个单例对象、类或特质里,不能单独出现在顶层。隐式类的特点就是让编译器在相同层次下自动生成一个与类名相同的隐式转换,该转换接收一个与隐式类的主构造方法相同的参数,并用这个参数构造一个隐式类的实例对象来返回。例如:
// test.scala
case class Rectangle(width: Int, height: Int)
object Rec {
implicit class RectangleMaker(width: Int) {
def x(height: Int) = Rectangle(width, height)
}
// 自动生成的
// implicit def RectangleMaker(width: Int) = new RectangleMaker(width)
}
将该文件编译后,就可以在解释器里用“import Rec._”或“import Rec.RectangleMaker”来引入这个隐式转换,然后用“1 x 10”这样的语句来构造一个长方形。实际上,Int类并不存在方法“x”,但是隐式转换把Int对象转换成一个RectangleMaker类的对象,转换后的对象有一个构造Rectangle的方法“x”。例如:
scala> 1 x 10
:12: error: value x is not a member of Int
1 x 10
^
scala> import Rec.RectangleMaker
import Rec.RectangleMaker
scala> 1 x 10
res0: Rectangle = Rectangle(1,10)
隐式类需要单参数主构造方法的原因很简单,因为用于转换的调用对象只有一个,并且自动生成的隐式转换不会去调用辅助构造方法。隐式类不能出现在顶层是因为自动生成的隐式转换与隐式类在同一级,如果不用导入就能直接使用,那么顶层大量的隐式类就会使得代码变得复杂且容易出错。
2、隐式参数函数最后一个参数列表可以用关键字“implicit”声明为隐式的,这样整个参数列表的参数都是隐式参数。注意,是整个参数列表,即使括号里有多个参数,也只需要开头写一个“implicit”。而且每个参数都是隐式的,不存在部分隐式部分显式。
当调用函数时,若缺省了隐式参数列表,则编译器会尝试插入相应的隐式定义。当然,也可以显式给出参数,但是要么全部缺省,要么全部显式给出,不能只写一部分。
要让编译器隐式插入参数,就必须事先定义好符合预期类型的隐式变量(val和var可以混用,关键在于类型)、隐式单例对象或隐式函数(别忘了函数也能作为函数的参数进行传递),这些隐式定义也必须用“implicit”修饰。隐式变量、单例对象、函数在当前作用域的引用也必须满足“单标识符”原则,即不同层次之间需要用“import”来解决。
隐式参数的类型应该是“稀有”或“特定”的,类型名称最好能表明该参数的作用。如果直接使用Int、Boolean、String等常用类型,容易引发混乱。例如:
// test.scala
class PreferredPrompt(val preference: String)
class PreferredDrink(val preference: String)
object Greeter {
def greet(name: String)(implicit prompt: PreferredPrompt,
drink: PreferredDrink) = {
println("Welcome, " + name + ". The system is ready.")
print("But while you work, ")
println("why not enjoy a cup of " + drink.preference + "?")
println(prompt.preference)
}
}
object JoesPrefs {
implicit val prompt = new PreferredPrompt("Yes, master> ")
implicit val drink = new PreferredDrink("tea")
}
scala> Greeter.greet("Joe")
:12: error: could not find implicit value for parameter prompt: PreferredPrompt
Greeter.greet("Joe")
^
scala> import JoesPrefs._
import JoesPrefs._
scala> Greeter.greet("Joe")
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
Yes, master>
scala> Greeter.greet("Joe")(prompt, drink)
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea?
Yes, master>
scala> Greeter.greet("Joe")(prompt)
:15: error: not enough arguments for method greet: (implicit
prompt: PreferredPrompt, implicit drink: PreferredDrink)Unit.
Unspecified value parameter drink.
Greeter.greet("Joe")(prompt)