印度朋友手把手教你学Scala(5):类


/**
 * 谨献给我最爱的YoYo 
 *
 * 原文出处:https://madusudanan.com/blog/scala-tutorials-part-5-classes/
 * @author dogstar.huang <chanzonghuang@gmail.com> 2017-03-05
 */

本翻译已征得Madusudanan.B.N同意,并链接在原文教程前面。

这是关于Scala系列教程的第五章。点击这里可查看整个系列。

目录

  • 简介
  • 访问和可见性
  • 构造器
  • 类参数和类字段
  • 类参数提升
  • 直接访问成员
  • 不可变对象和可变对象
  • 何时使用getter和setter
  • Scala风格的getter和setter
  • 辅助构造器
  • 默认构造参数
  • 抽象类
  • override关键字
  • 何时使用抽象类

简介

Scala中类的概念非常类似于他们在Java中的同行。但是它们提供的功能有很多不同。

首先,让我们举个简单的例子。

// 典型Java风格的类
class Person {  
   var name = "Noname"
   var age = -1
   def setName (name:String)  {
      this.name = name
   }

   def setAge (age:Int) {
     this.age = age
   }

   def getName () : String = {
     name
   }

   def getAge () : Int = {
     age
   }


}

我们有两个变量,nameage,和可以像下面这样访问的getter、setter。

object RunExample extends App{

  val personObj = new Person

  println(personObj.getName)
  println(personObj.getAge)

}

访问和可见性

上一个示例具有默认访问修饰符。让我们详细看看其他修饰符。

回顾一下,java有四个访问修饰符。

Java访问级别

修饰符子类项目
publicYYYY
protectedYYYN
默认/没有修改符YYNN
privateYNNN

可以为scala绘制类似的表示。

修饰符伴生对象子类项目
默认/没有修改符YYYYY
protectedYYYNN
privateYYNNN

它或多或少类似于Java的访问修饰符,除了只有三个级别。默认访问和Java的不一样,并且Scala中的默认值等于Java的public。

这样应该差不多了,但如果你感兴趣想更深入了解,Scala访问修饰符是一个很好的读物。

也可以参考官方文档

你会发现,与Java相比,scala访问级别稍微好一点。

构造器

要理解类的下一个东西明显是构造函数。与Java不同,有几种不同的方法来创建构造函数。

class Person (name:String,age:Int) {


  def getName () : String = {
    name
  }

  def getAge () : Int = {
    age
  }


}

这个类的实现和前面那个类似,除了没有setter而只有getter。

他们可以这样使用,如下。

object RunExample extends App{

  val personObj = new Person("John",-1)

  println(personObj.getName)
  println(personObj.getAge)

}

在类定义期间给出方法样式参数以指示构造函数。

一旦构造函数被声明,我们不能创建一个类,而不提供在构造函数中指定的默认参数。

我们不能直接调用变量,因为根据上面定义的访问级别,他们默认是private val,所以它会抛出一个错误。

类参数和类字段

重要的是要理解,对于一个类,有两个组件,即类参数和类字段。

类参数是你在声明中提到的参数,即主要构造函数。

类字段是常规的成员变量。

上面的例子有getter,没有这些则不可能被另外一个类访问该特定类的变量。

如果我们仔细看看,实际的原因比只是private更为微妙,参数只有构造函数的作用域,并且不能超出。这和访问在循环中声明的变量非常类似。这些是类参数,并且只能在构造函数的作用域内有效。

另一方面,类字段可以在类外部访问(基于它们的访问级别)。

这种区分很重要,它构成了以下讨论的基础。

类参数提升

提升类参数的过程只是改变它们在构造函数之外的使用范围。

这可以通过两种方式来完成。通过在构造函数参数中的变量之前附加valvar

class Person (val name:String,val age:Int) {}  

或者

class Person (var name:String,var age:Int) {}  

即使这个类没有其他东西,仍然可以创建和消费它的实例。

直接访问成员

将参数更改为val/var使我们可以直接访问类变量,而无需getter/setter。

上面的任何一个val/var的例子都可以如下使用。

object RunExample extends App{

  val example = new Person("Test",20)

  println(example.name)
  println(example.age)


}

我们可以直接访问变量,因为它是默认访问级别。事实上,val/var与访问没有任何关系。

设计这样的东西是一个坏主意,但它是可以做得到的。

这产生了两种设计类的方式,即可变/不可变。

不可变对象和可变对象

通过val,可以产生不可变对象。

不可变对象

我们可以在类构造器中声明不可变变量。

class Person (val name:String,val age:Int) {

   def getName () : String = {
     name
   }

   def getAge () : Int = {
     age
   }

}

一旦变量被声明,它就不能再次重新赋值,这是在第一章中探讨的val的默认行为。

Scala有一些叫做样本类的东西,它们专门用于处理诸如不可变类以及其他一些整齐特征的情况。我们将在未来的教程中探索样本类。

可变对象

class Person (var name:String,var age:Int) {

   def getName () : String = {
     name
   }

   def getAge () : Int = {
     age
   }

}

我们可以声明一个实例,并且直接改变它的名字。

object RunExample extends App{

  val p = new Person("Rob",43)

  p.name = "Jacob"

  // 打印改变后的名字
  println(p.name)

}

何时使用getter和setter

当设计一个类时,有两个重要的决定要做。

对于第一点,它基本上是权衡的。上面链接的文章总结了不可变对象的优点。

第二点是需要思考的东西。

在Java中传统的智慧依然适用于何时使用getter和setter。

一个简单的例子是做不仅仅是把值设置给变量,我们还可以做一些其他的事情,如检查访问,日志等。

但是Scala风格指南讲了一个不同的故事。

