Scala 编程类和对象

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Scala 编程类和对象相关的知识,希望对你有一定的参考价值。

类,字段和方法

  类是对象的蓝图。一旦定义了类,就可以用关键字new从类的蓝图里创建对象,类的定义:

class ChecksumAccumulator {
    // class definition goes here
}

  就能创建对象:

scala> new ChecksumAccumulator
res0: ChecksumAccumulator = [email protected]

  类定义里,可以放置字段和方法,这些被笼统地称为成员:member。字段,不管是用 val 或是用 var 定义都是指向对象的变量。方法,用 def 定义,包含了可执行的代码。字段保留了对象的状态或者数据,而方法使用这些数据对对象做运算工作。实例化类的时候,执行期环境会设定一些内存来保留对象状态的镜像——也就是说,变量的内容。举例来说,如果定义了 ChecksumAccumulator 类并给它一个叫做 sum 的 var 字段:

class ChecksumAccumulator { 
  var sum = 0
}

 

  并实例化两次:

cala> val acc = new ChecksumAccumulator
acc: ChecksumAccumulator = [email protected]

scala> val csa = new ChecksumAccumulator
csa: ChecksumAccumulator = [email protected]

  对象在内存里的镜像看上去大概是这样的:

技术分享

  由于在类 ChecksumAccumulator 里面定义的字段 sum 是 var ,而不是 val ,可以重新赋值给它不同的 Int 值:

scala> acc.sum = 3
acc.sum: Int = 3

  现在,图像看上去会变成:

技术分享

 

  字段的另一种说法是实例变量:instance variable,因为每一个实例都有自己的变量集。对象实例的变量组成了对象的内存镜像。本例中尽管 acc 是val,但仍可以改变 acc 指向的对象。对 acc 不能做的事情是由于它是 val ,而不是 var ,不可以把它们再次赋值为不同的对象。例如,下面的尝试将会失败:

scala> acc = new ChecksumAccumulator
<console>:12: error: reassignment to val
       acc = new ChecksumAccumulator
           ^

  acc 将永远指向初始化时指向的同一个 ChecksumAccumulator 对象,但是包含于对象中的字段可以随时改动。

  通过把字段变为私有的:private去阻止外界直接对它的访问可以让对象具有鲁棒性:

class ChecksumAccumulator { 
  private var sum = 0 
}

  现在 sum 是私有的,所以唯一能访问 sum 的代码是定义在类自己里面:

class ChecksumAccumulator { 
  private var sum = 0
  def add(b: Byte): Unit = { 
    sum += b 
  } 
  def checksum(): Int = {
    return ~(sum & 0xFF) + 1
  }
}

  传递给方法的任何参数都可以在方法内部使用。 Scala 里方法参数的一个重要特征是它们都是 val ,不是 var 。如果想在方法里面给参数重新赋值,结果是编译失败:

scala> def add(b: Byte): Unit = {
     |   b += 1 // 编译不过,因为b是val
     |   sum += b
     | }
<console>:11: error: value += is not a member of Byte
         b += 1 // 编译不过,因为b是val
           ^

  更简洁的风格是去掉 return 语句,如果没有发现任何显式的返回语句, Scala 方法将返回方法中最后一个计算得到的值。假如某个方法仅计算单个结果表达式,则可以去掉大括号。如果结果表达式很短,甚至可以把它放在 def 同一行里:

class ChecksumAccumulator {
  private var sum = 0
  def add(b: Byte): Unit = sum += b
  def checksum(): Int = ~(sum & 0xFF) + 1
}

  通常定义副作用为在方法外部某处改变状态或者执行 I/O 活动。比方说,在 add 这个例子里,副作用就是 sum 被重新赋值了。表达这个方法的另一种方式是去掉结果类型和等号,把方法体放在大括号里。这种形式下,方法看上去很像过程: procedure ,一种仅为了副作用而执行的方法:

class ChecksumAccumulator {
  private var sum = 0
  def add(b: Byte) { sum += b }
  def checksum(): Int = ~(sum & 0xFF) + 1
}

  当去掉方法体前面的等号时,它的结果类型将注定是 Unit 。不论方法体里面包含什么都不例外,因为 Scala 编译器可以把任何类型转换为 Unit 。就是说,带有大括号但没有等号的,在本质上当作是显式定义结果类型为 Unit 的方法:

scala> def g() {"this String gets lost too"}
g: ()Unit

  因此,如果需要返回一个非 Unit 的值,则必须插入等号:

scala> def h() = { "this String gets returned!" }
h: ()String

scala> h
res0: String = this String gets returned!

 

分号推断

  Scala 程序里,语句末尾的分号通常是可选的。若一行里仅有一个语句可以不写,但如果一行里写多个语句那么分号是需要的:

scala> val s = "hello"; println(s)
hello
s: String = hello

  如果想输入一个跨越多行的语句,多数时候只需输入, Scala 将在正确的位置分隔语句:

if (x < 2)
  println("too small")
else
  println("ok")

  偶尔 Scala 也把句子分割成两部分:

x
+ y

  这会被分成两个语句 x 和 + y 。如果希望把它作为一个语句 x + y ,需要把它包裹在括号里:

(x
+ y)

  或者也可以把 + 放在行末:

x +
y +
z

  在串接类似于+的中缀操作符,把操作符放在行尾而不是行头是普遍的Scala风格。

  分割语句的精确规则是:除非以下情况的一种成立,否则行尾被认为是一个分号:

    1.疑问行由一个不能合法作为语句结尾的字结束,如句点或中缀操作符。

    2.下一行开始于不能作为语句开始的字。

    3.行结束于括号(...)或方框[...]内部,因为这些符号不可能容纳多个语句。

 

Singleton 对象

  Scala 比 Java 更面向对象的一个方面是 Scala 没有静态成员。替代品是 Scala 有单例对象:singleton object。除了用 object 关键字替换了 class 关键字以外,单例对象的定义看上去就像是类定义:

// 文件ChecksumAccumulator.scala
import scala.collection.mutable.Map
object ChecksumAccumulator {
  private val cache = Map[String, Int]()
  def calculate(s: String): Int = 
    if (cache.contains(s)) 
      cache(s)
    else {
      val acc = new ChecksumAccumulator
      for (c <- s)
        acc.add(c.toByte)
      val cs = acc.checksum()
      cache += (s -> cs)
      cs
    }
}

 

  表中的单例对象被叫做 ChecksumAccumulator ,与前一个例子里的类同名。当单例对象与某个类共享同一个名称时,他被称作是这个类的伴生对象:companion object。必须在同一个源文件里定义类和它的伴生对象。类被称为是这个单例对象的伴生类:companion class。类和它的伴生对象可以互相访问其私有成员。

  ChecksumAccumulator 单例对象有一个方法calculate 用来计算所带的 String 参数中字符的校验和。考虑单例对象的一种方式是把它当作是 Java 的静态方法,用类似的语法调用方法:单例对象名,点,方法名:

ChecksumAccumulator.calculate("Every value is an object.")

 

  单例对象不只是静态方法的收容站。它同样是个第一类的对象,可以把单例对象的名字看作是贴在对象上的“名签”:

技术分享

  定义单例对象不是定义类型。如果只是 ChecksumAccumulator 对象的定义就建不了ChecksumAccumulator类型的变量。应该说 ChecksumAccumulator 类型是由单例对象的伴生类定义的。然而,单例对象扩展了超类并可以混入特质。由于每个单例对象都是超类的实例并混入了特质,可以通过这些类型调用它的方法,用这些类型的变量指代它,并把它传递给需要这些类型的方法。

  类和单例对象间的一个差别是,单例对象不带参数,而类可以。因为不能用 new 关键字实例化一个单例对象,没机会传递给它参数。每个单例对象都被作为由一个静态变量指向的虚构类:synthetic class的一个实例来实现,因此它们与 Java 静态类有着相同的初始化语法,单例对象会在第一次被访问的时候初始化。

  不与伴生类共享名称的单例对象被称为孤立对象:standalone object。很多种原因会用到它,包括把相关的功能方法收集在一起,或定义一个Scala应用的入口点。

 

