Scala教程–对象,类,继承,特征,具有多个相关类型的列表,适用

前言

这是面向初学者的Scala教程的第9部分。 该博客上还有其他文章,您可以在我正在为其创建的计算语言学课程的链接页面上找到这些链接和其他资源。 另外,您可以在“ JCG Java 教程”页面上找到本教程和其他教程系列。

本教程是有关使用Scala进行面向对象编程的。 到目前为止,我们看到的大多数内容都是使用函数和基本类型(例如Int,Double和String)以及预定义类型(例如List和Map)进行编程的。 事实证明,这些都是Scala数据结构的所有类或类型,可让它们创建对象或该类型的实例。 本教程不会广泛介绍面向对象的编程,但是会提供一些有关类和对象以及如何使用它们的实用示例。 对于表示面向对象的概念有些草率,我事先表示歉意。 目的是主要通过直观的示例向初学者传达想法,而不会陷入很多技术细节中。 有关更多详细信息,请参见Wikipedia页面上的面向对象编程

请注意,在本教程中,对象和类的定义最容易在REPL中以纯文本的形式查看。 因此,我将一段代码放入文本中,并且应该将其添加到自己的REPL中(通过简单地剪切和粘贴),以便能够继续进行下去。

对象

从本质上讲,对象可以看作是封装一些数据和功能的结构。 让我们从一个代表一个人及其一些可能属性的对象的示例开始。

object JohnSmith {
  val firstName = "John"
  val lastName = "Smith"
  val age = 37
  val occupation = "linguist"

  def fullName: String = firstName + " " + lastName

  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }

}

如果将其放入Scala REPL中,则可以访问字段( firstNamelastNameage职业 )和函数( fullNamegreet )。

scala> JohnSmith.firstName
res0: java.lang.String = John

scala> JohnSmith.fullName
res1: String = John Smith

scala> JohnSmith.greet(true)
res2: String = Hello, my name is John Smith. I'm a linguist.

scala> JohnSmith.greet(false)
res3: String = Hi, I'm John!

因此,在最基本的层次上,一个对象就是:值和函数的集合(通常也称为方法)。 您可以通过提供对象名称,后跟一个句点以及您要使用的值或函数来访问任何这些值或函数。 这对于组织此类集合很有用,但正如我们将看到的那样,它还带来了更多的可能性。

我们当然可能会对以这种方式封装有关另一个人的信息感兴趣。 我们可以通过模仿约翰·史密斯的定义来做到这一点。

object JaneDoe {
  val firstName = "Jane"
  val lastName = "Doe"
  val age = 34
  val occupation = "computer scientist"

  def fullName: String = firstName + " " + lastName

  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }

}

在将以上代码添加到REPL之后,现在Jane Doe可以向我们打招呼了。

scala> JaneDoe.greet(true)
res4: String = Hello, my name is Jane Doe. I'm a computer scientist.

scala> JaneDoe.greet(false)
res5: String = Hi, I'm Jane!

当然,我通过执行复制粘贴操作创建了JaneDoe对象,然后用Jane Doe的信息替换了这些字段。 这导致了很多浪费的工作:字段相同,但是值不同,并且功能完全相同。 如果要更改问候语的方式,则必须在所有对象之间进行更新。

更重要的是,这两个对象彼此完全不同:一个对象不能将它们放在列表中,而不能在该列表上映射函数。 考虑以下失败的尝试。

scala> val people = List(JohnSmith, JaneDoe)
people: List[ScalaObject] = List(JohnSmith$@698fcb66, JaneDoe$@5f72cbae)

scala> people.map(person => person.firstName)
<console>:11: error: value firstName is not a member of ScalaObject
people.map(person => person.firstName)
                                          ^

唯一令有关JohnSmith对JaneDoe斯卡拉的已知,是他们ScalaObjects。 这意味着此类对象的列表基本上可以只包含它们,并允许您将它们作为一个组移动。 因此,需要更多的东西来使这些集合更有用和更通用。

