scala编程基础

第二步:定义一些变量

Scala有两种变量,val和var。val类似于Java里的final变量。一旦初始化了,val就不能再赋值了。与之对应的,var如同Java里面的非final变量。var可以在它生命周期中被多次赋值。下面是一个val的定义:

 

scala> val msg = "Hello, world!" msg: java.lang.String = Hello, world! 这个语句引入了msg当作字串"Hello, world!"的名字。类型是java.lang.String,因为Scala的字串是由Java的String类实现的。

 

如果你之前曾定义过Java变量,你会发现一个很醒目的差别:无论java.lang.String还是String都没有出现在val的定义中。本例演示了类型推断:type inference,这种Scala能自动理解你省略的类型的能力。在这个例子里,因为你用一个字串文本初始化了msg,Scala推断msg的类型是String。如果Scala解释器(或编译器)可以推断类型,那么让它这么做而不是写一些没必要的显式类型标注常常是最好的选择。不过,如果你愿意,也可以显式地定义类型,也许有些时候你也应该这么做。显式的类型标注不但可以确保Scala编译器推断你倾向的类型,还可以作为将来代码读者有用的文档。Java中变量的类型指定在其名称之前,与之不同的是,Scala里变量的类型在其名称之后,用冒号分隔。如: scala> val msg2: java.lang.String = "Hello again, world!" msg2: java.lang.String = Hello again, world!

 

 你对msg不能做的,因为是val而不是var,就是再给它赋值。 例如,看看你做如下尝试的时候编译器怎么报错的:

scala> msg = "Goodbye cruel world!"

<console>:5: error: reassignment to val

msg = "Goodbye cruel world!"

 

如果可重赋值是你需要的,你应使用var,如下: scala> var greeting = "Hello, world!" greeting: java.lang.String = Hello, world! 由于greeting是var而不是val,你可以在之后对它重新赋值。比如说,如果你之后心态不平了,你可以修改你的greeting为: scala> greeting = "Leave me alone, world!" greeting: java.lang.String = Leave me alone, world!

 

第三步:定义一些函数

现在你已经用过了Scala的变量,或许想写点儿函数。下面是在Scala里面的做法: scala> def max(x: Int, y: Int): Int = { if (x > y) x else y } max: (Int,Int)Int
数的定义用def开始。函数名,本例中是max,跟着是括号里带有冒号分隔的参数列表每个函数参数后面必须带前缀冒号的类型标注,因为Scala编译器(还有解释器,但之后我们将只说编译器)没办法推断函数参数类型。本例中,名叫max的函数带有两个参数,x和y,都是Int类型。在max参数列表的括号之后你会看到另一个“: Int”类型标注。这个东西定义了max函数的结果类型:result type 。跟在函数结果类型之后的是一个等号和一对包含了函数体的大括号。本例中,函数体里仅有一个if表达式,选择x或者y,哪个较大,就当作max函数的结果。就像这里演示的,Scala的if表达式可以像Java的三元操作符那样产生一个值。举例来说,Scala表达式“if (x > y) x else y”与Java里的“(x > y) ? x : y”表现得很像。在函数体前的等号提示我们函数式编程的世界观里,函数定义一个能产生值的表达式。函数的基本结构在图2.1里面演示。

 

有时候Scala编译器会需要你定义函数的结果类型。比方说,如果函数是递归的,7你就必须显式地定义函数结果类型。然而在max的例子里,你可以不用写结果类型,编译器也能够推断它 。同样,如果函数仅由一个句子组成,你可以可选地不写大括号。这样,你就可以把max函数写成这样: scala> def max2(x: Int, y: Int) = if (x > y) x else y

 

一旦你定义了函数,你就可以用它的名字调用它,如: scala> max(3, 5) res6: Int = 5 还有既不带参数也不返回有用结果的函数定义: scala> def greet() = println("Hello, world!") greet: ()Unit 当你定义了greet()函数,解释器会回应一个greet: ()Unit。“greet”当然是函数名。空白的括号说明函数不带参数。Unit是greet的结果类型。Unit的结果类型指的是函数没有返回有用的值。Scala的Unit类型比较接近Java的void类型,而且实际上Java里每一个返回void的方法都被映射为Scala里返回Unit的方法。因此结果类型为Unit的方法,仅仅是为了它们的副作用而运行。在greet()的例子里,副作用是在标准输出上打印一句客气的助词。下一步,你将把Scala代码放在一个文件中并作为脚本执行它。如果你想离开解释器,输入:quit或者:q。 scala> :quit $

 

第五步:用while循环;用if判断

 

要尝试while,在printargs.scala文件里输入以下代码: var i = 0 while (i < args.length) { println(args(i)) i += 1 } 注意 虽然本节的例子有助于解释while循环,但它们并未演示最好的Scala风格。在下一段中,你会看到避免用索引枚举数组的更好的手段。 这个脚本开始于变量定义,var i = 0。类型推断认定i的类型是scala.Int,因为这是它的初始值的类型,0。下一行里的while结构使得代码块(大括号之间的代码)重复执行直到布尔表达式i < args.length为假。args.length给出了args数组的长度。代码块包含两句话,每个都缩进两个空格,这是Scala的推荐缩进风格。第一句话,println(args(i)),输出第i个命令行参数。第二句话,i += 1,让i自增一。注意Java的++i和i++在Scala里不起作用,要在Scala里自增,必须写成要么i = i + 1,或者i += 1。用下列命令运行这个脚本:

