您所在的位置:开发 > Java > Java+ > Scala编程指南 更少的字更多的事(1)

Scala编程指南 更少的字更多的事(1)

2010-09-14 14:28 bbsmrdj译 51CTO 字号:T | T
一键收藏,随时查看,分享好友!

本文为《Programming Scala》的中文译文《Scala 编程指南》的第二章,在前文中我们已经简单的介绍了Scala语言的基础以及安装、试用,在本章中我们将详细介绍如何使用Scala 来写出精炼的,灵活的代码。

AD:

本文为《Programming Scala》的中文译文《Scala 编程指南》的第二章,在《Scala语言编程入门指南》我们介绍了Scala语言编程的入门,在上一章中我们以几个撩拨性质的Scala 代码范例作为章节结束,在本章中我们将详细介绍如何使用Scala 来写出精炼的,灵活的代码。

51CTO推荐专题:Scala编程语言

章节概要

在这一章我们将讨论如何使用Scala 来写出精炼的,灵活的代码。我们会讨论文件和包的组织结构,导入其他的类型和变量声明,一些语法习惯和概念。我们会着重讨论Scala 简明的语法如何帮助你更好更快地工作。

Scala 的语法对于书写脚本特别有用。单独的编译和运行步骤对于简单的,仅有少量独立于Scala 提供的库之外的程序不是必须的。你可以用scala 命令一次性编译和运行这些程序。如果你已经下载了本书的实例代码,它们中的许多小程序可以用scala 命令来运行,比如scala filename.scala。参见每一章节代码实例中的README.txt 可以获取更多细节。也可以参见《第14章 - Scala 工具,库和IDE 支持》中的“命令行工具”章节来获取更多使用scala 命令的信息。

分号

你可能已经注意到,在上一章的代码示例中很少有分号出现。你可以使用分号来隔离各个声明和表达式,就像Java,C,PHP 以及其他类似的语言一样。然而在大多数情况下,Scala 的行为和许多脚本语言一样把一行的结尾看作是声明或者表达式的结尾。当一个声明或者表达式太长,一行不够的时候,Scala 通常可以推断你在什么时候要在下一行继续,就像这个例子中一样。

  1. // code-examples/TypeLessDoMore/semicolon-example-script.scala  
  2.  
  3. // Trailing equals sign indicates more code on next line  
  4. def equalsign = {  
  5.   val reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine =  
  6.     "wow that was a long value name" 
  7.   println(reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine)  
  8. }  
  9.  
  10. // Trailing opening curly brace indicates more code on next line  
  11. def equalsign2(s: String) = {  
  12.   println("equalsign2: " + s)  
  13. }  
  14.  
  15. // Trailing comma, operator, etc. indicates more code on next line  
  16. def commas(s1: String,  
  17.            s2: String) = {  
  18.   println("comma: " + s1 +  
  19.           ", " + s2)  
  20. }  

当你需要在同一行中放置多个声明或者表达式的时候,你可以使用分号来隔开它们。我们在《第1章 - 从0分到60分:Scala 介绍》的“初尝并发”章节中的ShapeDrawingActor 示例里面使用了这样的技巧。

  1. case "exit" => println("exiting..."); exit 

这样的代码也可以写成如下的样子。

  1. ...  
  2. case "exit" => 
  3.   println("exiting...")  
  4.   exit  
  5. ... 

你可能会想为什么在case... => 这行的后面不需要用大括号{ } 把两个语句括起来。。如果你想,你可以这么做,但是编译器其实会知道你什么时候会到达语句块的结尾,因为它会看到下一个case 块或者终结所有case 块的大括号。

省略可选的分号意味着更少的符号输入和更少的符号混乱。把各个语句隔离到它们自己单独的行上面可以提高你的代码的可读性。

变量声明

当你声明一个变量的时候,Scala 允许你来决定它是不变的(只读的)还是可变的(可读写的)。一个不变的“变量”可以用val 关键字来声明(想象一个值对象)。

  1. val array: Array[String] = new Array(5) 

更准确的说,这个引用array 不能被修改指向另外一个Array (数组),但是这个数组本身可以被修改,正如下面的scala 会话中演示的。

  1. scala> val array: Array[String] = new Array(5)  
  2. array: Array[String] = Array(null, null, null, null, null)  
  3. scala> array = new Array(2)  
  4. :5: error: reassignment to val  
  5.        array = new Array(2)  
  6.         ^  
  7. scala> array(0) = "Hello"  
  8. scala> array  
  9. res3: Array[String] = Array(Hello, null, null, null, null)  
  10. scala> 

一个不变的val 必须被初始化,也就是说在声明的时候就必须定义。

一个可变的变量用关键字var 来声明。

  1. scala> var stockPrice: Double = 100.  
  2. stockPrice: Double = 100.0  
  3. scala> stockPrice = 10.  
  4. stockPrice: Double = 10.0  
  5. scala> 

Scala 同时也要求你在声明一个var 时将其初始化。你可以在需要的时候给一个var 赋予新的值。这里再次严谨说明一下:引用stockPrice 可以被修改指向一个不一样的Double 对象(比如10)。在这个例子里,stockPrice 引用的对象不能被修改,因为Double 在Scala 里是不可变的。

