文章目录
  1. 1. 类型推断
  2. 2. 基本类型
  3. 3. 函数式对象
  4. 4. 函数和闭包
  5. 5. 控制抽象
  6. 6. 组合与继承
  7. 7. Scala 层级
  8. 8. 特质(trait)
  9. 9. 包和引用
  10. 10. 断言和单元测试
  11. 11. 样例类和模式匹配
  12. 12. 列表:List
  13. 13. 集合与映射
  14. 14. 类型参数化(重头戏)

本文摘录Scala语言的一些语法和关键概念,不成系统,可看做学习笔记罢。

类型推断

for (arg <- args)arg一定是val类型,循环中不能改变其值。

Scala程序员的平衡感:

  • 崇尚val,不可变对象和没有副作用的方法
  • 首先想到他们,只有在特定需要或权衡后才选择var,可变对象或者带副作用方法。

Scala 伴生对象,是一个单例对象,可以看做Java中可能用到的静态方法工具类

基本类型

任何方法都可以是操作符,任何操作符都是方法。

函数式对象

辅助构造器,关键词this指向当前执行方法被调用的对象实例
如果使用在构造器里的话,就是指正在构建的实例

辅助构造器使用def this(..)定义,每个Scala构造器调用终将结束于对主构造器的调用。因为主构造器是类的唯一入口点。

重载操作符,重载后仍然按照原来的优先级,比如* > +

字面量标识符 yield可以作为一个变量/常量名

函数和闭包

本地函数:函数定义在函数中,本地函数可以随意访问包含它的函数的参数

函数字面量
例子: (x: Int) => x + 1
foreachfilter等许多函数中会使用到,x的类型往往可以被推断,所以通常也可写成: x => x + 1
函数字面量存在于源代码,而函数值作为对象存在于运行期。

更简单的,可以使用占位符语法,用下划线当做一个或者多个参数的占位符,只要每个参数在函数字面量内只出现一次即可,第n个下划线代表第n个参数
filter(_ > 0),调用时,用参数来填补下划线,也即filter(x > 0)
reduce(_ + _),调用时,分别填补,也即reduce(l+r)

偏函数(部分应用函数),一个下划线代替所有参数

闭包:函数字面量中包含了自由变量的绑定,运行时必须捕获其绑定。
val addMore = (x: Int) => x + moremore是自由变量
注意,闭包是一个非常重要的概念,我们时常会想要在循环体,比如foreach,map中加入一些对外部变量的修改,这是我们在其他语言养成的习惯。
直觉上,Scala在运行时会捕获自由变量本身,而不是变量指向的值。
比如
(x: Int) => x + more
此时创建的闭包可以看到闭包外部对more的改变,同样,闭包对捕获变量做出的修改在闭包外部也可见,比如:

1
2
3
4
5
6
val someNumber = List(-11, -10, 0, 10)
var sum = 0
someNumber.foreach(x => sum += x)

scala> sum
res: Int = -11

但是如果读者用过Spark的话,一定会了解到Spark的闭包和Scala的闭包是不一样的,原因就在于Spark是分布式环境下运行的。
这里有Spark官方对closure的描述。

还是上面那个例子

1
2
3
4
5
6
7
8
9
var sum = 0
var rdd = sc.parallelize(someNumbers)

// Wrong: Don't do this!!
rdd.foreach(x => sum += x)

println("Counter value: " + counter)

// counter = 0, because driver cannot feel the change of counter

众所周知,在分布式环境下,rdd的操作形成一个闭包,闭包会先序列化,然后被调度到各个executor执行,且每个executor拿到的其实是序列化后的sum,相当于driver端的一个copy,executor对sum的操作对driver来说不可见,driver的sum对各个executor来说也不可见,所以在driver端,counter始终是0。这就是scala闭包和spark闭包概念的一个最大不同。

重复参数,即可变参数,尾部加*号即可,如:
def echo(args: String*)
args其实是Array[String]类型,但是仍然不能真的传入一个Array[String]类型的参数,比如要传入arr,你需要echo(arr: _*)这么写,意思是告诉编译器把每个元素当参数而不是把arr当做单一参数。