$ scala printargs.scala Scala is fun 你将看到: Scala is fun 想要更好玩儿一些,就把下列代码输入到新文件echoargs.scala: var i = 0 while (i < args.length) { if (i != 0)

print(args(i)) i += 1 } println() 在这个版本里,用print调用替代了println调用,这样所有参数将被输出在同一行里。为了更好的可阅读性,你应该用if(i != 0)检查,除了第一个之外的每个参数前插入一个空格。由于第一次做while循环时i != 0会失败,因此在头一个参数之前不会输出空格。最后,你应该在末尾多加一个println,这样在输出所有参数之后会有一个换行。这样你的输出就非常漂亮了。如果用下面的命令运行脚本: $ scala echoargs.scala Scala is even more fun 就能得到: Scala is even more fun

注意Scala和Java一样,必须把while或if的布尔表达式放在括号里。(换句话说,就是不能像在Ruby里面那样在Scala里这么写:if i < 10。在Scala里必须写成if (i < 10)。)另外一点与Java类似的,是如果代码块仅有一个句子,大括号就是可选的,就像echoargs.scala里面if句子演示的。并且尽管你没有看到,Scala也和Java一样使用分号分隔句子的,只是Scala里的分号经常是可选的,从而可以释放你的右小手指。如果你有点儿罗嗦的脾气,那么就把echoargs.scala脚本写成下面的样子好了: var i = 0; while (i < args.length) { if (i != 0) { print(" "); } print(args(i)); i += 1; } println();


第六步:用foreach和for枚举

尽管或许你没意识到,在前一步里写while循环的时候,你正在用指令式:imperative风格编程。指令式风格,是你常常使用像Java,C++和C这些语言里用的风格,一次性发出一个指令式的命令,用循环去枚举,并经常改变共享在不同函数之间的状态。Scala允许你指令式地编程,但随着你对Scala的深入了解,你可能常会发现你自己在用一种更函数式:functional的风格编程。实际上,本书的一个主要目的就是帮助你变得对函数式风格感觉像和指令式风格一样舒适。 函数式语言的一个主要特征是,函数是第一类结构,这在Scala里千真万确。举例来说,另一种(简洁得多)打印每一个命令行参数的方法是: args.foreach(arg => println(arg))

这行代码中,你在args上调用foreach方法,并把它传入函数。此例中,你传入了带有一个叫做arg参数的函数文本:function literal。函数体是println(arg)。如果你把上述代码输入到新文件pa.scala,并使用命令执行: $ scala pa.scala Concise is nice 你会看到: Concise is nice 前例中,Scala解释器推断arg的类型是String,因为String是你调用foreach的那个数组的元素类型。如果你喜欢更显式的,你可以加上类型名,不过如此的话你要把参数部分包裹在括号里(总之这是语法的普通形式): args.foreach((arg: String) => println(arg)) 运行这个脚本的结果与前一个相同。
如果你更喜欢简洁的而不是显式的风格,就可以充分体会到Scala特别简洁的优越性。如果函数文本由带一个参数的一句话组成,你都不需要显式命名和指定参数。11
这样,下面的代码同样有效: args.foreach(println)

总而言之,函数文本的语法就是,括号里的命名参数列表,右箭头,然后是函数体。语法演示在图2.2中。

总而言之,总而言之,函数文本的语法就是,括号里的命名参数列表,右箭头,然后是函数体。语法演示在图2.2中。现在,到这里你或许想知道那些你在指令式语言如Java或C里那么信任的for循环到哪里去了呢。为了努力引导你向函数式的方向,Scala里只有一个指令式for(称为for表达式:expression)的函数式近似。目前你还看不到他们全部的力量和表达方式,直到你读到了(或者先瞄一眼)第7.3节,我们仅仅带您在这里领略一下。创建一个新文件forargs.scala,输入以下代码: for (arg <- args) println(arg)
这个表达式里“for”之后的括号包含arg<-args。<-右侧的是熟悉的args数组。<-左侧的是“arg”,val的名称(不是var)。(因为总归是val,你只要写arg就可,不要写成val arg。)尽管arg可能感觉像var,因为他在每次枚举都会得到新的值,但它的确是val : arg不能在for表达式的函数体中重新赋值。取而代之,对每个args数组的元素,一个新的arg val将被创建并初始化为元素值,然后for的函数体将被执行。 如果执行forargs.scala脚本: $ scala forargs.scala for arg in args 可以看到: for arg in args Scala的for表达式可以比这个做得更多,但是这个例子足以让你起步了。我们将在7.3节和第二十三章中展示给你更多关于for的东西。//你可以认为<-符号代表“其中”。如果要读for(arg<-args),就读做“对于args中的arg”。