班级

在上面的列表中,我们想要的是List [Person] ,其中Person是具有已知字段和功能的类型。 我们可以通过定义一个Person类,然后将John和Jane定义为该类的成员来实现。 这也减少了前面提到的剪切和粘贴重复问题。 这是它的样子。

class Person (
  val firstName: String,
  val lastName: String,
  val age: Int,
  val occupation: String
) {

  def fullName: String = firstName + " " + lastName

  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }

}

class关键字指示这是一个类定义, Person是该类的名称。 定义的下一部分是该类的一组参数,这些参数使我们能够构造作为该类实例的对象-换句话说,它们是占位符,它们使我们可以将Person类用作创建Person对象的工厂。 为此,我们使用了new关键字,给出了类的名称并为每个参数提供了值。 例如,这是我们现在可以创建John Smith的方法。

scala> val johnSmith = new Person("John", "Smith", 37, "linguist")
johnSmith: Person = Person@1979d4fb

就像以前使用一次性的独立JohnSmith对象一样,我们现在可以访问字段和函数。

scala> johnSmith.age
res8: Int = 37

scala> johnSmith.greet(true)
res9: String = Hello, my name is John Smith. I'm a linguist.

定义其他人现在很容易,不需要任何繁琐的操作。

scala> val janeDoe = new Person("Jane", "Doe", 34, "computer scientist")
janeDoe: Person = Person@7ff5376c

scala> val johnDoe = new Person("John", "Doe", 43, "philosopher")
johnDoe: Person = Person@6544c984

scala> val johnBrown = new Person("John", "Brown", 28, "mathematician")
johnBrown: Person = Person@4076a247

现在可以将这些Person对象放到一个列表中,为我们提供一个List [Person] ,该列表允许映射检索特定的值(例如名字和年龄),并执行计算(例如计算列表中个人的平均年龄)。

scala> val people = List(johnSmith, janeDoe, johnDoe, johnBrown)
people: List[Person] = List(Person@1979d4fb, Person@7ff5376c, Person@6544c984, Person@4076a247)

scala> people.map(person => person.firstName)
res10: List[String] = List(John, Jane, John, John)

scala> people.map(person => person.age)
res11: List[Int] = List(37, 34, 43, 28)

scala> people.map(person => person.age).sum/people.length.toDouble
res12: Double = 35.5

我们可以根据年龄对它们进行分类。

scala> val ageSortedPeople = people.sortBy(_.age)
ageSortedPeople: List[Person] = List(Person@4076a247, Person@7ff5376c, Person@1979d4fb, Person@6544c984)

scala> ageSortedPeople.map(person => person.fullName + ":" + person.age)
res13: List[java.lang.String] = List(John Brown:28, Jane Doe:34, John Smith:37, John Doe:43)

我们还可以按名字,姓氏等对人员进行分组。

scala> people.groupBy(person => person.firstName)
res14: scala.collection.immutable.Map[String,List[Person]] = Map(Jane -> List(Person@7ff5376c), John -> List(Person@1979d4fb, Person@6544c984, Person@4076a247))

scala> people.groupBy(person => person.lastName)
res15: scala.collection.immutable.Map[String,List[Person]] = Map(Brown -> List(Person@4076a247), Smith -> List(Person@1979d4fb), Doe -> List(Person@7ff5376c, Person@6544c984))

这样,我们就可以让所有约翰人向我们打招呼了。

scala> people.groupBy(person => person.firstName)("John").foreach(john => println(john.greet(true)))
Hello, my name is John Smith. I'm a linguist.
Hello, my name is John Doe. I'm a philosopher.
Hello, my name is John Brown. I'm a mathematician.

独立对象

上面,我们看到了如何通过使用new关键字并将结果对象分配给变量来创建Person类的实例。 我们可以完整地回到我们创建的第一个JohnSmith对象,它是一个独立的ScalaObject 。 我们可以通过扩展 Person类来创建这样的独立对象。