在这里,对于val 和var 声明时即定义的规则有一些例外。这两个关键字都可以被用作构造函数参数。当作为构造函数参数时,这些可变或者不可变的变量会在一个对象被实例化的时候被初始化。两个关键字可以在抽象类型中被用来声明“抽象”(没有初始化的)的变量。同时,继承类型可以重写在父类型中声明的值。我们会在《第5章 - Scala 基础面向对象编程》中讨论这些例外。

Scala 鼓励你在任何可能的时候使用不可变的值。正如我们即将看到的,这会促进更佳的面向对象设计,而且这和“纯”函数式编程的原则相一致。

注意

var 和val 关键字指定了该引用能否被修改指向另一个对象。它们并不指定它们引用的对象是否可变。

方法声明

我们在《第1章 - 从0分到60分:Scala 介绍》中见到了几个如何定义方法的例子,它们都是类的成员函数。方法定义由一个def 关键字开始,紧接着是可选的参数列表,一个冒号“:” 和方法的返回类型,一个等于号“=”,最后是方法的主体。如果你不写等于号和方法主体,那么方法会被隐式声明为“抽象”。包含它的类型于是也是一个抽象类型。我们会在《第5章,Scala 基础面向对象编程》中详细讨论抽象类型。

我们刚才说到“可选的参数列表”,这意味着一个或更多。Scala 可以让你为方法定义一个以上的参数列表。这是级联方法(currying methods)所需要的。我们会在《第8章 - Scala 函数式编程》中的“级联(Currying)章节讨论它。这个功能对于定义你自己的域特定语言(DSLs)也很有帮助。我们会在《第11章 - Scala 中的域特定语言》 中看到它。注意,每一个参数列表会被括号所包围,并且所有的参数由逗号隔开。

如果一个方法的主体包含多于一个的表达式,你必须用大括号{ } 来把它们包起来。你可以在方法主体只有一个表达式的时候省略大括号。

方法的默认参数和命名参数(Scala 版本2.8)

许多语言都允许你为一个方法的一个或多个参数定义默认值。考虑下面的脚本,一个StringUtil 对象允许你用一个用户定义的分隔符来连接字符串。

  1. // code-examples/TypeLessDoMore/string-util-v1-script.scala  
  2. // Version 1 of "StringUtil".  
  3. object StringUtil {  
  4.   def joiner(strings: List[String], separator: String): String =  
  5.     strings.mkString(separator)  
  6.   def joiner(strings: List[String]): String = joiner(strings, " ")  
  7. }  
  8. import StringUtil._  // Import the joiner methods.  
  9. println( joiner(List("Programming", "Scala")) ) 

实际上,有两个“重载”的jioner 方法。第二个方法使用了一个空格作为“默认”分隔符。写两个函数似乎有点浪费,如果我们能消除第二个joiner 方法,在第一个jioner 方法里为separator 参数声明一个默认值,那就太好了。事实上,在Scala 2.8 版本里,你可以这么做。

  1. // code-examples/TypeLessDoMore/string-util-v2-v28-script.scala  
  2. // Version 2 of "StringUtil" for Scala v2.8 only.  
  3. object StringUtil {  
  4.   def joiner(strings: List[String], separator: String = " "): String =  
  5.     strings.mkString(separator)  
  6. }  
  7. import StringUtil._  // Import the joiner methods.println(joiner(List("Programming", "Scala")))  

对于早些版本的Scala 还有另外一种选择。你可以使用隐式参数,我们会在《第8章 - Scala 函数式编程》的“隐式函数参数”章节讨论。

2.8 版本的Scala 提供了另外一种对方法参数列表进行增强,就是命名参数。我们实际上可以用多种方法重写上一个例子的最后一行。下面所有的println 语句在功能上都是一致的。

  1. println(joiner(List("Programming", "Scala")))  
  2. println(joiner(strings = List("Programming", "Scala")))  
  3. println(joiner(List("Programming", "Scala"), " "))   // #1  
  4. println(joiner(List("Programming", "Scala"), separator = " ")) // #2  
  5. println(joiner(strings = List("Programming", "Scala"), separator = " ")) 

为什么这样有用呢?第一,如果你为方法参数选择了好的名字,那么你对那些函数的调用事实上为每一个参数记载了一个名字。举例来说,比较注释#1 和#2 的两行。在第一行,第二个参数“ ”的用处可能不是很明显。在第二行中,我们提供了参数名separator,同时也暗示了参数的用处。

第二个好处则是你可以以任何顺序指定参数的顺序。结合默认值,你可以像下面这样写代码

  1. // code-examples/TypeLessDoMore/user-profile-v28-script.scala  
  2. // Scala v2.8 only.  
  3. object OptionalUserProfileInfo {  
  4.   val UnknownLocation = "" 
  5.   val UnknownAge = -1  
  6.   val UnknownWebSite = "" 
  7. }  
  8.  
  9. class OptionalUserProfileInfo(  
  10.   location: String = OptionalUserProfileInfo.UnknownLocation,  
  11.   age: Int         = OptionalUserProfileInfo.UnknownAge,  
  12.   webSite: String  = OptionalUserProfileInfo.UnknownWebSite)  
  13.  
  14. println( new OptionalUserProfileInfo )  
  15. println( new OptionalUserProfileInfo(age = 29) )  
  16. println( new OptionalUserProfileInfo(age = 29location="Earth") )  

