Scala入门系列终章:类与对象

2021-03-04 14:38:27 浏览数 (1)

导读

截至本篇,Scala入门系列即将告一段落,先后用7篇文章将Scala语言的四梁八柱进行了简要介绍,虽然阅读量相较于其他文章更为惨淡,但相信对于有一定编程语言基础又想快速入门Scala的读者来说还是会有一定收获和借鉴的。

本文作为该系列的最后一篇,将重点介绍Scala中类和对象的概念,毕竟Scala也是一门面向对象的编程语言。

坦白讲,个人在学习Scala中这一部分内容时其实也是有些凌乱的,一直觉得未能理解到Scala中类和对象的精髓,所以当下完成此文也仅仅是出于系列内容的完整性,后续也将适时推出其他分享。

首先给出Scala快速入门系列的文章列表,自认为掌握这些内容即可开始一些编程实战和进阶了。

  • 终于,为了大数据分析我还是开启了Scala学习之路
  • Scala从零起步:变量和标识符
  • Scala从零起步:运算符
  • 就是个控制结构,Scala能有什么新花样呢?
  • Scala中的方法与函数
  • 曾经以为Python中的List用法足够灵活,直至我遇到了Scala…

延续以上6篇推文,本文介绍类和对象,主要包括:

  • 关于面向对象的理解
  • 类、对象和特质
  • 伴生类(对象)、抽象类、样例类

01 关于面向对象的理解

提到面向对象,首先就不得不讲述编程语言中范式的概念。提到编程语言范式,其实一般是指编程语言的一种风格或规范。例如当我们讲到关系型数据库一般有6大范式,其中一般会满足前3个范式;那么当我们讲编程语言的范式时呢,其实主流的可分为以下3种范式:

  • 面向过程
  • 面向对象
  • 函数式

而Scala的一大特性就是支持多范式的编程语言,其实一般就是指的面向对象和函数式。当然,实质上面向过程肯定也都是支持的,只不过一般不特意强调而已。那么到底何为面向对象?个人拙见:

面向对象,编程语言中实现一个特殊的语法结构(类),将一类事物(严格来讲,这里用对象object更为贴切,但此处有意避用这一词语)的所有属性和方法封装在一起,就实现了类的定义。而后,通过该类(class)的定义就可创建(new)出具有这些属性和方法的若干实例,这些实例就叫做对象(object)(相当于类是加工厂,对象是加工厂生产的产品,所以类是抽象的,对象是具体的)。值得补充说明的是,前面提到将属性和方法封装成一个类的定义,那么如何区分属性和方法呢?以类的经典案例:Student类来说,属性就是姓名、年龄和身高体重的那些取值,方法则是学习、工作、跑步那些动作。比如在Python中类的属性和方法的明显区别是:属性不带小括号,表示一个变量取值;方法都是带小括号的,表示一个函数,对应一套处理逻辑。而在Scala中,由于方法不接受任何参数时可缺省小括号,所以这个区别反倒不那么明显,也就不能通过是否带小括号来区分属性和方法。

理解完类和对象的概念,那么面向对象编程则是指将一类事物封装成类,而后执行操作和查找取值时则使用该类创建的对象来完成,典型画风是这样的:某对象执行什么什么操作,某对象取什么什么属性值,这里都是在用对象来调用方法或属性,体现的思想就是面向对象。

类似地,面向过程编程呢,则是这样的画风:把某些变量输入给一些函数来完成相应操作。这里实现的主体显然是函数,函数不仅要调度这一过程、还要针对具体需求实现相应的处理逻辑,体现的思想就是面向过程;

那么函数式编程呢?则将是这样的画风:不仅把一些变量输入给函数/方法,还把一些执行特定操作的函数也一并作为输入,并由这个函数和变量来完成预定需求,就好像数学中定义的函数那样,指定输入和函数,得到相应的输出。这里的调度交给外面的函数/方法,而具体需求的处理逻辑则交由里面的函数,体现的思想就是函数式。

02 类、对象和特质

不同于其他语言中仅提供了class一个关键字来定义类,Scala中为了更好的支持面向对象的设计,提供了类(class)、对象(object)和特质(trait)三个相关概念,其中class是主体,类似于其他面向对象编程语言中类的概念,用于定义标准的类;object是单例的(Singleton),即全局有且仅有这一个实例,且这唯一的实例就是定义object本身;trait设计的初衷是为了丰富类的继承,用于将一类特殊的方法和属性抽象成一类特质,便于其他类的继承。三者中,class是主角,而object和trait其实都可看做是为class服务和辅助的。