scala> object ThomYorke extends Person("Thom", "Yorke", 43, "musician")
defined module ThomYorke

scala> ThomYorke.greet(true)
res25: String = Hello, my name is Thom Yorke. I'm a musician.

通过扩展Person类来创建对象,我们是说对象是一种Person -有关继承的更多信息,请参见下文。 因此, ThomYorke是一个Person对象,就像我们创建的其他对象一样,但是它是针对不同的用例的,我们将在下一个教程中对其进行介绍。 现在,我将粗略地概括一下,可以通过可能正在使用我的代码的其他代码使ThomYorke对象更易于访问,而johnSmithjaneDoe对象将更局部地包含在其中。

遗产

独立对象自然将我们引向继承的思想。 在许多领域中,都有自然类型的等级,因此,超级类型的属性被其子类型继承(例如,鱼类有and和游泳,而鲑鱼有g和游泳)。 例如,我们可以有一种语言类型,它是Person的一种,而ComputerScientist类型是一种Person ,等等。 为了对此建模,我们创建了一个扩展了另一个类的类,并可能提供了一些其他参数,例如以下Person类的语言学家子类型的定义。

class Linguist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  val favoriteLanguage: String
) extends Person(firstName, lastName, age, "linguist") {

  def workGreeting =
    "As a " + occupation + ", I am a " + speciality + " who likes to study the language " + favoriteLanguage + "."

}

Linguist类具有自己的参数列表:其中一些参数(例如firstNamelastNameage )传递给Person ,并且有新的参数字段specialityfavoriteLanguage 。 定义的扩展部分传递了构造所有信息以构成Person所需的相关参数,对于语言学家 ,它直接将职业参数设置为“语言学家”,因此,当我们构建了语言学家 ,例如Noam Chomsky。

scala> val noamChomsky = new Linguist("Noam", "Chomsky", 83, "syntactician", "English")noamChomsky: Linguist = Linguist@54c0627f

通过这种方式定义了一个语言学家对象,我们可以要求它给予工作问候。

scala> noamChomsky.workGreeting
res26: java.lang.String = As a linguist, I am a syntactician who likes to study the language English.

我们还可以访问Person对象的字段和功能,例如agegreet

scala> noamChomsky.age
res27: Int = 83

scala> noamChomsky.greet(true)
res28: String = Hello, my name is Noam Chomsky. I'm a linguist.

当然,也可以访问特定于语言学家的字段,例如favoriteLanguage

scala> noamChomsky.favoriteLanguage
res29: String = English

细心的读者会注意到,有些参数以val开头,而有些则没有。 稍后我们将回到这种区别。

特质

当然,我们现在可以继续定义一个ComputerScientist类,该类也具有workGreeting功能,但是Linguist.workGreetingComputerScientist.workGreeting将完全分开。 为了实现这一点,我们可以使用特性,它们类似于类,但是定义了函数和字段的接口,类可以为其提供具体的值和实现。 (注意:特征也可以定义具体的字段和函数,因此它们不限于占位符函数,如下所示)。

例如,这是一个Worker特性,它简单地定义一个函数workGreeting并声明它必须返回String

trait Worker {
  def workGreeting: String
}

先前定义的Linguist类已经提供了该功能的实现。 为了使语言学家被视为一种工作者 ,我们在扩展Person之后添加Worker

class Linguist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  val favoriteLanguage: String
) extends Person(firstName, lastName, age, "linguist") with Worker {

  def workGreeting =
    "As a " + occupation + ", I am a " + speciality + " who likes to study the language " + favoriteLanguage + "."

}

这就是所谓的特质工作者 “中混”,因为在工人与这些的领域和功能语言学家类混音。

注意,我们还可以创建仅扩展特性的类,例如Worker