尾递归:在最后一个动作调用自己的函数。注意只能是单纯的调用自己,不能有多余的表达式,也不能通过其它函数中转

Scala的核心:简洁,简洁,简洁!

控制抽象

柯里化,传名参数

组合与继承

组合指一个类持有另一个的引用,借助被引用的类完成任务。

不带参数,且没有副作用的方法可以不写括号

“脆基类”问题:意外的方法重写

多态的重新理解:父类型引用可以指向子类型对象 => 父类对象可有多种形式 => 多态

动态绑定:被调用的实际方法取决于运行期对象基于的类型

Scala 层级

所有类的父类是Any类,下辖两个子类,AnyRef(所有引用类的父类)和AnyVal(所有值类的父类)
底层有Nothing类和Null类,Null类是所有引用类的子类,不兼容子类型,而Nothing是所有类的子类。
scala的==对值类型为自然相等,对引用类型来说被视为equals方法的别名,equals初始定义为引用相等,但许多子类都会重写它以实现自然意义上的相等。
要比较引用相等,可以使用eq方法(反面是ne方法)

特质(trait)

特质类似Java中的接口,混入特质可以使用extends或者with

特质像是带有具体方法的Java接口,并且可以声明字段和维持状态值,特质可以做类定义所能做的事
但与类定义有两点不同:
1) 特质不能有参数(传递给主构造器)
2)super调用时动态绑定的

胖接口:拥有更多方法的接口
特质的一个用法就是把瘦接口变成胖接口

需要排序比较时,可以混入(mixin)Ordered特质
步骤:混入Ordered特质,实现compare方法,可以自动拥有大多数比较方法,但是不会有equals方法 => 类型擦除

特质的第二个用法:为类提供可堆叠的改变

混入多个特质,最右边的特质最先起作用

不同的组合,不同的次序混入特质,可以依靠少量的特质得到多个不同的类

特质线性化地解释super

特质,用还是不用?
1) 如果行为不会被重用,那做成具体类
2)如果要在多个不相关的类中重用,那就做成特质
3)如果希望从Java代码继承,那就是用抽象类 (只含有抽象成员的scala特质会被直接翻译成Java接口)
4)如果计划以编译后的方式发布,或者希望外部组织继承它,更倾向使用抽象类
5)如果效率很重要,倾向于使用类

包和引用

_root_顶层包:所有你能写出来的顶层包都是_root_的成员,可以用_root_.yourpack来访问

scala应用灵活在于:
1)可以随处import
2)可以指对象或包
3)可以重命名或者隐藏

每个scala源文件都隐含引用java.lang包,scala包以及单例对象Predef

访问修饰符:protected比Java中的更加严格:仅限子类访问,同一包中的类不能访问

访问修饰符限定规则:

private[X] method/class 此类或方法对X下所有类和对象可见

protected[X] method/class 对此类或子类或修饰符所在的包,类或对象X可见 (?)

断言和单元测试

assert:
assert(ele.width === 2) 三等号,如果不等,会报告“3 dit not equal to 2”

1
2
3
expect (2) {
ele.width
}

intercept检查是否抛出了期待的异常

1
2
3
intercept(class[IllegalArgumentException]) {
elem('x', -2, 3)
}

scala中的一些测试方法:
1) ScalaTest
2) Suite:
3) JUnit
4) TestNG
5) Specs 规格测试, a should be … 具有描述部分和规格部分
ScalaCheck 属性测试,测试代码具有属性

样例类和模式匹配

样例类 case class CLSNAME(argA: argAType, ...)
最大的好处是他们可以支持模式匹配

模式有很多种,包括通配,常量模式,变量模式,构造器模式,序列模式,元组模式,类型模式,更高级的还有变量绑定
使用类型模式应注意类型擦除,擦除规则不适用于数组

编译器会为case class自动生成伴生对象
编译器也会为该伴生对象自动生成apply,unapply方法

模式守卫

列表:List

List是协变的,意味着,如果S是T的子类,List[S]就是List[T]的子类,故而 List[Nothing]List[String] 的子类,故可以val List[String] = List()

:: 元素与List连接,元素与元素连接
::: List与List连接