1)类——class

class的标准定义与其他编程语言也是大同小异,这里仍然以定义一个Student类为例,并支持name和age属性以及学习方法:

代码语言:javascript复制
scala> class Student{
     |   var name:String = _
     |   var age:Int = _
     |   def study = println(s"${name} is studying")
     | }
class Student

scala> val stu = new Student
val stu: Student = Student@5cd8d029

scala> stu.study
null is studying

在以上代码块中,通过class关键字定义了一个Student类,该类包含name和age两个属性以及study一个方法,其中name和age属性是var类型,并通过下划线_来实现默认值的初始化(Scala中,String的默认值为"",Int的默认值为0)。而后,通过new关键字创建了一个Student类的实例对象stu,由于未进行任何的属性赋值,所以在调用study方法时,打印的name字段为空。

以上是一个基础的类定义,注意在类名后紧跟着大括号囊括的代码块,对应类的定义语句。有时还希望能在创建对象的同时完成一些属性的赋值,例如Student类中name和age字段,也就是对象初始化的过程,这一需求可实现如下:

代码语言:javascript复制
scala> class Student(var name:String, age:Int){
     |   def study = println(s"${name} is studying")
     | }
class Student

scala> val stu2 = new Student("zhangsan", 20)
val stu2: Student = Student@326d39fd

scala> stu2.study
zhangsan is studying

scala> stu2.name = "lisi"
// mutated stu3.name
scala> stu2.age = 21
            ^
       error: value age is not a member of Student
       did you mean name?

这个类的定义与前述定义中的主要区别仅在于将两个属性字段的声明从{}中转移到了()中,但同时还有本质上的区别:在()中声明的属性实际上称之为主构造器,或者理解为初始化的过程(类似于Python中定义类都要定义的那个__init__()函数)。正因为将其放在了主构造器中(其实就是那个小括号),所以在后续创建实例对象的同时,便可直接初始化这些属性值。另外值得注意的是,在上述主构造器的属性构造中,name属性前显示的加了var关键字,而age属性前则是缺省了Scala中声明变量的标志性关键字val/var,这种情况下默认该属性为val,即不可更改。例如在后续分别尝试为name和age重新赋值时,name赋值成功,而age则提示不具有该属性值。

除了在类名后增加小括号用于实现主构造器,Scala中的类定义还支持辅助构造器。这里辅助构造器可理解为是对主构造器的重载或多态的过程,例如:

代码语言:javascript复制
scala> class Student(var name:String, var age:Int){
     |   def this(name:String) = { this(name, -1) }
     |   def this(age:Int) = { this("unNamed", age) }
     |   def study = println(s"${name} is studying")
     | }
class Student

scala> val stu3 = new Student("lisi")
val stu3: Student = Student@427b24c0

scala> val stu4 = new Student(20)
val stu4: Student = Student@152a41ab

scala> stu4.study
unNamed is studying

上述定义中,除了类名Student后的()用于主构造器,在{}内部,还增加了两个名为this的方法,分别接收不同的变量数量和类型,而后在方法体中又调用this方法并提供缺省的属性值。这里,重新def的两个this方法其实就称作辅助构造器,而各自内部调用的this方法则实际上就是主构造器,通过重载this方法实现了不同的构造形式。当然,定义辅助构造器的要求是:必须都以this命名方法,且在方法体内部也都必须首先调用名为this的主构造器。

以上就是Scala中类的定义和创建对象的常用方法,此外还包括的两个知识点是:主构造器中的变量支持初始化默认值,同时类的方法和属性也支持不同的权限等级,在不做任何显示声明的情况下即为public级别(Scala中并无public关键字,缺省即表示该权限等级),还可指定protected和private两种权限,这里不再拓展。

另外,上述由类创建对象过程中,都用到了关键字new来实例化一个对象,Scala还支持定义伴生对象apply的方法实现省略new来创建对象的过程,这将在后文给出。

2)单例对象——object

object关键字的用法与class很是相似,但定义的对象有且仅有一个实例,就是定义的这个object本身。也正因这一特性,所以object定义的对象不支持new的过程,也不支持接收变量初始化构造(因为就这一个实例,不支持个性化的构造多个实例),调用相应方法和属性时则是直接用object名调用即可。

