以下内容基于 Tour of Scala
在快速浏览了一一遍 tour 中的内容后,我把 Scala 与其他常见的编程语言如 Java、Python、JavaScript 等在设计、结构、推荐写法等方面,做一个横向的对比。由于这份文档是基于 Scala2 的,而我本地调试用的是 Scala3,所以还会提及一些 2 和 3 的区别。
introduction
- 面向对象,所有值都是对象
- 函数式编程语言
- 静态类型语言、类型推断
- 可扩展性,通过宏的方式实现其他语言结构
- 互通性,JRE 环境友好,与主流面向对象java编程语言无缝衔接,对应的 Java 库也都可以调用
Basics
Expression 的 result 类型是可以自动推断出的,也可以显式声明。
Val (values)是不可变的,无法 reassign,var (variables) 是可以重复赋值的。By default, Scala always picks immutable collections.
functions 和 method 的区别是, function 类似 JavaScript 中的匿名函数,method 是类似 Python 一样的用 def 定义的方法
method 还可以接受多组参数
def addThenMultiply(x: Int, y: Int)(multiplier: Int): Int = (x + y) * multiplier
这个设计起初我不太理解,因为完全可以把后面的的参数变成默认唯一一组参数中的某个值,后面在讲到柯里化的一系列应用以及帮助类型推断方面有很大的用处。
类型的声明总在冒号后,这句话重复了好多次。
Scala 默认不(建议)用 return,最后一个表达式的结果就是返回值,这里如果之前只接触过 oop 没接触过 fp 的人来说会有点奇怪,查了一下主要有以下原因
- 副作用:影响 type inferred,影响性能
- 不符合 fp 的设定:尽量用表达式,而非语句
类,和其他语言差不多,接受构造参数,实现方法。没有意义的返回是 Unit 类型,类似 Java 和 C 中的 void,type Unit 返回值是 ()。
Case class 有点像 atom,实例是不可变的,比较的时候是值,而非引用。
Traits 特征/特质,类似 interface,可以继承,方法可以重写
main 作为入口接受 array of strings 作为参数,因为 JVM 虚拟机需求。
Unified Types
- Any 是 top type,实现了一些通用的方法。下面氛围 AnyVal 和 AnyRef
- AnyVal 表示值类型,有9种预先设定的非空的子类型;Unit 表示空,它只有一种实例,是
()
,有时候用于 return。 - AnyRef 表示引用类型,用户定义的都是 AnyRef,类似 java.lang.Object
- 类型转换是单向的
- Nothing 是一个 bottom type,没有任何值的类型是它,通常用于抛出异常,未正确返回的函数,无法 eval 成值的表达式等。
- Null 是 AnyRef 的一个 subtype,有一个 null 关键字值,主要为了转成其他 JVM 语言,在 scala 里不应该使用。
Class
没定义构造函数的,会有一个不接受参数的默认构造函数
class Point(var x: Int, var y: Int) {
def move(dx: Int, dy: Int): Unit = {
x = x + dx
y = y + dy
}
override def toString: String =
s"($x, $y)"
}一个4个成员的类,构造函数的参数可以写进 class 定义部分
move 方法啥也没返回,所以是 Unit,类似 void
函数/类的参数可以有默认值以及传递命名参数,这可比 java 强多了
private val/var,会自动生成 getter setter,此外的都是 private values 对外不可访问
class Point(x: Int, y: Int)
val point = new Point(1, 2)
point.x // <-- does not compile
Default Parameter Values
除了转成 java 调用时,会遇到问题,注意重新抽象或者必传所有参数。其他的与 Python 的使用没有差别。
Named Arguments
命名参数可以乱序,未命名的一定要在命名的之前,从 java 中调用还是有问题。
Traits
无法实例化,没有参数,非常适合用于泛型和抽象方法。
继承类中,需要实现 trait 的方法,包括遵循子类型。
Tuple
和 Python 的 tuple 基本一样,通过下标访问的方式是 _1
Pattern matching 这个部分,可以用于拆解赋值,也可以用于 for 循环中的匹配输出,还可以用于列表推导,还是很灵活的。
val numPairs = List((2, 5), (3, -7), (20, 56)) |
这里 for 循环的倒箭头惊到我了hhh。
关于 tuple 和 case classes 的选择问题,case classes 的可读性会更好一些。
Class Composition with Mixins
class D extends B with C |
这里 D 的父类是 B,mixin 就是 C。父类只有一个,mixin 可以有多个。
Trait 间的继承不需要实现父 trait 的方法。
trait RichIterator extends AbsIterator { |
这个写法就很 cool 了。
Higher-order Functions
参数是 function 或者返回值是 function 。这和 Python 的函数作为 first-class 是一个意思。
一个 map(function) 这样的例子。
另外一种方式是类似工厂模式,通过实现不同函数的细化某一类事情。
返回值是一个匿名函数。仔细想了一下,创建函数的必要性,有点像 Python 中的偏函数,当然也可以用其他方式去实现,只是这样会更直观,我返回的就是一个函数,而不需要像在 java 中做一些重载或者反复定义的事情了。
nested methods
函数的嵌套,这很函数式编程,很早的时候我看 SICP 和 c311 的时候用到 Scheme 这门 Lisp 方言,经常写一些回括号很多的套了好多表达式的代码。
对 scala 的 function 和 method 的定义区分又模糊了,在原有的 Python 或者 Node 中,我会认为 method 是 class 中的方法,function 是外面单独定义的命名或者匿名的,现在没有上下文的情况下,文档把这种东西叫 method,有点迷惑。
Multiple Parameter Lists (Currying)
Currying 这种东西在 JavaScript,尤其是一些前端的 code 中经常能看到,就是函数后接着好多组参数,每一个前面的结果是另一个函数,继续接受下一个参数,一直到末尾。
我对这种东西持有中立态度。理论上所有高阶函数都可以写成更易读的普通函数,这里还要看文档和逻辑支撑情况。
这里举了一个很好的例子,就是类型推断,如果用 flatten parameters 的话,匿名函数的参数需要显式指明,否则会编译失败。
def foldLeft1[A, B](as: List[A], b0: B, op: (B, A) => B) = ??? |
此时编译器仍然在推断 A 和 B。而如果改成多参数的话:
def foldLeft2[A, B](as: List[A], b0: B)(op: (B, A) => B) = ??? |
编译器就能自己 infer 出来了。
偏函数也提了一下,在 3 中,柯里化已不再需要占位符。
其实这一节的重点是 implicit
,虽然之前没有接触过,但是看了一些别人写的东西,感觉还是大有可为的。另外 scala 没有静态成员,可以把 implicit function 写在半生对象里。隐式转换只会触发一次,另外是在编译器查找方法失败的情况下才会触发,这里要特别注意一下。
Case Classes
一种主打 immutable 的类。实例化的时候不用 new,case class 能自己 apply ,参数尽量不用 var。比较时比较的是值,而不是引用,复制的时候,也是浅拷贝,这两个挺好的,用起来会比较顺手,不会出现某些语言大量地址引用,复制修改时要格外小心。
Pattern Matching
import scala.util.Random |
比 switch 写起方便一点;另一个例子中参数定义的是父类,但是传入 instance match 到具体子类(case class),比 java 的不停 cast 强了很多。另外就是可以通过 boolean expression 的方式去 match 某个参数值
case Email(sender, _, _) if importantPeopleInfo.contains(sender) => |
还可以只 match 类型
def goIdle(device: Device) = device match { |
sealed
密封这个概念也很有趣,让所有的子类型都在同一个文件里,不过这样确实保证了无 catch other 的状况。
Singleton Objects
全局的单例对象,非常适合用作工具方法。另外就是伴生对象和伴生类。这里提到了一个Option
类别,类型如其名,当函数有可能不返回值的时候使用,Option
有两个子类 Some
和 None
,在这里 Understanding Some and Option in Scala - Stack Overflow 提到,用 Option
的好处是调用者都必须要检查值是否存在,而且不仅仅用在 match 这里,在任何 map
,flatMap
和 getOrElse
中都可以使用。
Regular Expression Patterns
用 .r
结尾是没想到的。虽然后面两节才介绍 for 循环,但是每次看到反箭头还是会别扭一下。
Extractor Objects
开始的时候会有点误会 apply
和 unapply
是在做 serialization deserialization,实际上是在做 construction 和 deconstruction,解构这个概念挺好的,
For Comprehensions
for (enumerators) yield e
也可以不 yield 这样 for 语句就返回了 Unit。注意返回值的类型是由第一个 generator 决定的,如 xx <- xxList
那这返回的就是一个 List,然后 yield 的值的类型决定了 List 中 item 的类型。
Generic Classes
泛型类, calass Stack[A]
虽然也是用什么都行,之前看到的可能更多是在用 T。 elements = x :: elements
这里是个 prepending 如果是要 appending,则是 elements = elements :+ x
。Nil
表示的是空数组,如果我们创建一个空数组 var a = List.empty[String]
和 Nil
做一下比较会发现二者是一样的。子类型也可以传入泛型类。
Variances
这一部分讲的是协变和逆变。和 C# 一样,是在抽象类定义时就约定好的,而 Java 是在使用时通过 <? Extends T>
和 <? Super T>
实现的。在相当长的一段时间里,我并不懂为什么需要有逆变这种东西,这里关于 Java 的逆变的作用,Kotlin 的文档讲得挺好的,
Upper Type Bounds
class PetContainer[P <: Pet](p: P) { |
T <: A
表示当T 为 A 的子类型时,构建这个 container。