Scala 程序

  要执行 Scala 程序一定要提供一个有main方法(仅带一个参数,Array[String],且结果类型为Unit)的孤立单例对象名。任何拥有合适签名的 main 方法的单例对象都可以用来作为程序的入口点:

// 文件Summer.scala
import ChecksumAccumulator.calculate
object Summer { 
  def main(args: Array[String]) { 
for (arg <- args)
  println(arg + ": " + calculate(arg))
  }
}

  代码中单例对象的名字是 Summer 。它的 main 方法具有合适的签名,可以把它用作程序。文件中的第一个语句是引用定义在前例中 ChecksumAccumulator 对象中的 calculate 方法。这个引用语句允许在文件之后的部分里使用方法的简化名。 main 方法体简单地打印输出每个参数和参数的校验和,用冒号分隔。

  无论 ChecksumAccumulator.scala 还是 Summer.scala 都不是脚本,因为他们是以定义结束的。反过来说,脚本必然以一个结果表达式结束。因此如果尝试以脚本方式执行 Summer.scala , Scala 解释器将会报错说 Summer.scala 不是以结果表达式结束的。正确的做法是,用 Scala 编译器真正地编译这些文件,然后执行输出的类文件。其中一种方式是使用 scalac , Scala 的基本编译器。输入:

?  work  scalac ChecksumAccumulator.scala Summer.scala
?  work  ls
ChecksumAccumulator$$anonfun$calculate$1.class
ChecksumAccumulator$.class
ChecksumAccumulator.class
ChecksumAccumulator.scala
Summer$$anonfun$main$1.class
Summer$.class
Summer.class
Summer.scala

  这将编译源文件,不过在编译完成之前或许会有一个可感知的停顿。原因是每次编译器启动时,都要花一些时间扫描jar文件内容,并在即使提交的是新的源文件也在查看之前完成其他初始化工作。因此, Scala 的发布包里还包括了一个叫做 fsc (快速 Scala 编译器)的 Scala 编译器后台服务:daemon。可以这样使用:

fsc ChecksumAccumulator.scala Summer.scala

  第一次执行 fsc 时,会创建一个绑定在计算机端口上的本地服务器后台进程。然后它就会把文件列表通过端口发送给后台进程去编译,后台进程完成编译。下一次执行 fsc 时,后台进程就已经在运行了,于是 fsc 将只是把文件列表发给后台进程,它会立刻开始编译文件。使用 fsc ,只需要在第一次等待Java运行时环境的启动。如果想停止 fsc 后台进程,可以执行 fsc –shutdown 来关闭。

  不论执行 scalac 还是 fsc 命令,都将创建 Java 类文件,然后可以用 scala 命令用包含了正确签名的 main 方法的孤立对象名运行:

?  work  scala Summer of love
of: -213
love: -182

 

Application 特质

  Scala 提供了一个特质, scala.Application ,可以节省一些手指的输入工作:

import ChecksumAccumulator.calculate
object FallWinterSpringSummer extends Application {
  for (season <- List("fall", "winter", "spring"))
    println(season +": "+ calculate(season))
}

  使用这个特质的方法是,首先在单例对象名后面写上“extends Application” 。然后代之以 main 方法,可以把想要放在 main 方法里的代码直接放在单例对象的大括号之间,之后可以像对其它程序那样编译和运行。

  这种方式之所以能奏效是因为特质 Application 声明了带有合适的签名的 main 方法,并由单例对象继承,使它可以像个 Scala 程序那样用。大括号之间的代码被收集进了单例对象的主构造器:primary constructor,并在类被初始化时被执行。

  继承自 Application 比写显式的 main 方法要短,不过它也有些缺点:

    1. 想访问命令行参数的话不能用它,因为args数组不可访问。

    2. 因为某些JVM线程模型里的局限,如果程序是多线程的就需要显式的 main 方法。

    3. 某些JVM的实现没有优化被 Application 特质执行的对象的初始化代码。

  因此只有当程序相对简单和单线程情况下才可以继承Application特质。

以上是关于Scala 编程类和对象的主要内容,如果未能解决你的问题,请参考以下文章

Scala 分号

object

Scala 编程类和对象

scala编程——类和对象

Scala基础:类和对象访问修饰符和构造器

简单定义Python和Scala的类和对象