OptionalUserProfileInfo 为你的下一个Web 2.0 社交网站提供了“可选的”用户概要信息。它定义了所有字段的默认值。这段脚本在创建实例的时候提供了0个或者更多的命名参数。而参数的顺序却是任意的。

在这个我们展示的例子里,常量值被用来作为默认值。大多数支持默认参数的语言只允许编译时能决定的常量或者值作为默认值。然而,在Scala 里,任何表达式都可以被作为默认值,只要它可以在被使用的时候正确编译。比如说,一个表达式不能引用类或者对象主体内才被计算的实例字段,但是它可以引用一个方法或者一个单例对象。

一个类似的限制是一个参数的默认表达式不能引用列表中的另外一个参数,除非被引用的参数出现在列表的更前面,或者参数已经被级联(我们会在《第8章 - Scala 函数式编程》的“级联”这一章节详细讨论)。

最后,还有一个对命名参数的约束就是一旦你为一个方法掉哦那个指定了参数名称,那么剩下的在这个参数之后的所有参数都必须是命名参数。比如,new OptionalUserProfileInfo(age =29, "Earch") 就不能被编译,因为第二个参数不是通过命名方式调用的。

我们会在《第6章 - Scala 高级面向对象编程》中的“Case Class(案例类)”中看到另外一个使用命名参数和默认参数的例子。

嵌套方法定义

方法定义也可以被嵌套。这里是一个阶乘计算器的实现,我们会使用一种常规的方法,通过调用第二个,嵌套的方法来完成计算。

  1. // code-examples/TypeLessDoMore/factorial-script.scala  
  2. def factorial(i: Int): Int = {  
  3.   def fact(i: Int, accumulator: Int): Int = {  
  4.     if (i <= 1)  
  5.       accumulator  
  6.     else  
  7.       fact(i - 1, i * accumulator)  
  8.   }  
  9.   fact(i, 1)  
  10. }  
  11.  
  12. println( factorial(0) )  
  13. println( factorial(1) )  
  14. println( factorial(2) )  
  15. println( factorial(3) )  
  16. println( factorial(4) )  
  17. println( factorial(5) )  

第二个方法递归地调用了自己,传递一个accumulator 参数,这个参数是计算结果累积的地方。注意,我们当计数器i 达到1 的时候返回了累积的值。(我们会忽略负整数。实际上这个函数在i<0 的时候会返回1 。)在嵌套方法的定义后面,factorial 以传入值i 和初始accumulator 值1 来调用它。

就像很多语言中声明局部变量一样,一个嵌套方法尽在方法内部可见。如果你尝试在factorial 之外去调用fact,你会得到一个编译错误。

你注意到了吗,我们两次把i 作为一个参数名字,第一次是在factorial 方法里,然后是在嵌套的fact 方法里。就像在其它许多语言中一样,在fact 中的i 参数会屏蔽掉外面factorial 的i 参数。这样很好,因为我们在fact 中不需要在外面的i 的值。我们只在第一次调用fact 的时候需要它,也就是在factorial 的最后。

那如果我们需要使用定义在嵌套函数外面的变量呢?考虑下面的例子。

  1. // code-examples/TypeLessDoMore/count-to-script.scala  
  2. def countTo(n: Int):Unit = {  
  3.   def count(i: Int): Unit = {  
  4.     if (i <= n) {  
  5.       println(i)  
  6.       count(i + 1)  
  7.     }  
  8.   }  
  9.   count(1)  
  10. }  
  11. countTo(5) 

注意嵌套方法count 使用了作为参数传入countTo 的n 的值。这里没有必要把n 作为参数传给count。因为count 嵌套在countTo 里面,所以n对于count 来说是可见的。

字段(成员变量)的声明可以用可见程度关键字来做前缀,就像Java 和C# 这样的语言一样。和非嵌套方法的生命类似,这些嵌套方法也可以用这些关键字来修饰。我们会在《第5章 - Scala 面向对象编程》中的“可见度规则”章节来讨论可见度的规则和对应的关键字。

内容导航
 第 1 页:本文概要  第 2 页:类型推断
 第 3 页:各种赋值  第 4 页:用文件和名称空间来组织代码



分享到:

热点职位

更多>>

热点专题

更多>>

读书

构件中国:面向构件的方法与实践
本书通过丰富的案例研究示例,阐明了构建面向构件软件的最重要因素:概念、技术、规范、管理以及分析与设计过程。 本书的涵盖范

51CTO旗下网站

领先的IT技术网站 51CTO 领先的中文存储媒体 WatchStor 中国首个CIO网站 CIOage 中国首家数字医疗网站 HC3i