class Student (school: String, subject: String) extends Worker {
  def workGreeting = "I'm studying " + subject + " at " + school + "!"
}

现在,我们可以创建一个Student对象并请求他们的问候。

scala> val anonymousStudent = new Student("The University of Texas at Austin", "history")
anonymousStudent: Student = Student@734445b5

scala> anonymousStudent.workGreeting
res32: java.lang.String = I'm studying history at The University of Texas at Austin!

注意,在Student的定义中,参数school和subject之前没有val 。 这意味着它们不是Student类的成员字段,这意味着它们不能从外部访问。 例如,尝试访问为匿名 学生提供的学校值失败。

scala> anonymousStudent.school
<console>:11: error: value school is not a member of Student
anonymousStudent.school

当然,在内部, Student可以使用提供给此类参数的值,例如,在定义workGreeting的结果时。 这种封装对类外部的代码隐藏了类对象的属性。 此策略可以帮助减少代码用户可用的自由度,以便他们仅使用您想要的代码。 通常,如果其他人不需要使用它,则不应让他们使用它。

回到既属于又属于工人的类 ,当我们定义计算机科学家时 ,我们进行了类似的扩展…声明,就像对语言学家所做的那样。

class ComputerScientist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  favoriteProgrammingLanguage: String
) extends Person(firstName, lastName, age, "computer scientist") with Worker {

  def workGreeting =
    "As a " + occupation + ", I work on " + speciality + ". Much of my code is written in " + favoriteProgrammingLanguage + "."

}

让我们将Andrew McCallum创建为ComputerScientist对象。

scala> val andrewMcCallum = new ComputerScientist("Andrew", "McCallum", 44, "machine learning", "Scala")
andrewMcCallum: ComputerScientist = ComputerScientist@493cd5ba

scala> andrewMcCallum.workGreeting
res31: java.lang.String = As a computer scientist, I work on machine learning. Much of my code is written in Scala.

由于我们将语言学家重新定义为Worker ,因此我们需要使用新定义重新创建Noam Chomsky。 (创建的外观与以前相同,但是它使用REPL中已更新的新类定义。)

scala> val noamChomsky = new Linguist("Noam", "Chomsky", 83, "syntactician", "English")
noamChomsky: Linguist = Linguist@6fccaf14

需要注意的小事: ComputerScientist专业领域与Linguist的领域是分离的,因此没有特别期望两者之间使用的一致性:对于Linguist,这是对在分区工作的人员的描述,而对于ComputerScientist是对子区域的描述。

那么,如果将noamChomsky和andrewMcCallum放在一个List中会发生什么呢?

scala> val professors = List(noamChomsky, andrewMcCallum)
professors: List[Person with Worker] = List(Linguist@6fccaf14, ComputerScientist@493cd5ba)

Scala创建了一个类型为List [Person with Worker]的列表 ; 这是对列表的所有元素均有效的最特定类型。 这意味着我们可以将所有元素都视为Persons ,例如访问其职业 (这是Person的成员字段)。

scala> professors.map(prof => prof.occupation)
res34: List[String] = List(linguist, computer scientist)

我们可以将列表中的每个元素都视为PersonWorker ,例如,打印出他们的fullName (来自Person )和他们的workGreeting (来自Worker )。

scala> professors.foreach(prof => println(prof.fullName + ": " + prof.workGreeting))
Noam Chomsky: As a linguist, I am a syntactician who likes to study the language English.
Andrew McCallum: As a computer scientist, I work on machine learning. Much of my code is written in Scala.

但是,我们无法访问语言学家计算机 科学家特有的字段和功能,例如Linguist的 favorite 语言

scala> professors.map(prof => prof.favoriteLanguage)
<console>:15: error: value favoriteLanguage is not a member of Person with Worker
professors.map(prof => prof.favoriteLanguage)