结语
本章,你学习了一些Scala的基础并,但愿,利用此机会去写了一些Scala代码。下一章,我们将继续这个概况介绍并深入一些更先进的话题中去

 

 

第七步:带类型的参数化数组
Scala里可以使用new实例化对象或类实例。当你在Scala里实例化对象,可以使用值和类型把它参数化:parameterize。参数化的意思是在你创建实例的时候“设置”它。通过把加在括号里的对象传递给实例的构造器的方式来用值参数化实例。例如,下面的Scala代码实例化一个新的java.math.BigInteger并使用值"12345"参数化: val big = new java.math.BigInteger("12345") 通过在方括号里设定一个或更多类型来参数化实例。代码3.1里展示了一个例子。在这个例子中,greetStrings是类型Array[String](字串数组)的值,并被第一行代码里的值3参数化,使它的初始长度为3。如果把代码3.1里的代码作为脚本执行,你会看到另一个Hello, world!的祝词。请注意当你同时用类型和值去参数化实例的时候,类型首先在方括号中出现,然后跟着值在圆括号中。

代码3.1 用类型参数化数组

val greetStrings = new Array[String](3) greetStrings(0) = "Hello" greetStrings(1) = ", " greetStrings(2) = "world!\n" for (i <- 0 to 2) print(greetStrings(i))
注意:尽管代码3.1里的代码演示了一些重要的概念,但它没有展示Scala里创建和初始化数组的推荐方式。你会在第48页的代码3.2中看到更好的方式。

如果想用一种更显式的方式,你可以显式定义greetStrings的类型:val greetStrings: Array[String] = new Array[String](3) 由于Scala有类型推断,这行代码与代码3.1里的第一行代码语义一致。不过这种形式说明了类型参数化部分(方括号里的类型名)形成了实例类型的部分,而值参数化部分(圆括号里的值)不是。greetStrings的类型是Array[String],不是Array[String](3)。 代码3.1的下三行代码初始化了greetStrings数组的每个元素: greetStrings(0) = "Hello"  greetStrings(1) = ", " greetStrings(2) = "world!\n" 正如前面提到的,Scala里的数组是通过把索引放在圆括号里面访问的,而不是像Java那样放在方括号里。所以数组的第零个元素是greetStrings(0),不是greetStrings[0]。 这三行代码演示了搞明白Scala如何看待val的意义的重要概念。当你用val定义一个变量,那么这个变量就不能重新赋值,但它指向的对象却仍可以暗自改变。所以在本例中,你不能把greetStrings重新赋值成不同的数组;greetStrings将永远指向那个它被初始化时候指向的同一个Array[String]实例。但是你能一遍遍修改那个Array[String]的元素,因此数组本身是可变的。 代码3.1的最后两行包含一个for表达式用来依次输出每个greetStrings数组元素。 for (i <- 0 to 2) print(greetStrings(i))
这个for表达式的第一行代码演示了Scala的另一个通用规则:如果方法仅带一个参数,你可以不带点或括号的调用它。本例中的to实际上是带一个Int参数的方法。代码0 to 2被转换成方法调用(0).to(2) 请注意这个语法仅在你显示指定方法调用的接受者时才起作用。不可以写 pringln 10,但是可以写成“Console println 10”。 从技术上讲,Scala没有操作符重载,因为它根本没有传统意义上的操作符。取而代之的是,诸如+,-,*和/这样的字符可以用来做方法名。因此,当第一步里你在Scala解释器里输入1 + 2,你实际上正在Int对象1上调用一个名为+的方法,并把2当作参数传给它。如图3.1所示,你也可以使用传统的方法调用语法把1 + 2替代写成(1).+(2)。

 

这里演示的另一重要思想可以让你看到为什么数组在Scala里是用括号访问的。与Java比Scala很少有特例。数组和Scala里其他的类一样只是类的实现。当你在一个或多个值或变量外使用括号时,Scala会把它转换成对名为apply的方法调用。于是greetStrings(i)转换成greetStrings.apply(i)。所以Scala里访问数组的元素也只不过是跟其它的一样的方法调用。这个原则不仅仅局限于数组:任何对某些在括号中的参数的对象的应用将都被转换为对apply方法的调用。当然前提是这个类型实际定义过apply方法。所以这不是一个特例,而是一个通则。
与之相似的是,当对带有括号并包括一到若干参数的变量赋值时,编译器将把它转化为对带有括号里参数和等号右边的对象的update方法的调用。例如, greetStrings(0) = "Hello" 将被转化为 greetStrings.update(0, "Hello") 因此,下列Scala代码与你在代码3.1里的代码语义一致: val greetStrings = new Array[String](3) greetStrings.update(0, "Hello") greetStrings.update(1, ", ") greetStrings.update(2, "world!\n") for (i <- 0.to(2)) print(greetStrings.apply(i)) Scala在对待任何事上追求概念的简洁性,从数组到表达式,包括带有方法的对象。你不必记住太多特例,如Java里原始类型和相应的包装类间的,或者数组和正常的对象间的差别。而且这种统一并未损害重要的性能代价。Scala编译器使用Java数组,原始类型,及可存在于编译完成代码里的原生数学类型。 尽管目前为止在这一步里你看到的例子编译运行良好,Scala提供了通常可以用在你真实代码里的更简洁的方法创造和初始化数组。它看起来就像展示在代码3.2中的样子。这行代码创建了长度为3的新数组,用传入的字串"zero","one"和"two"初始化。编译器推断数组的类型是Array[String] ,因为你把字串传给它。