计算长度.length方法需要遍历整个列表,所以如果判断长度为0的话最好使用.isEmpty方法
访问头部:init方法,访问除了最后一个元素外的子列表, head方法:访问第一个元素
访问尾部:last方法,访问最后一个元素, tail方法:访问除第一个元素外的列表
更一般的,drop, take方法

copyToArray: 把列表元素复制到目标数组的一段连续空间
elements方法:返回迭代器

其他的List高阶方法:

1
2
3
4
5
map,flatMap,foreach
过滤:filter,partition,find,takeWhile,dropWhile,span
论断:forall,exists
折叠:/: 和 :\
翻转reverse,排序sortWith

List对象的方法:

1
2
3
List.apply,List.range,List.make(4, 'a')
List.unzip 解除啮合
连接: List.flatten, List.concat

区别在于前者用列表的列表做参数,后者可以直接用多个列表作为参数(以可变参数的方式)

Scala类型推断
Scala采用局部的,基于流的类型推断算法

通常,一旦有需要推断多态方法类型参数的任务时,类型推断器只会参考第一个参数列表中所有的值参数类型,而不会参考之后的参数。
库方法设计原则:
如果需要把参数设计为若干非函数值即一个函数值的某种多态方法,需要把函数参数独自放在柯里化参数列表的最后面。
即,在柯里化方法中,方法类型仅取决于第一段参数。
同样的,一种快速解决类型错误问题的方法:
添加明确的类型标注

集合与映射

1
2
3
Set, Seq, Map -> Iterable
SortedSet
SynchronizedMap

如果元素数量不多,不可变集合比可变集合存储更紧凑,空间更加节省。

可变状态的对象

状态与var变量常常一起出现,但并不具有严格的关系。
类即使没有定义或继承var变量,也可以由于把方法调用传递给其他具有可变状态的对象而带有状态,(有点拗口)
类即使包含了var变量也可以仍是纯函数的

Scala中,对象的每个非私有的var类型成员变量都隐含定义了gettersetter方法。
getter方法为x
setter方法为x_

类中字段初始化为0/false/null,
var x = _

不可以省略 “= _”,否则var x: Float 为抽象变量,而不是未初始化的变量

类型参数化(重头戏)

类型参数化让我们能够编写泛型和特质
信息隐藏:
隐藏主构造器: private加载类名的后面,类参数列表的前面:

1
2
3
4
class Queue[T] private (
private val leading: List[T],
private val tailing: List[T]
)

那么如何构造对象呢?

方法1:定义辅助构造器

1
2
def this() = this(Nil, Nil)
def this(elem: T*) = this(elem.toList, Nil)

方法2:在伴生对象中编写apply工厂方法

1
2
3
object Queue {
def apply[T](xs: T*) = new Queue[T](xs.toList, Nil)
}

(注意,scala没有全局方法,方法必须包含在类或对象中)

另一种信息隐藏的方法,直接把类本身通过暴露特质(trait)而隐藏掉

如果类或者特质声明时带类型参数,那么创建变量时也要制定具体的参数化的类型

1
2
trait Queue[T] { .. }
Queue是特质, Queue[String]是类型

泛型:通过一个能够广泛适用的类或特质,定义了许多特定的类型

在scala中,泛型类默认是非协变的子类型化
如果要表明参数的子类型化是协变的,需要变成如下形式:
trait Queue[+T] { .. }

加上-号表示需要逆变的子类型化

只要泛型的参数类型被当做方法参数的类型,那么包含它的类或特质就有可能不能与这个类型参数一起协变

1
2
3
class Queue[+T] {
def append(x: T) = ...
}

下界和上界
def append[U>: T](x: U) = new Queue[U](leading, x :: tailing)
语法U >: T定义了T为U的下界,结果U必须是T的超类型,append的参数现在为U而不是T,返回类型也变成了Queue[U],不过,自身即是自身的超类型又是自身的子类型,所以是类似小于等于的关系。

def orderedMergeSort[T <: Ordered[T]](xs: List[T]): List[T] = ...
语法T <: Ordered[T]定义了类型参数T具有上界Ordered[T],即传递给orderedMergeSort的参数必须是Ordered[T]的子类型。

编程语言 | Program Lang.