代码语言:javascript复制
scala> object MathFunctions{
     |   val PI:Double = 3.1415926
     |   def calCircleArea(r:Double) = PI * r * r
     | }
object MathFunctions

scala> MathFunctions.PI
val res7: Double = 3.1415926

scala> MathFunctions.calCircleArea(2.0)
val res9: Double = 12.5663704

上述是演示了object实现一些特殊需求的例子,即作为工具类。由于Scala中所有执行程序都必须以main作为主入口,所以object的另一个用法是用于承载项目主入口的main函数,这也是绝大多数Scala项目的设计方式(这里用绝大多数是因为,还可以通过obejce继承APP类来实现不提供main函数入口)。

此外,object的一个重要用途应当就是作为class的伴生对象,提供一些特殊方法,例如apply方法。这将在后文介绍。

3)特质——trait

trait的概念其实是比较抽象的,毕竟在其他编程语言中并不常见这一设计。在Scala中,特质的用法初衷也是为了实现class的多继承,通过将某些相近的属性和方法封装在一个trait中,后续即可在定义class时继承该trait。例如,一个可能的情形是:

  • 定义一个Study的trait
  • 定义一个Work的trait
  • 定义一个Eat相关的trait
  • 定义一个Sleep相关的trait
  • 定义一个Person类,继承了Study、Work、Eat和Sleep4个trait
  • 定义一个Student类,继承了Study、Eat和Sleep3个trait
  • 定义一个Worker类,继承了Work、Eat和Sleep3个trait
  • ……

trait的具体用法这里不再给出。

03 伴生类(对象)、抽象类、样例类

最后介绍类的几个拓展相关概念。

1)伴生类(对象)——Companions

在Scala中,当定义了一个同名的class和object时,这个类和对象互为伴生,即这个class为object的伴生类,这个object为class的伴生对象。伴生的要求就是同名,伴生的好处就是可以互相访问对方的属性和方法,甚至也因此带来一些实现上的便利。例如,当对一个class实现伴生对象的apply方法时,在后续new这个class的实例对象时,可缺省new关键字而更为方便(就省略了一个new,好像也没有多方便?好吧,既然大家都说方便了那就方便了吧)。

代码语言:javascript复制
scala> :paste
// Entering paste mode (ctrl-D to finish)
class Student(name:String, age:Int){
  def printInfo =  println(s"New a student with name: ${name}, age: ${age}.")
}
object Student{
  def apply(name:String, age:Int) = new Student(name, age)
}
// Exiting paste mode, now interpreting.
class Student
object Student

scala> val stu = Student("zhangsan", 20)
val stu: Student = Student@7a7fbfbb

scala> stu.printInfo
New a student with name: zhangsan, age: 20.

注:上述由于是在shell中编辑,所以要进入到块编辑模式下表,否则定义的class和object由于分属于不同的context(上下文环境)而未能实现伴生。

通过定义Student的同名class和object,并且在object中设计了apply方法,所以在创建Student对象时,可缺省new关键字。

2)抽象类——abstract class

Scala中支持定义抽象类,这里的抽象在于定义一些属性和方法时只给出声明而不提供具体的赋值和逻辑,所以对于使用者而言是抽象的(或者说不具体的),定义方法只需在普通class前增加一个abstract关键字,实现的目的是为了便于其他类的继承,即仅列出属性和方法清单却不给出具体实现,某种程度上类似于trait的定位。

但凡以abstract前缀修饰的class定义,在声明属性和方法时即可以不给出具体赋值和实现:

代码语言:javascript复制
scala> abstract class Student{
     |  var name:String
     |  val age:Int
     |  def study:Unit
     | }

3)样例类——case class

class的另一个前缀修饰词是case,用于实现样例类。样例类的核心优势在于会自动实现一些配套方法,例如前述讲到的伴生对象apply方法,这使得通过样例类创建对象时也可缺省new关键字。

代码语言:javascript复制
scala> case class Student(name:String, age:Int)
class Student

scala> Student("zhangsan", 20)
val res12: Student = Student(zhangsan,20)

样例类中,自动实现的方法除了伴生对象的apply外,还包括unapply,setter, getter 和toString,equals,copy和hashCode等。

0 人点赞