代码3.2 创造和初始化数组
val numNames = Array("zero", "one", "two")


你在代码3.2里实际做的就是调用了一个叫做apply的工厂方法,从而创造并返回了新的数组。apply方法带可变数量个参数,,被定义在Array的伴生对象:companion object上。你会在4.3节里学到更多关于伴生对象的东西。如果你是一个Java程序员,你可以认为这个就像在Array类上调用一个叫做apply的静态方法。更罗嗦的调用同样的apply方法的办法是: val numNames2 = Array.apply("zero", "one", "two")

 

第八步:使用List

方法不应该有副作用是函数风格编程的一个很重要的理念。方法唯一的效果应该是计算并返回值。用这种方式工作的好处就是方法之间很少纠缠在一起,因此就更加可靠和可重用。另一个好处(静态类型语言里)是传入传出方法的所有东西都被类型检查器检查,因此逻辑错误会更有可能把自己表现为类型错误。把这个函数式编程的哲学应用到对象世界里意味着使对象不可变。
如你所见,Scala数组是一个所有对象都共享相同类型的可变序列。比方说Array[String]仅包含String。尽管实例化之后你无法改变Array的长度,它的元素值却是可变的。因此,Array是可变的对象。 说到共享相同类型的不可变对象序列,Scala的List类才是。和数组一样,List[String]包含的仅仅是String。Scala的List,scala.List,不同于Java的java.util.List,总是不可变的(而Java的List可变)。更通常的说法,Scala的List是设计给函数式风格的编程用的。创建一个List很简单。

代码3.3做了展示: val oneTwoThree = List(1, 2, 3)
代码3.3 创造和初始化列表

 

代码3.3中的代码完成了一个新的叫做oneTwoThree的val,并已经用带有整数元素值1,2和3的新List[Int]初始化. 因为List是不可变的,他们表现得有些像Java的String:当你在一个List上调用方法时,似乎这个名字指代的List看上去被改变了,而实际上它只是用新的值创建了一个List并返回。比方说,List有个叫“:::”的方法实现叠加功能。你可以这么用: val oneTwo = List(1, 2) val threeFour = List(3, 4) val oneTwoThreeFour = oneTwo ::: threeFour println(oneTwo + " and " + threeFour + " were not mutated.") println("Thus, " + oneTwoThreeFour + " is a new List.") 如果你执行这个脚本,你会看到: List(1, 2) and List(3, 4) were not mutated. Thus, List(1, 2, 3, 4) is a new List. 或许List最常用的操作符是发音为“cons”的‘::’。Cons把一个新元素组合到已有List的最前端,然后返回结果List。例如,若执行这个脚本: val twoThree = list(2, 3) val oneTwoThree = 1 :: twoThree println(oneTwoThree) 你会看到: List(1, 2, 3).

//注意 表达式“1 :: twoThree”中,::是它右操作数,列表twoThree,的方法。你或许会疑惑::方法的关联性上有什么东西搞错了,不过这只是一个简单的需记住的规则:如果一个方法被用作操作符标注,如a * b,那么方法被左操作数调用,就像a.*(b)——除非方法名以冒号结尾。这种情况下,方法被右操作数调用。因此,1 :: twoThree里,::方法被twoThree调用,传入1,像这样:twoThree.::(1)。 5.8节中将描述更多操作符关联性的细节。//

由于定义空类的捷径是Nil,所以一种初始化新List的方法是把所有元素用cons操作符串起来,Nil作为最后一个元素. 比方说,下面的脚本将产生与之前那个同样的输出,“List(1, 2, 3)”: val oneTwoThree = 1 :: 2 :: 3 :: Nil println(oneTwoThree) Scala的List包装了很多有用的方法,表格3.1罗列了其中的一些。列表的全部实力将在第十六章释放。

为什么列表不支持append? 类List没有提供append操作,因为随着列表变长append的耗时将呈线性增长,而使用::做前缀则仅花费常量时间。如果你想通过添加元素来构造列表,你的选择是把它们前缀进去,当你完成之后再调用reverse;或使用ListBuffer,一种提供append操作的可变列表,当你完成之后调用toList。ListBuffer将在22.2节中描述。

表格3.1 类型List的一些方法和作用

List() 或 Nil
空List