很容易看出Scala为什么会出现这种行为:即使那对noamChomsky有效,但对于andrewMcCallum (根据我们定义LinguistComputerScientist的方式 )却不是。

匹配多态列表中的类型

考虑当匿名学生在教授列表中时会发生什么。

scala> val workers = List(noamChomsky, andrewMcCallum, anonymousStudent)
workers: List[ScalaObject with Worker] = List(Linguist@6fccaf14, ComputerScientist@493cd5ba, Student@734445b5)

Person类型不见了,现在我们有了带有Worker的更通用类型ScalaObject的列表。 现在我们只能使用WorkerworkGreeting方法。

但是,值得指出的是,当您具有异构对象的集合时, match语句会派上用场。 例如,将以下代码放入REPL。

val people = List(johnSmith, noamChomsky, andrewMcCallum, anonymousStudent)

people.foreach { person =>
  person match {
    case x: Person with Worker => println(x.fullName + ": " + x.workGreeting)
    case x: Person => println(x.fullName + ": " + x.greet(true))
    case x: Worker => println("Anonymous:" + x.workGreeting)
  }
}

结果如下(请记住,JohnSmith对从未定义为语言学家 -他被定义为个人为职业的“语言学家”)。

John Smith: Hello, my name is John Smith. I'm a linguist.
Noam Chomsky: As a linguist, I am a syntactician who likes to study the language English.
Andrew McCallum: As a computer scientist, I work on machine learning. Much of my code is written in Scala.
Anonymous:I'm studying history at The University of Texas at Austin!

因此,我们可以使用Scala的模式匹配通过匹配到更具体的类型来切换行为。

申请功能

Scala提供了一个简单但令人难以置信的好功能:如果您在类或对象中定义了apply函数,则实际上不需要编写“ apply”即可使用它。 例如,以下对象将一个添加到为其apply方法提供的参数中。

object AddOne {
  def apply (x: Int): Int = x+1
}

因此,我们可以像您通常期望的那样使用它。

scala> AddOne.apply(3)
res41: Int = 4

但是,我们也可以不使用“ .apply”部分而获得相同的结果。

scala> AddOne(3)
res42: Int = 4

如果一个类具有apply方法,那么我们可以对该类的任何对象执行相同的技巧。

class AddN (amountToAdd: Int) {
  def apply (x: Int): Int = x + amountToAdd
}

scala> val add2 = new AddN(2)
add2: AddN = AddN@43ca04a1

scala> add2(5)
res43: Int = 7

scala> val add42 = new AddN(42)
add42: AddN = AddN@83e591f

scala> add42(8)
res44: Int = 50

事实证明,您经常不知不觉地使用apply方法! 当拥有List并按索引访问元素时,就使用了List类的apply方法。

scala> val numbers = 10 to 20 toList
numbers: List[Int] = List(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

scala> numbers(3)
res46: Int = 13

scala> numbers.apply(3)
res47: Int = 13

使用Map中的键访问值的操作相同,到目前为止,您在Scala中使用的许多其他类也是如此。

包起来

本教程介绍了Scala中面向对象编程的基础。 希望这足以让人们对什么是对象和类以及如何使用它们做事有一个体面的认识。 关于它们,还有很多要学习的知识,但这应该足以让您入门,以便有意义地进行进一步的研究。 了解这些概念非常重要,因为Scala从一开始就是面向对象的。 实际上,在以前的许多教程中,我有时都会经历一些额外的麻烦来尝试描述正在发生的事情,而不必谈论面向对象。 但是现在您可以看到Int,Double,List,Map之类的东西:包含特定字段和函数的类,您可以使用它们完成工作。 现在,您可以开始编写自己的类,以在应用程序中启用自己的自定义行为。

参考: Scala入门程序员的第一步,来自BCG博客的JCG合作伙伴 Jason Baldridge的 第9部分

相关文章 :


翻译自: https://www.javacodegeeks.com/2011/10/scala-tutorial-objects-classes.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值