欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

scala入门

程序员文章站 2024-01-23 09:27:04
...

scala入门

SCALA,英文名:Scalable Language;中文名:可伸缩的语言, 是一门多范式的编程语言,一种类似java的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。

1       安装 Scala

    这个章节描述了如何安装Scala 的命令行工具, 以便可以尽快让Scala 跑起来。

    访问Scala 的官方网站 。要安装Scala,去到下载页面(http://www.scala-lang.org/download/2.11.6.html)。按照下载页面上的指示下载适合你系统环境的安装包。

²  Windows环境

双击执行scala-2.11.6.msi即可。

要测试你的安装,在命令行下运行如下命令:

 scala -version  

你应该能获得如下输出:

 Scala code runner version 2.11.6 -- Copyright 2002-2013, LAMP/EPFL

     当然,你看到的版本号会根据你安装的版本而改变。从现在起,当我们展示命令行输出时候如果包含版本号,我们会使用2.11.6

2       初尝 Scala

作为第一个实例,你可以用两种方式来运行它:交互式的,或者作为一个“脚本”。

2.1             交互式运行方式

让我们从交互式模式开始。我们可以通过在命令行输入scala,回车,来启动scala 解释器。你会看到如下输出。(版本号可能会有所不同。)

Welcome to Scala version 2.8.0.final (Java ...).  

Type in expressions to have them evaluated.  

Type :help for more information.  

scala> 

最后一行是等待你输入的提示符。交互式的scala 命令对于实验来说十分方便。一个像这样的交互式解释器被称为REPL:读(Read),评估(Evaluate),打印(Print),循环(Loop

输入如下的两行代码。

val book = "Programming Scala" 

println(book) 

实际上的输入和输出看起来会像是这样。

scala> val book = "Programming Scala" 

book: java.lang.String = Programming Scala  

scala> println(book)  

Programming Scala  

scala> 

在第一行我们使用了val 关键字来声明一个只读变量 book。注意解释器的输出显示了book 的类型和值。这对理解复杂的声明会很方便。第二行打印出了book 的值 -- Programming Scala

提示

在交互模式(REPL)模式下来测试scala 命令是学习Scala 细节的一个非常好的方式。

2.2              脚本运行方式

然而,通常使用我们提到的第二个方式会更加方便:在文本编辑器中或者IDE 中编写Scala 脚本,然后用同样的scala 命令来执行它们。

用你选择的文本编辑器,保存下面例子中的Scala 代码到一个名为upper1-script.scala 的文件,放在你选择的目录中。

// code-examples/IntroducingScala/upper1-script.scala  

class Upper {  

  def upper(strings: String*): Seq[String] = {  

    strings.map((s:String) => s.toUpperCase())  

  } 

}

val up = new Upper

Console.println(up.upper("A", "First", "Scala", "Program")) 

这段Scala 脚本把一个字符串转换到大写。

Scala 使用和JavaC#C++等一样的注释方式。一个// 注释会影响整个一行,而/* 注释 */ 方式则可以跨行。

要运行这段脚本,打开命令行窗口,定位到对应目录,然后运行如下命令。

scala upper1-script.scala 

文件会被解释,这意味着它会被编译和执行。你会获得如下输出:

Array(A, FIRST, SCALA, PROGRAM) 

总的来说,如果你在命令行输入scala 而不输入文件名参数,解释器会运行在交互模式。你输入的定义和语句会被立即执行。如果你附带了一个scala 文件作为命令参数,它会把文件作为脚本编译和运行,就像我们的 scala upper1-script.scala 例子一样。最后,你可以单独编译scala 文件,运行class 文件,只要你有一个main 函数,就像你通常使用java 命令一样。

当我们提及执行一个脚本时,就是说用scala 命令运行一个Scala 源文件。

在这个例子里,类Upper (字面意思,没有双关) 里的upper 函数把输入字符串转换为大写,然后作为一个数组返回。最后一行把4个字符串转换完以后输出。

为了学习Scala 语法,让我们来更详细地解释一下代码。在这仅有的6行代码里面有许多细节!我们会解释一下基础的概念。

在这个例子里,Upper 类以class 关键字开始。类的主体被概括在最外面的大括号中 {...}

upper 方法的定义在二行,以def 关键字开始,紧接着是方法名,参数列表,和方法的返回类型,最后是等于号“=”,和方法的主体。

在括号里的参数列表实际上是一个String(字符串)类型的可变长度参数列表,由冒号后面后面的String* 类型决定。也就是说,你可以传入任意多的,以分号分隔的字符串(包括空的列表)。这些字符串会被存在一个名为strings 的参数中。在这个方法里面,strings 实际上是一个Array(数组)。

注意

当在代码里显式地为变量指定类型信息时,类型注解应该跟在变量名的冒号后面(也就是类Pascal 语法)。Scala 为什么不遵照Java 的惯例呢? 回想一下,类型信息在Scala 中经常是被推断出来的(不像Java),这意味着我们并不总是需要显式的声明类型。和Java 的类型习惯比较,item: type 模式在你忽略掉冒号和类型注解的时候,更容易被编译器清楚地分析。

方法的返回类型在参数列表的最后出现。在这个例子里,返回类型是Seq[String]Seqsequence)是一种特殊的集合。它是参数化的类型(像Java 中的泛型),在这里String 是参数。注意,Scala 使用方括号[...] 来指定参数类型,而Java 使用尖括号<...>

注意

Scala 允许在方法名中使用尖括号,比如命名“小于”方法为<,这很常见。所以,为了避免二义性,Scala 使用了方括号来声明参数类型。它们不能被用于方法名。这就是为什么Scala 不允许像Java 那样的使用尖括号的习惯。

upper 方法的主体跟在等于号“=”后面。为什么是一个等于号?为什么不像Java 一样直接使用大括号{...} 呢?因为分号,函数返回类型,方法参数列表,甚至大括号都经常会被省略,使用等于号可以避免几种可能的二义性。使用等于号也提醒了我们,即使是函数,在Scala 里面也是值。这和Scala 对函数是编程的支持是一致的。

函数的主体调用了strings 数组的map 方法,它接受一个字面函数(Function Literal)作为参数。字面函数也就是“匿名”函数。它们类似于其它语言中的Lambda 表达式,闭包,块,或者过程。在Java 里,你可能会在这里使用一个匿名内部类来实现一个接口(interface)定义的方法。

在这个例子里,我们传入这样的一个字面函数。

(s:String) => s.toUpperCase() 

它接受一个单独的名为s String 类型参数. 函数的主体在“箭头” => 的后面。它调用了s toUpperCase() 方法。调用的结果会被函数返回。在Scala 中,函数的最后一个表达式就是返回值,尽管你也可以在其它地方使用return 语句。return 关键字在这里是可选的,而且很少被用到,除非在一段代码中间返回(比如在一个if 语句块中)。

注意

最后一个表达式的值是默认的返回值。不需要显式的return

继续,map strings 里面的每一个String 传递给字面函数,从而用这些返回值创建了一个新的集合。

要运行这些代码,我们创建一个新的Upper 实例,然后把它赋值给一个名为up 的变量。变量up val 关键字定义为一个只读的值。

最后,我们对一个字符串列表调用upper 方法,然后用Console.println(...) 方法打印出来。这和Java System.out.println(...) 等效。

2.3              简化脚本代码

实际上,我们可以更加简化我们的代码。来看下面这一段简化版的脚本。

// code-examples/IntroducingScala/upper2-script.scala  

 object Upper {  

  def upper(strings: String*) = strings.map(_.toUpperCase())  

}

println(Upper.upper("A", "First", "Scala", "Program"))  

这段代码做了一模一样的事情,但是用了更少的字符。

在第一行,Upper 被定义为一个object,也就是单体模式。实际上我们是定义了一个class,但是Scala 运行时仅会创建Upper 的一个实例。(比如,你就不能写new Upper了。)Scala objects 被使用在其他语言需要“类级别”的成员的时候,比如Java statics (静态成员)。我们实际上并不需要更多的实例,所以单体模式也不错。

注意

Scala 为什么不支持statics?因为在Scala 中,所有的东西都是一个objectobject 结构使得这样的政策保持了一致。Java static 方法和字段并不绑定到一个实际的实例。

注意这样的代码是完全线程安全的。我们没有定义任何会引起线程安全问题的变量。我们使用的API 方法也是线程安全的。所以,我们不需要多个实例。单体模式工作的很好。

在第二行的upper 方法的实现也变简单了。Scala 通常可以推断出方法的返回值(但是方法参数的类型就不行了),所以我们不用显式声明。而且,因为在方法的主体中只有一个表达式,我们也省略了括号,把整个方法的定义放到一行中。方法主体前面的等于号告诉编译器函数的主体从这里开始,就像我们看到的一样。

我们也在字面函数里利用一些简写。之前我们像这样写一个函数:

(s:String) => s.toUpperCase() 

我们可以简化成如下表达式:

_.toUpperCase() 

因为map 接受一个参数,即一个函数,我们可以用 _ 占位符来替代有名参数。也就是说,_ 像是一个匿名变量,在调用 toUpperCase 之前每一个字符串都会被赋值给它。注意,String 类型是被推断出来的。将来我们会看到,Scala 还会在某些上下文中充当通配符。

你可以在一些更复杂的字面函数中使用这种简化的语法。

在最后一行,我们使用了一个object 而不是一个class 来简化代码。我们只要在Upper object 上直接调用upper 方法,而不用new Upper 来创建一个新的实例。(注意,这样的语法看起来很像在Java 类中调用一个静态方法。

最后,Scala 自动导入了许多用以输入输出的方法,比如println,所以我们不用写成Console.println()。我们只使用println 本身就可以了。

2.4              命令行工具运行方式

让我们来做最后一次重构;让我们把这段脚本变成一个编译好的命令行工具。

// code-examples/IntroducingScala/upper3.scala  

object Upper {  

  def main(args: Array[String]) = {  

    args.map(_.toUpperCase()).foreach(printf("%s ",_))  

    println("")  

  }  

}

现在upper 方法被重命名为main。因为Upper 是一个object,这个main 方法就像Java 类里的static main 方法一样。这个Upper 程序的入口。

注意

Scalamain 必须是一个object 的函数。(在Javamain 必须是一个类的静态方法。)命令行参数会作为一个字符串数组被传入应用程序,比如 args: Array[String]

main 方法的第一行使用了和我们刚才产看过的map 方法一样的简写。

args.map(_.toUpperCase())... 

调用map 会返回一个新的集合。我们用foreach 来遍历它。我们在传给foreach 的这个字面函数中再一次使用了一个 _ 占位符。这样,集合的每一个字符串会被作为printf 的参数传入。

...foreach(printf("%s ",_)) 

更清楚地说明一下,这两个“_”是完全相互独立的。这个例子里的连锁方法(Method Chaining)和简写字面函数需要花一些时间来习惯,但是一旦你熟悉了它们,他们用最少的临时变量来产生可读性很高的代码。

main 的最后一行在输出中加入了一个换行。

在这次,你必须先用scalac 来把代码编译成JVM 可认的.class 文件。 

scalac upper3.scala

你现在应该有一个名为Upper.class 的文件,就像你刚编译了一个Java 类一样。

注意

你可能已经注意到编译器并没有因为文件名为upper3.scala object 名为Upper 而抱怨。不像Java,这里文件名不用和公开域内的类型名字一致。实际上,和Java 不同,你可以在一个单独文件中有很多公开类型。此外,文件的地址也不用和包的声明一致。不过,如果你愿意,你可以依旧遵循Java 的规则。

现在,你可以传入任意多个字符串来执行这个命令了。比如:

scala -cp . Upper Hello World! 

-cp 选项会把当前目录加入到“类路径”的搜索中去。你会得到如下输出:

HELLO WORLD! 

3       初尝并发

    Scala 吸引有很多原因。 其中一个就是Scala 库的Actors API。它基于Erlang [Haller2007] 强大的Actors 并发模型建立。这里有一个例子来满足你的好奇心。

    Actor 并发模型中, 被称为执行者(Actor) 的独立软件实体不会互相之间共享状态信息. 相反, 它们通过交换消息来通信. 没有了共享易变状态的需要, 就更容易写出健壮的并发应用程序.

    在这个例子里, 不同的图形的实例被发送到执行者(Actor )来进行绘画和显示. 想象这样一个场景: 一个渲染集群在为动画生成场景. 在场景渲染完成之后, 场景中的元图形会被发送到一个执行者中由显示子系统处理.

我们从定义一系列的Shape (形状) 类开始。

// code-examples/IntroducingScala/shapes.scala  

package shapes {  

  class Point(val x: Double, val y: Double) {  

    override def toString() = "Point(" + x + "," + y + ")"  

  }  

  abstract class Shape() {  

    def draw(): Unit  

  }  

 class Circle(val center: Point, val radius: Double)   

  extends Shape {  

    def draw() = println("Circle.draw: " + this)  

    override def toString() =   

       "Circle(" + center + "," + radius + ")"  

  }  

  class Rectangle(val lowerLeft: Point, val height: Double, val width: Double)  

        extends Shape {  

    def draw() = println("Rectangle.draw: " + this)  

    override def toString() =  

      "Rectangle(" + lowerLeft + "," + height + "," + width + ")"  

  }  

  class Triangle(val point1: Point, val point2: Point, val point3: Point)  

        extends Shape {  

    def draw() = println("Triangle.draw: " + this)  

    override def toString() =  

      "Triangle(" + point1 + "," + point2 + "," + point3 + ")"  

  }  

Shape 的继承结构在shapes 包(package)中定义。你可以用Java 的语法定义包,但是Scala 也支持类似于C# 的名称空间的语法,就是把整个声明都包含在大括号的域中,就像这里所做的。Java 风格的包声明语法并不经常用到,然而,它们都一样精简和可读。

Point(点)表示了在一个平面上的二位点。注意类名字后面的参数列表。它们是构造函数的参数。在Scala 中,整个类的主体就是构造函数,所以你可以在类名字后面,类实体之前的主构造函数里列出所有参数。因为我们在每一个参数声明前放置了val 关键字,它们会被自动地转换为有同样名字的只读的字段,并且伴有同样名字的公开读取方法。也就是说,当你初始化一个Point 的实例时,比如point 你可以通过point.x point.y 来读取字段。如果你希望有可变的字段,那么使用var 关键字。

Point 类的主体定义了一个方法,类似于Java toString 方法的重写(或者C# ToString 方法)。主意,Scala C# 一样,在重写一个具体方法时需要显式的override 关键字。不过和C# 不一样的是,你不需要一个virtual (虚拟)关键字在原来的具体方法上。实际上,在Scala 中没有virtual 关键字。像之前一样,我们省略了toString 方法主体两边的大括号“{…}”,因为我们只有一个表达式。

Shape 是一个抽象类。Scala 中的抽象类和Java 以及C# 中的很像。我们不能实例化一个抽象类,即使它们的字段和方法都是具体的。

在这个例子里,Shape 声明了一个抽象的draw (绘制)方法。我们说它抽象是因为它没有方法主体。在方法上不用写abstract (抽象)关键字。Scala 中的抽象方法就像Java C# 中的一样。

draw 方法返回Unit,这种类型和Java 这样的C 后继语言中的void 大体一致。

Circle (圆)被声明为Shape 的一个具体的子类。 它定义了draw 方法来简单地打印一条消息到控制台。Circle 也重写了toString

Rectangle 也是Shape 得一个具体子类,定义了draw 方法,重写了toString。为了简单起见,我们假设它不会相对X Y 轴旋转。于是,我们所需要的就是一个点,左下角的点就可以,以及长方形的高度和宽度。

Triangle (三角形)遵循了同样的模式。它获取3个点作为它的构造函数参数。

CircleRectangle Triangle 的所有draw 方法里都用到了this。和JavaC# 一样,this 是一个实例引用自己的方式。在这里的上下文中,this 在一个String 的链接表达式(使用加号)的右边,this.toString 被隐式地调用了。

注意

既然我们已经定义了我们的形状类型,让我们回过头来看Actors。我们定义了一个Actor 来接受消息(需要绘制的Shape)。

// code-examples/IntroducingScala/shapes-actor.scala  

package shapes {  

  import scala.actors._  

  import scala.actors.Actor._  

  object ShapeDrawingActor extends Actor {  

    def act() {  

      loop {  

        receive {  

          case s: Shape => s.draw()  

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

          case x: Any   => println("Error: Unknown message! " + x)  

        }  

      }  

    }  

  }  

Actor 被声明为shapes 包的一部分。接着,我们有两个import (导入)表达式。

第一个import 表达式导入了所有在scala.actors 包里的类型。在Scala 中,下划线_ 的用法和Java 中的星号* 的用法一致。

注意

因为* 是方法名允许的合法字符,它不能在import 被用作通配符。所以,_ 被保留来作为替代。

Actor 的所有方法和公开域内的字段会被导入。Actor 类型中没有静态导入类型,虽然Java 中会。不过,它们会被导入为一个object,名字一样为Actor。类和object 可以使用同样的名字。

我们的Actor 类定义,ShapeDrawingActor,是继承自Actor (类型,不是实体)的一个实体。它的act 方法被重写来执行Actor 的实际工作。因为act 是一个抽象方法,我们不需要显式地用override 关键字来重写。我们的Actor 会无限循环来等待进来的消息。

在每一次循环中,receive 方法会被调用。它会阻塞当前线程直到一个新的消息到来。为什么在receive 后面的代码被包含在大括号{}中而不是小括号()呢?我们会在后面学到,有些情况下这样的替代是被允许的,而且十分有用。

现在,我们需要知道的是,在括号中的表达式组成了一个字面函数,并且传递给了receive。这个字面函数给消息做了一个模式匹配来决定它被如何处理。由于case 语句的存在,它看上去像Java 中的一个典型的switch 表达式,实际上它们的行为也很相像。

第一个case 给消息做了一个类型比较。(在代码中没有为消息实体做显式变量声明;它是被推断出来的。)如果消息是Shape 类型的,第一个case 会被满足。消息实体会被转换成Shape 并且赋值给变量s,然后s draw 方法会被调用。

如果消息不是一个Shape,第二个case 会被尝试。如果消息是字符串 exit Actor 会打印一条消息然后结束执行。Actors 通常需要一个优雅退出的方式。

最后一个case 处理所有其它任何类型的消息实例,作用和default (默认)case 一样。Actor 会报告一个错误然后丢弃这个消息。Any Scala 类型结构中所有类型的父类型,就像Java 和其他类型语言中的Object 根类型一样。所以,这个case 块会匹配任何类型的消息。模式匹配是头饥饿的怪兽,我们必须把这个case 块放在最后,这样它才不会把我们需要的消息也都吃掉!

回想一样我们在Shape 类里定义draw 为一个抽象方法,然后我们在具体的子类里实现它。所以,在第一个case 块中的代码执行了一个多态操作。

模式匹配 vs. 多态

模式匹配在函数式编程中扮演了中心角色, 就好像多态在面向对象编程中扮演着中心角色一样。函数式的模式匹配比绝大多数像Java 这样的命令式语言中的switch/case 语句更加重要和成熟。在我们的这个例子里,我们可以开始看到,函数式模式匹配和面向对象多态调度的有力结合会给Scala 这样的混合范式语言带来巨大好处。

最后,这里有一段脚本来使用ShapeDrawingActor

// code-examples/IntroducingScala/shapes-actor-script.scala  

import shapes._  

ShapeDrawingActor.start()  

ShapeDrawingActor ! new Circle(new Point(0.0,0.0), 1.0)  

ShapeDrawingActor ! new Rectangle(new Point(0.0,0.0), 2, 5)  

ShapeDrawingActor ! new Triangle(new Point(0.0,0.0),  

                                 new Point(1.0,0.0),  

                                 new Point(0.0,1.0))  

ShapeDrawingActor ! 3.14159  

ShapeDrawingActor ! "exit" 

shapes 包里的所有形状类会被导入。

ShapeDrawingActor 会被启动。默认情况下,它会运行在它自己的线程中等待消息。

5个消息通过使用语法 actor ! message 被送到Actor。第一个消息发送了一个Circle 实例。Actor 会“画”出这个圆。第二个消息发送了Rectangle 消息。Actor 会“画”出这个长方形。第三个消息对一个三角形做了同样的事情。第四个消息发送了一个约等于Pi Double (双精度浮点数)值。这对于Actor 来说是一个未知消息,所以它只是打印了一个错误消息。最后一个消息发送了exit 字符串,它会导致Actor 退出。

要实验这个Actor 例子,从编译这两个源文件开始。

使用下面的命令来编译文件。

scalac shapes.scala shapes-actor.scala 

虽然源文件的名字和位置并不和文件内容匹配,你会发现生成的class 文件被写入到一个shape 文件夹内,每一个类都会有一个class 文件对应。这些class 文件的名字和位置必须和JVM 的需求相吻合。

现在你可以运行这个脚本来看看Actor 的实际运行。

scala -cp . shapes-actor-script.scala 

你应该可以看到如下输出。

Circle.draw: Circle(Point(0.0,0.0),1.0)  

Rectangle.draw: Rectangle(Point(0.0,0.0),2.0,5.0)  

Triangle.draw: Triangle(Point(0.0,0.0),Point(1.0,0.0),Point(0.0,1.0))  

Error: Unknown message! 3.14159  

exiting... 

4       概括

 

通过Scala 的示例来开始对Scala 有所了解,其中一个还给出了Scala Actors 库的强大并发编程体验。

相关标签: scala 入门