List("Cool", "tools", "rule)
创建带有三个值"Cool","tools"和"rule"的新List[String]


val thrill = "Will"::"fill"::"until"::Nil
创建带有三个值"Will","fill"和"until"的新List[String]


List("a", "b") ::: List("c", "d")
叠加两个列表(返回带"a","b","c"和"d"的新List[String])
thrill(2)
返回在thrill列表上索引为2(基于0)的元素(返回"until")
thrill.count(s => s.length == 4)
计算长度为4的String元素个数(返回2)
thrill.drop(2)
返回去掉前2个元素的thrill列表(返回List("until"))
thrill.dropRight(2)
返回去掉后2个元素的thrill列表(返回List("Will"))
thrill.exists(s => s == "until")
判断是否有值为"until"的字串元素在thrill里(返回true)
thrill.filter(s => s.length == 4)
依次返回所有长度为4的元素组成的列表(返回List("Will", "fill"))
thrill.forall(s => s.endsWith("1"))
辨别是否thrill列表里所有元素都以"l"结尾(返回true)
thrill.foreach(s => print(s))
对thrill列表每个字串执行print语句("Willfilluntil")
thrill.foreach(print)
与前相同,不过更简洁(同上)
thrill.head
返回thrill列表的第一个元素(返回"Will")
thrill.init
返回thrill列表除最后一个以外其他元素组成的列表(返回List("Will", "fill"))
thrill.isEmpty
说明thrill列表是否为空(返回false)
thrill.last
返回thrill列表的最后一个元素(返回"until")
thrill.length
返回thrill列表的元素数量(返回3)
thrill.map(s => s + "y")
返回由thrill列表里每一个String元素都加了"y"构成的列表(返回List("Willy", "filly", "untily"))
thrill.mkString(", ")
用列表的元素创建字串(返回"will, fill, until")

thrill.remove(s => s.length == 4)
返回去除了thrill列表中长度为4的元素后依次排列的元素列表(返回List("until"))

thrill.reverse
返回含有thrill列表的逆序元素的列表(返回List("until", "fill", "Will"))
thrill.sort((s, t) => s.charAt(0).toLowerCase < t.charAt(0).toLowerCase)
返回包括thrill列表所有元素,并且第一个字符小写按照字母顺序排列的列表(返回List("fill", "until", "Will"))
thrill.tail
返回除掉第一个元素的thrill列表(返回List("fill", "until"))

 

第九步:使用Tuple

另一种有用的容器对象是元组:tuple。与列表一样,元组也是不可变的,但与列表不同,元组可以包含不同类型的元素。而列表应该是List[Int]或List[String]的样子,元组可以同时拥有Int和String。元组很有用,比方说,如果你需要在方法里返回多个对象。Java里你将经常创建一个JavaBean样子的类去装多个返回值,Scala里你可以简单地返回一个元组。而且这么做的确简单:实例化一个装有一些对象的新元组,只要把这些对象放在括号里,并用逗号分隔即可。一旦你已经实例化了一个元组,你可以用点号,下划线和一个基于1的元素索引访问它。代码3.4展示了一个例子:代码3.4 创造和使用元组

val pair = (99, "Luftballons") println(pair._1) println(pair._2)

代码3.4的第一行,你创建了元组,它的第一个元素是以99为值的Int,第二个是"luftballons"为值的String。Scala推断元组类型为Tuple2[Int, String],并把它赋给变量pair。第二行,你访问_1字段,从而输出第一个元素,99。第二行的这个“.”与你用来访问字段或调用方法的点没有区别。本例中你正用来访问名叫_1的字段。如果执行这个脚本,你能看到: 99 Luftballons  。元组的实际类型取决于它含有的元素数量和这些元素的类型。因此,(99, "Luftballons")的类型是Tuple2[Int, String]。('u', 'r', 'the', 1, 4, "me")是Tuple6[Char, Char, String, Int, Int, String]

。访问元组的元素 你或许想知道为什么你不能像访问List里的元素那样访问元组的,就像pair(0)。那是因为List的apply方法始终返回同样的类型,但是元组里的或许类型不同。_1可以有一个结果类型,_2是另外一个,诸如此类。这些_N数字是基于1的,而不是基于0的,因为对于拥有静态类型元组的其他语言,如Haskell和ML,从1开始是传统的设定。

 

第十步:使用Set和Map
因为Scala致力于帮助你充分利用函数式和指令式风格两方面的好处,它的集合类型库于是就区分了集合类的可变和不可变。例如,数组始终是可变的,而列表始终不可变。当问题讨论到集和映射,Scala同样提供了可变和不可变的替代品,不过用了不同的办法。对于集和映射,Scala把可变性建模在类继承中。 例如,Scala的API包含了集的一个基本特质:trait,特质这个概念接近于Java的接口。(你将在第12章找到更多关于特质的说明。)Scala于是提供了两个子特质,一个是可变的集,另一个是不可变的集。就如你在图3.2里会看到的,这三个特质都共享同样的简化名,Set。然而它们的全称不一样,因为每个都放在不同的包里。Scala的API里具体的Set类,如图3.2的HashSet类,扩展了要么是可变的,要么不可变的Set特质。(尽管Java里面称为“实现”了接口,在Scala里面称为“扩展”或“混入”了特质。)因此,如果你想要使用HashSet,你可以根据你的需要选择可变的或不可变的变体。创造集的缺省方法展示在代码3.5中:代码3.5 创造,初始化,和使用不可变集

var jetSet = Set("Boeing", "Airbus") jetSet += "Lear" println(jetSet.contains("Cessna"))

代码3.5的第一行代码里,定义了名为jetSet的新var,并使用了包含两个字串,"Boeing"和"Airbus"的不可变集完成了初始化。就像例子中展示的,Scala中创建集的方法与创建列表和数组的类似:通过调用Set伴生对象的名为apply的工厂方法。代码3.5中,对scala.collection.immutable.Set的伴生对象调用了apply方法,返回了一个缺省的,不可变Set的实例。Scala编译器推断jetSet的类型为不可变Set[String]。

要向集加入新的变量,可以在集上调用+,传入新的元素。可变的和不可变的集都提供了+方法,但它们的行为不同。可变集将把元素加入自身,不可变集将创建并返回一个包含了添加元素的新集。代码3.5中,你使用的是不可变集,因此+调用将产生一个全新集。因此尽管可变集提供的实际上是+=方法,不可变集却不是。本例中,代码的第二行,“jetSet += "Lear"”,实质上是下面写法的简写: jetSet = jetSet + "Lear" 因此在代码3.5的第二行,你用一个包含了"Boeing","Airbus"和"Lear"的新集重新赋值了jetSet这个var。最终,代码3.5的最后一行打印输出了集是否包含字串"Cessna"。(正如你所料到的,输出false。) 如果你需要不可变集,就需要使用一个引用:import,如代码3.6所示:代码3.6 创建,初始化,和使用可变集

import scala.collection.mutable.Set val movieSet = Set("Hitch", "Poltergeist") movieSet += "Shrek" println(movieSet)

代码3.6的第一行里引用了可变Set。就像Java那样,引用语句允许你使用简单名,如Set,以替代更长的,全标识名。结果,当你在第三行写Set的时候,编译器就知道你是指scala.collection.mutable.Set。在那行里,你使用包含字串"Hitch"和"Poltergeist"的新可变集初始化了movieSet。下一行通过在集上调用+=方法向集添加了"Shrek"。正如前面提到的,+=是实际定义在可变集上的方法。如果你想的话,你可以替换掉movieSet += "Shrek"的写法,写成movieSet.+=("Shrek")。6
6 因为代码3.6里的集是可变的,所以不需要对movieSet重新赋值,所以它是val。相对的,在代码3.5中对可变集使用+=需要对jetSet重新赋值,因此它是var。 尽管目前为止看到的通过可变和不可变的Set工厂方法制造的缺省的集实现很可能能够满足极大多数的情况,但偶尔你也或许想要个显式的集类。幸运的是,语法是相同的。只要引用你需要的类,并使用它伴生对象的工厂方法即可。例如,如果你需要一个不可变的HashSet,你可以这么做: import scala.collection.immutable.HashSet val hashSet = HashSet("Tomatoes", "Chilies") println(hashSet + "Coriander") Map是Scala里另一种有用的集合类。和集一样,Scala采用了类继承机制提供了可变的和不可变的两种版本的Map,你能在图3.3里看到,Map的类继承机制看上去和Set的很像。scala.collection包里面有一个基础Map特质和两个子特质Map:可变的Map在scala.collection.mutable里,不可变的在scala.collection.immutable里。 Map的实现,如显示在类继承图3.3里的HashMap,扩展了要么可变,要么不可变特质。你可以使用与那些用在数组,列表和集中的一样的工厂方法去创造和初始化映射。例如,代码3.7展示了可变映射的创造过程:

import scala.collection.mutable.Map val treasureMap = Map[Int, String]() treasureMap += (1 -> "Go to island.") treasureMap += (2 -> "Find big X on ground.") treasureMap += (3 -> "Dig.") println(treasureMap(2))

 

代码3.7的第一行里,你引用了可变形式的Map。然后就定义了一个叫做treasureMap的val并使用空的包含整数键和字串值的可变Map初始化它。映射为空是因为你没有向工厂方法传递任何值(“Map[Int, String]()”的括号里面是空的)。7下面的三行里你使用->和+=方法把键/值对添加到Map里。像前面例子里演示的那样,Scala编译器把如1 -> "Go to island"这样的二元操作符表达式转换为(1).->("Go to island.")。因此,当你输入1 -> "Go to island.",你实际上是在值为1的Int上调用->方法,并传入值为"Go to island."的String。这个->方法可以调用Scala程序里的任何对象,并返回一个包含键和值的二元元组。8
代码3.8 创造,初始化,和使用不可变映射然后你在把这个元组传递给treasureMap指向的Map的+=方法。最终,最后一行输出打印了treasureMap中的与键2有关的值。如果你执行这段代码,将会打印: Find big X on ground. 如果你更喜欢不可变映射,就不用引用任何类了,因为不可变映射是缺省的,代码3.8展示了这个例子:

val romanNumeral = Map( 1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V" ) println(romanNumeral(4))

由于没有引用,当你在代码3.8的第一行里提及Map时,你会得到缺省的映射:scala.collection.immutable.Map。传给工厂方法入五个键/值元组,返回包含这些传入的键/值对的不可变Map。如果你执行代码3.8中的代码,将会打印输出IV。

 

 第十一步:学习识别函数式风格
第1章里提到过,Scala允许你用指令式风格编程,但是鼓励你采用一种更函数式的风格。如果你是从指令式的背景转到Scala来的——例如,如果你是Java程序员——那么学习Scala是你有可能面对的主要挑战就是理解怎样用函数式的风格编程。我们明白这种转变会很困难,在本书中我们将竭尽所能把你向这方面引导。不过这也需要你这方面的一些工作,我们鼓励你付出努力。如果你来自于指令式的背景,我们相信学习用函数式风格编程将不仅让你变成更好的Scala程序员,而且还能拓展你的视野并使你变成通常意义上好的程序员。 通向更函数式风格路上的第一步是识别这两种风格在代码上的差异。其中的一点蛛丝马迹就是,如果代码包含了任何var变量,那它大概就是指令式的风格。如果代码根本就没有var——就是说仅仅包含val——那它大概是函数式的风格因此向函数式风格推进的一个方式,就是尝试不用任何var编程。 如果你来自于指令式的背景,如Java,C++,或者C#,你或许认为var是很正统的变量而val是一种特殊类型的变量。相反,如果你来自于函数式背景,如Haskell,OCamel,或Erlang,你或许认为val是一种正统的变量而var有亵渎神灵的血统。然而在Scala看来,val和var只不过是你工具箱里两种不同的工具。它们都很有用,没有一个天生是魔鬼。Scala鼓励你学习val,但也不会责怪你对给定的工作选择最有效的工具。尽管或许你同意这种平衡的哲学,你或许仍然发现第一次理解如何从你的代码中去掉var是很挑战的事情。 考虑下面这个改自于第2章的while循环例子,它使用了var并因此属于指令式风格: def printArgs(args: Array[String]): Unit = { var i = 0 while (i < args.length) { println(args(i)) i += 1 } } 你可以通过去掉var的办法把这个代码变得更函数式风格,例如,像这样: def printArgs(args: Array[String]): Unit = { for (arg <- args) println(arg) } 或这样: def printArgs(args: Array[String]): Unit = { args.foreach(println) } 这个例子演示了减少使用var的一个好处。重构后(更函数式)的代码比原来(更指令式)的代码更简洁,明白,也更少机会犯错。Scala鼓励函数式风格的原因,实际上也就是因为函数式风格可以帮助你写出更易读懂,更不容易犯错的代码。 当然,你可以走得更远。重构后的printArgs方法并不是纯函数式的,因为它有副作用——本例中,其副作用是打印到标准输出流。函数有副作用的马脚就是结果类型为Unit。如果某个函数不返回任何有用的值,就是说其结果类型为Unit,那么那个函数唯一能让世界有点儿变化的办法就是通过某种副作用。更函数式的方式应该是定义对需打印的arg进行格式化的方法,但是仅返回格式化之后的字串,如代码3.9所示:

def formatArgs(args: Array[String]) = args.mkString("\n")  //代码3.9 没有副作用或var的函数

现在才是真正函数式风格的了:满眼看不到副作用或者var。能在任何可枚举的集合类型(包括数组,列表,集和映射)上调用的mkString方法,返回由每个数组元素调用toString产生结果组成的字串,以传入字串间隔。因此如果args包含了三个元素,"zero","one"和"two",formatArgs将返回"zero\none\ntwo"。当然,这个函数并不像printArgs方法那样实际打印输出,但可以简单地把它的结果传递给println来实现: println(formatArgs(args)) 每个有用的程序都可能有某种形式的副作用,因为否则就不可能对外部世界提供什么值。偏好于无副作用的方法可以鼓励你设计副作用代码最少化了的程序。这种方式的好处之一是可以有助于使你的程序更容易测试。举例来说,要测试本节之前给出三段printArgs方法的任一个,你将需要重定义println,捕获传递给它的输出,并确信这是你希望的。相反,你可以通过检查结果来测试formatArgs: val res = formatArgs(Array("zero", "one", "two")) assert(res == "zero\none\ntwo") Scala的assert方法检查传入的Boolean并且如果是假,抛出AssertionError。如果传入的Boolean是真,assert只是静静地返回。你将在第十四章学习更多关于断言和测试的东西。虽如此说,不过请牢记在心:不管是var还是副作用都不是天生邪恶的。Scala不是强迫你用函数式风格编任何东西的纯函数式语言。它是一种指令式/函数式混合的语言。你或许发现在某些情况下指令式风格更符合你手中的问题,在这时候你不应该对使用它犹豫不决。然而,为了帮助你学习如何不使用var编程,在第7章中我们会给你看许多有var的特殊代码例子和如何把这些var转换为val。

//Scala程序员的平衡感 崇尚val,不可变对象和没有副作用的方法。 首先想到它们。只有在特定需要和判断之后才选择var,可变对象和有副作用的方法。//

第十二步:从文件里读取信息行
处理琐碎的,每日工作的脚本经常需要处理文件。本节中,你将建立一个从文件中读行记录,并把行中字符个数前置到每一行,打印输出的脚本。第一版展示在代码3.10中:

import scala.io.Source if (args.length > 0) { for (line <- Source.fromFile(args(0)).getLines) print(line.length + " " + line) } else Console.err.println("Please enter filename")
代码3.10 从文件中读入行

此脚本开始于从包scala.io引用名为Source的类。然后检查是否命令行里定义了至少一个参数。若是,则第一个参数被解释为要打开和处理的文件名。表达式Source.fromFile(args(0)),尝试打开指定的文件并返回一个Source对象,你在其上调用getLines。函数返回Iterator[String],在每个枚举里提供一行包括行结束符的信息。for表达式枚举这些行并打印每行的长度,空格和这行记录。如果命令行里没有提供参数,最后的else子句将在标准错误流中打印一条信息。如果你把这些代码放在文件contchars1.scala,并运行它调用自己: $ scala countchars1.scala countchars1.scala 你会看到: 23 import scala.io.Source 1 23 if (args.length > 0) { 1 50 for (line <- Source.fromFile(args(0)).getLines) 36 print(line.length + " " + line) 2 } 5 else 47 Console.err.println("Please enter filename") 尽管当前形式的脚本打印出了所需的信息,你或许希望能让数字右序排列,并加上管道符号,这样输出看上去就替换成: 23 | import scala.io.Source1 | 23 | if (args.length > 0) { 1 | 50 | for (line <- Source.fromFile(args(0)).getLines) 36 | print(line.length + " " + line) 2 | } 5 | else 47 | Console.err.println("Please enter filename")
想要达到这一点,你可以对所有行枚举两次。第一次决定每行字符计数的最大宽度。第二次打印输出之前计算的最大宽度。因为要枚举两次,你最好把它们赋给变量:

val lines = Source.fromFile(args(0)).getLines.toList 最后的toList是必须加的,因为getLines方法返回的是枚举器。一旦你使用它完成遍历,枚举器就失效了。而通过调用toList把它转换为List,你就可以枚举任意次数,代价就是把文件中的所有行一次性贮存在内存里。lines变量因此就指向着包含了命令行指定的文件文本字串的数组。 下一步,因为要对每行字符数计算两次,每个枚举计算一次,你或许会考虑把表达式拉出来变成一个小函数,专门用来计算传入字串的字符长度: def widthOfLength(s: String) = s.length.toString.length 有了这个函数,你就可以计算最大长度了: var maxWidth = 0 for (line <- lines) maxWidth = maxWidth.max(widthOfLength(line)) 这里你用一个for表达式枚举了每一行,计算这些行的宽度,并且,如果比当前最大宽度还大,就把它赋值给maxWidth,一个初始化为0的var。(max方法是你可以在任何Int上调用的,可以返回被调用者和被传入者中的较大的值。)如果你希望不用var发现最大值,替代的方法是可以首先找到最长的一行,如: val longestLine = lines.reduceLeft( (a, b) => if (a.length > b.length) a else b ) val widths = lines.map(widthOfLength)  reduceLeft方法把传入的方法应用于lines的前两个元素,然后再应用于第一次应用的结果和lines接下去的一个元素,等等,直至整个列表。每次这样的应用,结果将是碰到的最长一行,因为传入的函数,(a, b) => if (a.length > b.length) a else b,返回两个传入字串的最长那个。reduceLeft将传回最后一次应用的结果,也就是本例lines中包含的最长字串。 得到这个结果之后,你可以通过把最长一行传给widthOfLength计算最大的宽度: val maxWidth = widthOfLength(longestLine) 最后剩下的就是用一个合适的格式把这些行打印出来。你可以这么做: for (line <- lines) { val numSpaces = maxWidth - widthOfLength(line) val padding = " " * numSpaces print(padding + line.length + " | " + line) } 在这个for表达式里,你再一次枚举了全部行记录。对于每一行,首先计算行长度前所需的空格并把它赋给numSpaces。然后用表达式:" " * numSpaces创建包含numSpaces个空格的字串。最终,你打印出你想要格式的信息。全部的脚本展示在代码3.11中:

import scala.io.Source

def widthOfLength(s: String) = s.length.toString.length if (args.length > 0) { val lines = Source.fromFile(args(0)).getLines.toList val longestLine = lines.reduceLeft( (a, b) => if (a.length > b.length) a else b ) val maxWidth = widthOfLength(longestLine) for (line <- lines) { val numSpaces = maxWidth widthOfLength(line) val padding = " " * numSpaces print(padding + line.length +" | "+ line) } } else Console.err.println("Please enter filename")
//代码3.11 对文件的每行记录打印格式化的字符数量

结语
有了本章获得的知识,你应该已经可以开始在小任务,尤其是脚本里使用Scala。下一章里,我们将在这些话题上深入更多细节,并介绍其它这里没有提到过的话题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值