建议是,类的用户不需要知道是否正在访问的是一个方法,还是类成员。他们可以简单地选择使用变量名称本身来更改/访问它。这大大简化了代码,并且看起来更整洁。

下一节将介绍如何创建一个带有模糊语法的getter/setter。

Scala风格的getter和setter

Scala有不同的方式来创建getters/setter,虽然仍然支持Java风格,正如我们在第一个例子中看到的。

让我们来看下面的类。

class Person() {

  private var _age: Int = -1

  def age: Int = _age

  def age_=(value: Int) = {
    _age = value
  }


}

可以像下面这样使用。

object RunExample extends App{

  val p = new Person

  p.age = 20

  println(p.age)

}

参数age实际上是Person类中的一个方法,但是用户可以像访问一个变量一样访问它。

语法可能看起来很奇怪,但是了解它们适合的位置将会给出更清晰的图片。让我们来逐步分解。

age实际是幕后的_age。上面的getter十分清晰,只是返回了_age变量。而setter则令人有一点困惑。首先方法是def age_=,这里的下划线是一个特殊的字符,它可以允许使用空格调用方法。这使得我们可以像age =这样调用方法。

注意,方法名,下划线和等号应该连在一起,之间不能有任何空格。其他的则已经是有意义了,即它接收一个参数value并将其赋给_age变量。

这个定义应该足够让你一时半会转不过弯来,但下划线字符有更多其他的含义,这里有一个称为统一访问原则的东西。我们将在以后的专门教程中探讨。

在IDE Intellij里的Scala插件可以帮你做全部的代码生成

通常getter和setter被视为第二等类公民,因为Scala鼓励不可变对象。

辅助构造器

在现实世界场景中,我们通常需要有两个或三个有不同参数的构造器。

为了做到这一点,我们有一个特别的构造器创建风格,叫做辅助构造器。

class Person (name:String,age:Int) {

  // 什么也不提供
  def this(){
    this("",-1)
  }

  // 提供了name,但没提供age
  def this(name:String){
    this(name,-1)
  }

  // 提供了age,但没提供name
  def this(age:Int){
    this("",age)
  }

  def getName () : String = {
    name
  }

  def getAge () : Int = {
    age
  }

}

对代码的注释几乎总结了这个想法。一个重要的事情要注意的是,辅助构造函数不是一个实际的构造函数,而是作为现有构造函数的包装器。

当我们想给上面的Person类添加一个参数时,例如说一个电话号码,那么我们将它添加到主构造函数,即类参数。

class Person (name:String,age:Int,phone:String) {

  // 什么也不提供
  def this(){
    this("",-1,"")
  }

  // 提供了name,但没提供age
  def this(name:String){
    this(name,-1,"")
  }

  // 提供了age,但没提供name
  def this(age:Int){
    this("",age,"")
  }

  def getName () : String = {
    name
  }

  def getAge () : Int = {
    age
  }

}

请注意,所有辅助构造函数现在都必须将phone作为参数。这可能看起来是一个开销,但实际上它是一个很好的设计。

默认构造参数

辅助构造函数适用于实现多态行为,即不同的构造函数可以在类执行/功能中展示不同的流程。

最常见的坏做法是使用这些实现默认变量值。

像方法一样,我们可以在构造函数声明过程中提供默认值。

class Person(name:String = "Unnamed",age:Int = -1,phone:String = "No number") {


  def getName () : String = {
    name
  }

  def getAge () : Int = {
    age
  }

  def getPhone () : String ={
    phone
  }

}

然后我们可以不用声明其中某个参数,甚至是全部,就可以使用这个类。

object RunExample extends App{

  val example = new Person()

  println(example.getName())
  println(example.getAge())
  println(example.getPhone())


}

列出的示例能正确地工作,并将打印默认值:Unnamed,-1,No number。

这是一种非常方便的途径来声明预定义类参数。

抽象类

在Scala中的抽象类与Java的类似,但它们被一个名为traits的概念所取代。我们将在后续的教程中探索traits。

下面是一个抽象类的简单例子。

abstract class Parent(name:String = "Noname") {

  // 带定义/返回类型的方法 
  def getAge

  // 没定义但返回String类型的方法 
  def getID : String

  // 显式地表明没有实现存在
  def getEmail () : String {}

  // 实现功能的方法
  // 不需要通过子类实现,如果需要可以覆盖
  def getName : String = {
    name
  }


}

注释很好地说明了这些方法是做什么的做什么。abstract关键字不是必需的,当方法体未定义时,它会被认为是一个抽象方法。

如果我们将它扩展到另一个类,我们需要实现getAgegetIDgetEmail这三个方法。

我们可以选择重载/不重载getName方法,因为它已经实现了。

override关键字

在Java中,当需要从父类重载一个方法时,我们需要做任何特殊的事情。为了简明起见,我们可以使用@Override注释。

Scala有一个override关键字,在从父类重载一个具体方法的情况下是强制的。

这提升了更好的类型安全和意外的重载。

我们可以在方法前面添加一个override关键字,以按预期方式工作。

何时使用抽象类

正如我前面所说,抽象类可以被traits所取代,那么在什么情况下我们需要使用抽象类呢?

这需要更多关于特质的知识,但我们将简要谈谈两种情况。

  • 当我们想要一个带有构造函数参数的基类时

    在进一步的教程中我们将看到为什么特质不允许这样。

  • 当我们想使用Java代码中的类时

    由于特质是特定于Scala的,所以抽象类在兼容性方面更好。

有了这个,我想结束这个教程。接下来这个系列,我将带你了解样本类、特质以及为什么它们真的很酷。

敬请关注!^_^

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州