07 Mar 2016
读书笔记(6)
Chapter 7 Built-in Control Structures
Scala的if, for, try, match
都有返回值。
Scala的if
val filename =
if (!args.isEmpty)
args(0)
else
"default.txt"
这种写法有两个优点:
- 更短
- 使用
val
而不是var
。
不带else
的if
也有返回值,会返回Unit
类型的()
。当一个值的类型转换成Unit
,关于值的所有消息都会丢失。所以()
不表示任何值。
Scala的while
上
var line = ""
do {
line = readline
println("read: " + line)
} while (!line.isEmpty)
Scala的while
和do-while
跟Java里的差不多,然而while
和do
都返回不含任何值的()
。这虽然有悖纯函数语言的哲学,但是方便了用户用命令式语言风格写出更具可读性的程序,比如一些算法。而有些算法虽然用while
也能写,但是用递归等函数语言风格的话可能会更清晰,比如算最大公约数的算法:
def gcd(x: Long, y: Long): Long =
if (b == 0) a else gcd(b, a % b)
总的来说,更推荐用函数风格来写程序,因为while
不返回值,需要引入var
。
Scala的for
最简单的用法就是循环一整个collection
val fileHere = (new java.io.File(".")).listFiles
for (file <- filesHere)
println(file)
这种写法能循环所有collection,不只数组(必须实现了scala.Iterable
这个trait的<-
方法)。比如说循环一个Range
类型。
for (i <- 1 to 5)
println("Iteration " + i)
过滤
for (file <- filesHere; if file.getName.endsWith(".scala"))
println(file)
或
for (file <- filesHere)
if (file.getName.endsWith(".scala"))
println(file)
因为for
有返回值,所以for
是一种expression。
可以加入更多的判断条件。
for (
file <- filesHere;
if file.isFile;
if file.getName.endsWith(".scala")
) println(file)
为了更可读,可以把()
换成{}
。这样就可以省略掉分号;
。
for {
file <- filesHere
if file.isFile
if file.getName.endsWith(".scala")
} println(file)
嵌套循环
def fileLines(file: java.io.File) =
scala.io.Source.fromFile(file).getLines
def grep(pattern: String) =
for {
file <- filesHere
if file.getName.endsWith(".scala")
line <- fileLines(file)
if line.trim.matches(pattern)
} println(file + ": " + line.trim)
grep(".*gcd.*")
中途赋值(Mid-stream assignment)
def grep(pattern: String) =
for {
file <- filesHere
if file.getName.endsWith(".scala")
line <- fileLines(file)
trimmed = line.trim
if trimmed.matches(pattern)
} println(file + ": " + trimmed)
grep(".*gcd.*")
例子中的trimmed = line.trim
的效果类似val
,只是不用标明val
。减少了一次算trim
的重复开销。
生成新collection
def scalaFiles =
for {
file <- filesHere
if file.getName.endsWith(".scala")
} yield file
这里在循环体之前用了yield
,生成了新的collection。
当使用yield
的时候,实际上是如下的结构:
for
循环判断语句 yield
循环体
yield
必须放在循环体之前。(跟ruby和C#等放置的位置不同)
Scala的try
Scala的try
跟其他语言的的异常处理差不多。
抛出异常
throw new NullPointerException
val half =
if (n % 2 == 0)
n/2
else
throw new Exception("n must be even")
- 用法跟Java差不多。
- 技术上来说,
throw
返回了类型Nothing
。这使得上面例子的写法可以成立。
捕获异常
try {
doSomething()
}
catch {
case ex: IOException => println("Oops!")
case ex: NullPointerException => println("Oops!!")
}
- 写法跟其他语言类似。
- 这里用了一种叫模式匹配(pattern matching)的方法,后面会提到。
- 上面的异常处理会按顺序从上往下检查。如果无法捕获,再往外层抛出。
- 不像Java,你无需处理已检查的异常。
finally
语句
跟Java几乎一样。
生成值
try-catch-finally
结构是有返回值的。比如:
val url = try {
new URL(path)
}
catch {
case e: MalformedURLException =>
new URL("http://www.scala-lang.org")
}
这里有个要注意的地方,如果在Scala的finally
的结构体内中途显式return
返回值,这个值会覆盖之前的返回值,比如:
def f(): Int = try { return 1 } finally { return 2 }
def g(): Int = try { 1 } finally { 2 }
这里f()
返回2,而g()
返回1。
Scala里的match
Scala里的match
,相当于其他语言的switch
。不过Scala的match
能让你匹配任何模式(详见Chapter 12)。
val firstArg = if (args.length > 0) args(0) else ""
firstArg match {
case "salt" => println("pepper")
case "chips" => println("salsa")
case "eggs" => println("bacon")
case _ => println("huh?")
- 这个例子依次把
firstArg
匹配"salt","chips","eggs"
,而通配符_
则匹配默认值。
- 不像Java里的
switch
,只能匹配整数类型和枚举常量,Scala的match
能匹配所有常量。(虽然书里没提,实际上Java7的switch
也能匹配字符串,是通过语法糖实现的,方法是先计算变量的.hashCode()
,再比较常量的哈希值,最后用equals
按值比较。枚举类型本来就是语法糖,实现方法应该也类似。)
break
不可用。(这东西本来就是语法盐。近代的语言基本都不保留了。)
没有break
和continue
也能活
有点意外的是,Scala保留了while
,却非常前卫地完全去掉了break
和continue
。
书里提的例子就是用递归或者用其他函数式的方法去避免用break
和continue
,介绍得不怎么详细。有空做一下深入调查再写。
27 Feb 2016
读书笔记(5)
Chapter 6 Functional Objects
这章用了一个叫ChecksumCalculator
的实例介绍Scala里的OOP。
09 Feb 2016
读书笔记(4)
Chapter 4 Classes and Objects
变量作用域
- 跟Java一样,Scala也是用花括号
{}
定义新的变量作用域(scope)。要在scope之外引用变量,只能通过继承/import/成员变量。
- 一旦定义了变量,不能在同一域里用同一名字定义变量
val a = 1
val a = 2 // 编译错误
println(a)
val a = 1; // 这里";"是必须的
{
val a = 2 // 编译通过
a
}
println(a) // 输出1, 因为花括号内的a跟外面的a无关。
- 第二段代码在Scala能编译通过,然而在Java里是不允许括号内部的变量与外部的变量同名。在Scala里此括号外部的同名变量在括号内部是不可见的。
- Scala这么做是为了创造更方便的交互环境。
- 在interpreter里,Scala可以重复定义一个变量。
scala> val a = 1
a: Int = 1
scala> val a = 2
a: Int = 2
scala> println(a)
2
- interpreter可以这样做的原因是它会自动为每一段语句创建一个嵌套的花括号。上面的代码相当于
val a = 1;
{
val a = 2;
{
println(a)
}
}
分号推定
- Scala里句末的分号通常是可省略的。
- 只有在你要在一行里写多句语句时,分号才是必要的。
- 这个特性有时会产生有违你本意的结果,比如
Scala 会把这段分成两行:x
和+y
。要两行算x+y
,可用
或
这样的写法。
如果下面三个条件满足一个或以上,该行就不会被判断为已完结
- 该行用
.
或infix-operator
(后述)等不合法的语法结尾。
- 次行的开头的词不能用作开头。
- 该行的括号
()
[]
没完结。因为它们内部不允许多行。
单例对象
又是单例对象。
- 单例对象与其伴生类可以互相访问私有成员。
- 对Java程序员来说,理解单例对象最好的方法就是:它是Java里所有静态方法的集合体。用法跟Java里的也差不多。
- 跟Java最大的不同是,单例对象不能用
new
初始化,所以它们的构造函数是不能带参数的。
- 单例对象的实现方式是通过静态变量,实际上它们的初始化方式是跟Java的静态成员差不多的。单例对象只会在第一次被访问时初始化。
- 单例对象适合用来放置工具函数。(比如之前关于
List
的那章,就用List.apply
这个工厂函数生成新的不可修改对象)
- 没有伴生类的
standalone object
还可以用来定义一个Scala程序的入口,书里的4.9小节有介绍,这里省略。
Chapter 5 基本类型和操作符
- 虽然Scala里所有值皆是对象,但为了运行效率Scala编译器实际上会把一部分值用Java的原始类型表达。
- Scala的基本类型:
Byte
,Short
,Int
,Long
,Char
,String
,Float
,Double
,Boolean
。(本来想找官方文档的,没有。书里的Table 5.1)
- 除了
String
在java.lang
包里,其他的基本类型都是scala
包里。比如Int
的全名是scala.Int
。
scala
和java.lang
会被自动引用到所有的Scala源文件里。你只用使用缩写,比如Int
。
- 这些类型的取值范围跟Java的原始类型一致,这是为了能在转换成字节码时优化成Java的原始类型。
- 这些基本类型都有全小写的别名,跟Java的原始类型一样,但请跟随社区的选择,别使用。(我用2.11.7试了下,已经不能使用了)。
Literals(符码)
- Literal指的是我们在代码里对常量的在文面上的表达方式。
- Scala的literals跟Java的几乎一样。
- 整数(与Java一样)
- 浮点小数(与Java一样)
- 字符(与Java一样)
- 字符串(与Java一样,唯一的不同,就是Scala支持多行字符串符码),比如:
println("""Welcome to Ultamix 3000.
Type "HELP" for help.""") // 没有对齐
println("""|Welcome to Ultamix 3000.
|Type "HELP" for help.""".stripMargin) // 通过`stripMargin`方法对齐
用的是"""
,跟python与ruby类似。
操作符是方法
- 反复说了。Java里的操作符是语法,Scala里的操作符是方法。既然是方法,自然也可重载。
- 当不使用
.()
来调用一个方法时,使用的是下面三个操作符记法的其中之一:
- prefix(前缀):操作符放于值之前,比如
-7
里的-
。
- postfix(后缀):操作符放于值之后,比如
7 toLong
里的to Long
。
- infix(中缀):操作符放于值之中,比如
7 + 2
里的+
。
- 操作符与值之间的空格不是必须的,比如
1+2
与1 + 2
与(1).+(2)
与1.+(2)
是完全一样的。(当然了,这只在不存在歧义时才成立)。
- 操作符记法可适用于任何方法,比如String的
indexOf
,如str indexOf 'o'
。
- 当你使用中缀记法来调用一个多于一个参数的方法时,要用括号,如
str indexOf ('o',5)
,相当于str.indexOf('o', 5)
。
- 与中缀记法不一样,前缀与后缀记法是
unary
的,也就是一元操作符。
- 前缀记法是调用特定方法的简略写法,比如
-2.0
里的-
,实际调用的是一个带unary_
前缀的方法:unary_-
。Scala会把-2.0
自动转换成(2.0).unary_-
。
- 只有
+
,-
,!
,~
四个特殊字符能成为前缀方法。像unary_*
一类的方法是不能用前缀记法调用的(但可正常调用)。
- 后缀就比较简单了,没有任何参数的方法都可以用后缀记法调用。
Scala里的操作符
虽然本质不一样,用起来跟Java大同小异,只写比较关心的部分。
- Scala里的浮点小数求余用的不是IEEE 754标准,要用专用的
IEEEremainder
方法。(Java好像也是一样的。)
- Scala里的逻辑与和逻辑或跟Java一样,是可以短路的。由于Scala的逻辑与和逻辑或实际上是方法,所以其实Scala是可以做到选择性地不计算参数里的函数的。这是通过Scala的延迟计算特性来实现的,具体后述。
对象比较(Object equality)
- Scala里的对象比较可以比较所有对象,不仅是基本类型。如:
scala> 1 == 2
res24: Boolean = false
scala> 1 != 2
res25: Boolean = true
scala> 2 == 2
res26: Boolean = true
scala> List(1,2,3) == List(1,2,3)
res27: Boolean = true
scala> List(1,2,3) == List(4,5,6)
res28: Boolean = false
scala> 1 == 1.0
res29: Boolean = true
scala> List(1,2,3) == "hello"
res30: Boolean = false
scala> List(1,2,3) == null
res31: Boolean = false
scala> null == List(1,2,3)
res32: Boolean = false
- 在Java里,对于原始类型,
==
比较值;对于引用型变量,==
比较的是两者是否引用JVM的heap里的相同对象。
- 在Scala里,
==
实际是调用了equals
,只要实现了基于内容比较的equals
,就能比较两者内容是否一致。
- Scala里有Java式的比较函数的实现
eq
和ne
,后述。
操作符优先级(operator precedence)
- Scala里没有操作符优先级,但有方法优先级。
- 方法的优先级是通过方法名的首字符判断的。比如说,
*
开头的方法比+
开头的方法优先,所以2 + 2 * 7 = 2 + (2 * 7)
- 具体优先级如下:
- 当优先级一样时,Scala通过方法名的未字符判断其关联性(associativity,就是判断操作数与其左侧还是右侧的操作数组合)。一般的方法是从左到右结合,当方法名以
:
结尾时,从右到左结合(前面关于String
的一章有提过)。所以,a ::: b ::: c
相当于a ::: (b ::: c)
,而a * b * c
相当于a * (b * c)
。
- 千万别为了炫耀自己对于操作符的理解而省略括号!!!你应该默认所有的程序员只知道
*,/,%
优先于+,-
。除此之外的所有运算符都应该加括号!!!
关于这一点,我个人深有同感。我自己在给后辈做code review时,有违这个原则的,都打回去要求写括号。Scala作为一门为了简洁可以去掉句末;
的语言,作者却要求明确写出括号注明优先级,可以看出这个括号是不可少的。究其原因,除了作者所述的原因外,我觉得还有一点:不同的语言的操作符优先级不一定一样。
比如说,PHP里,赋值运算符比比较运算符优先级高,而在JavaScript里,比较运算符比赋值运算符优先级高。在某些脚本语言里,逻辑与和逻辑或是同级的,如bash。更极端的甚至有语言是没有运算符优先级的,比如LISP那堆。要填上不同背景的工程师之间的鸿沟,括号是最优雅简单的选择。如果你觉得括号省去也没啥问题,很可能是你学的语言还不够多。
未完待续。
08 Feb 2016
读书笔记(3)
Chapter 3 Next Steps in Scala
类与单例对象
构造类
class FancyGreeter(greeting: String) {
def greet() = println(greeting)
}
val g = new FancyGreeter("Salutations, world")
这里的greeting
是私有的不可修改量。
- 所有的包含在花括号内的非函数或变量定义的部分,都是构造函数。称之为
primary constructor
。
class RepeatGreeter(greeting: String, count: Int) {
def this(greeting: String) = this(greeting, 1)
def greet() = {
for (i <- 1 to count)
println(greeting)
}
}
val g1 = new RepeatGreeter("Hello, world", 3)
g1.greet()
val g2 = new RepeatGreeter("Hi there!")
g2.greet()
- Scala也支持多个构造函数。但是必须选定一个
primary constructor
,其他的是auxiliary constructor
。
auxiliary constructor
的名字必须是this
,必须无返回值,第一个参数必须是同一类内的其他构造函数。
单例对象
- Scala不允许类内有静态变量或静态方法。
- 取而代之,Scala引入了
singleton object
。
singleton object
不能被,也不应该用new
创建实例。它会在第一次被引用时被创建,且只有一个实例。
singleton object
的名字可以与类相同,这个singleton object
被称为这个类的companion object
(伴生对象)。
- 只有
object
没有class
的话,叫stand-alone object
。
实例: WorldlyGreeter.scala
(非脚本,故用驼峰式命名。实际上不像Java,Scala的文件名跟内容没关系,只是推荐像Java一像用跟内容类一致的文件名。)
// The WorldlyGreeter class
class WorldlyGreeter(greeting: String) {
def greet() {
val worldlyGreeting = WorldlyGreeter.worldify(greeting)
println(worldlyGreeting)
}
}
// The WorldlyGreeter companion object
object WorldlyGreeter {
def worldify(s: String) = s + ", world!"
}
trait
与mixin
- Scala的
trait
可以包含非抽象方法,而Java的interface
就只能包含抽象方法。所以Java里是实现interface
,而Scala是扩展trait
。
- Java跟Scala的类都只能继承自一个父类。
- Java的类可以实现0~无限个
interface
,Scala的类可以扩展0~无限个trait
。
implements
并非Scala的关键字。
- 不同于Java,Scala里重载一个函数要有
override
关键字。
- 不同于Java,Scala可以在初始化时混合
trait
。
混合例子
trait ExclamatoryGreeter extends Friendly {
override def greet() = super.greet() + "!"
}
val pup: Friendly = new Dog with ExclamatoryGreeter
println(pup.greet())
- 这里创建的类叫
synthetic class
,意为“编译器创建的类”。(应该是编译时创建而非运行时)
Chapter 4 Classes and Objects
- A class should be responsible for an understaandable amount of functionality. (不知道怎么译好)
- Scala里定义一个值的时候必须初始化。
- Java里的类变量是可以不初始化的,它们会被根据类型被自动初始化对应的值(数值就是0或0.0,布尔值是false,引用则是null,等等)。Scala里要获得同样效果可以用
_
,比如var x: Int = _
。
(文章里还介绍了一个关于unreachable
的例子,不难,割爱掉。)
4.2 Mapping to Java
感觉这一小节挺重要的。
Fields and methods
class ChecksumCalculator {
private var sum = 0
def add(b: Byte) { sum += b }
def checksum: Int = ∼(sum & 0xFF) + 1
}
- 如果方法前没有
=
,会返回Unit
类型,因为Scala会把任何类型转换为Unit
scala> def g { "this String gets lost too" }
g: Unit
scala> def h = { "this String gets returned!" }
h: java.lang.String
未完待续。
03 Feb 2016
读书笔记(2)
Chapter 3 Next Steps in Scala
##带类型数组
Scala里的用参数创建实例(parameterize)的方法:
val greetStrings = new Array[String](3)
greetStrings(0) = "Hello"
greetStrings(1) = ","
greetStrings(2) = "world! \n"
for (i <- 0 to 2)
print(greetStrings(i))
- 这里
greetStrings
的类型是Array[String]
。(类似Java5引入的泛型。)
- 使用
val
后,不能再修改引用,但引用的对象可以修改。
- 跟Java不同,泛型使用方括号
[]
,下标引用使用圆括号()
。
0 to 2
里的to
是方法名,相当于(0).to(2)
,它返回一个Scala的包含0,1,2
的iterator
。Scala里的+,-,*,/
都是方法名。
- 在Scala里,Array也是类。当在实例后面使用
()
,Scala会调用一个叫apply
的方法。greetStrings(i)
相当于greetStrings.apply(i)
。不止Array,任何实例后面接()
都会调用对应的apply
。
- 类似地,
greetStrings(0) = "Hello"
相当于greetStrings.update(0, "Hello")
- 不像其他语言,上述的这种变换在Scala里不会明显影响性能。Scala编译器会尽量把他们优化成Java的本地数组或者原始类型(primitive type,像int一类)。
##关于List和Tuple
- 如上例所示,Scala的Array是同一类型的值的可修改的序列。像上述的
Array[String]
,虽然长度不可修改,但内容可修改。所以Array是可修改的。(为避免混乱,本笔记系列会把所有mutable都译成可修改,反之immutable都译成不可修改)
- 比Array更适合函数语言的,是List,它不可修改。它也是同一类型的值的序列。跟Java的
java.util.List
不一样,Scala的List,也就是scala.List
,它是总不可修改的。它是为函数风格编程设计的。
List的示例
val oneTwo = List(1, 2)
val threeFour = List(3, 4)
val oneTwoThreeFour = oneTwo ::: threeFour
val twoThree = List(2, 3)
val oneTwoThree = 1 :: twoThree
- List的创建没用
new
,因为它调用的是List的伴生对象(companion object,后述)的apply
方法。
- 两个List相接,用
:::
。它用于把一个List接到另一个List的前面,也就是prepend
。
- 在Scala里一般的方法是从左到右的,比如
a * b
相当于a.*(b)
。而:
结尾的方法则反之,a ::: b
相当于b.:::(a)
,所以说是把a接于b的前面。
::
念“cons”。cons用来把一个元素接到一个List前面,与:::
同理。
- List是没有
append
的,因为它的时间复杂度是O(n)。
题外话:关于List的时间复杂度
自己总结了一下Scala的List的一些性能资料:
- List是为后入先出(LIFO)优化的,类似Stack。如果要使用其他访问方式,别用List。
- List只有
prepend
,访问链首or链尾三个操作是O(1)的,其他如随机访问,length
,append
,reverse
都是O(n)的。
- Java的List的话则有两个实现
ArrayList
和LinkedList
,伪代码如下
public ArrayList<T> {
private Object[] array;
private int size;
}
public LinkedList<T> {
class Node<T> {
T data;
Node next;
Node prev;
}
private Node<T> first;
private Node<T> last;
private int size;
}
可以看出来,随机访问的话,ArrayList快,增删的话,LinkedList比较好。
Scala的Tuple
- 跟List一样,是不可修改的。
- 跟List不同,Tuple的各个值类型是不一样的。
- Tuple非常适合同时返回多个值。(Python等语言也可以,非常实用。Java的话只能用类似JavaBean或直接修改输入,超级恶心)
val pair = (99, "Luftballons")
println(pair._1)
println(pair._2)
- 可以直接用
()
生成。Scala会自动判断类型,这里是生成了一个可容纳两个值的Tuple2[Int, String]
。
- 不可以像List那样通过像
pair(1)
一样的下标访问的方式使用,因为Tuple各个值类型不一样。只能通过像pair._1
一样访问
- 数据序号从1开始,Tuple最多只能到
Tuple22
,容纳22个值。这个数字其实是任意的。
Set与Map
- Scala里的List总是不可修改的,Array总是可修改的。然而Set与Map则同时有可修改与不可修改两套实现。
- 无论Set或Map,它们的可修改不可修改版都是从同一个trait继承出来的两个subtrait,然后再继承成类。
这是可修改Set的例子:
import scala.collection.mutable.HashSet
val jetSet = new HashSet[String]
jetSet += "Lear"
jetSet += ("Boeing", "Airbus")
println(jetSet.contains("Cessna"))
- HashSet要事先import。
("Boeing", "Airbus")
,这不是Tuple,而是相当于jetSet.+=("Boeing", "Airbus")
。要传入Tuple的话,要使用jetSet += (("Boeing", "Airbus"))
,双圆括号。
这是不可修改Set的例子:
val movieSet = Set("Hitch", "Shrek")
println(movieSet)
- 这里用了
scala.collection.Set
的伴生对象的工厂方法,这个对象是自动import的。
这是可修改的HashMap的例子:
import scala.collection.mutable.HashMap
treasureMap += (1 -> "Go to island.")
treasureMap += (2 -> "Find big X on ground.")
treasureMap += (3 -> "Dig.")
println(treasureMap(2))
1 -> "Go to island."
相当于(1).->("Go to island.")
,相当于生成一个两个元素的Tuple
val romanNumeral = Map(
1 -> "I",
2 -> "II",
3 -> "III",
4 -> "IV",
5 -> "V" )
println(romanNumeral(4))
- 跟不可修改的Set差不多,也是工厂方法。这个工厂方法就是
Map(...)
,也就是Map.apply(...)
。
未完待续。