Scala 3 — 书籍

工具

语言

Scala 提供了许多不同的构造,因此我们可以对周围的世界进行建模

  • 对象
  • 伴生对象
  • 特质
  • 抽象类
  • 枚举 仅限 Scala 3
  • 样例类
  • 样例对象

本节简要介绍了这些语言特性的每一个。

与其他语言一样,Scala 中的是用于创建对象实例的模板。以下是一些类的示例

class Person(var name: String, var vocation: String)
class Book(var title: String, var author: String, var year: Int)
class Movie(var name: String, var director: String, var year: Int)

这些示例表明 Scala 具有非常轻量级的类声明方式。

我们示例类中的所有参数都定义为 var 字段,这意味着它们是可变的:你可以读取它们,也可以修改它们。如果你希望它们是不可变的(只读),请将它们创建为 val 字段,或使用样例类。

在 Scala 3 之前,你使用 new 关键字来创建类的实例

val p = new Person("Robert Allen Zimmerman", "Harmonica Player")
//      ---

但是,使用 通用应用方法 在 Scala 3 中不需要这样做:仅限 Scala 3

val p = Person("Robert Allen Zimmerman", "Harmonica Player")

一旦你拥有一个类实例,例如 p,就可以访问它的字段,在本例中它们都是构造函数参数

p.name       // "Robert Allen Zimmerman"
p.vocation   // "Harmonica Player"

如前所述,所有这些参数都是作为 var 字段创建的,因此您还可以对其进行变异

p.name = "Bob Dylan"
p.vocation = "Musician"

字段和方法

类还可以具有不属于构造函数一部分的方法和附加字段。它们在类的正文中定义。正文作为默认构造函数的一部分进行初始化

class Person(var firstName: String, var lastName: String) {

  println("initialization begins")
  val fullName = firstName + " " + lastName

  // a class method
  def printFullName: Unit =
    // access the `fullName` field, which is created above
    println(fullName)

  printFullName
  println("initialization ends")
}
class Person(var firstName: String, var lastName: String):

  println("initialization begins")
  val fullName = firstName + " " + lastName

  // a class method
  def printFullName: Unit =
    // access the `fullName` field, which is created above
    println(fullName)

  printFullName
  println("initialization ends")

以下 REPL 会话展示了如何使用此类创建新的 Person 实例

scala> val john = new Person("John", "Doe")
initialization begins
John Doe
initialization ends
val john: Person = Person@55d8f6bb

scala> john.printFullName
John Doe
scala> val john = Person("John", "Doe")
initialization begins
John Doe
initialization ends
val john: Person = Person@55d8f6bb

scala> john.printFullName
John Doe

类还可以扩展特性和抽象类,我们将在下面的专门部分中介绍。

默认参数值

快速了解一些其他特性,类构造函数参数也可以具有默认值

class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) {
  override def toString = s"timeout: $timeout, linger: $linger"
}
class Socket(val timeout: Int = 5_000, val linger: Int = 5_000):
  override def toString = s"timeout: $timeout, linger: $linger"

此特性的一个优点是,它允许您的代码使用者以多种不同方式创建类,就好像该类具有备用构造函数一样

val s = new Socket()                  // timeout: 5000, linger: 5000
val s = new Socket(2_500)             // timeout: 2500, linger: 5000
val s = new Socket(10_000, 10_000)    // timeout: 10000, linger: 10000
val s = new Socket(timeout = 10_000)  // timeout: 10000, linger: 5000
val s = new Socket(linger = 10_000)   // timeout: 5000, linger: 10000
val s = Socket()                  // timeout: 5000, linger: 5000
val s = Socket(2_500)             // timeout: 2500, linger: 5000
val s = Socket(10_000, 10_000)    // timeout: 10000, linger: 10000
val s = Socket(timeout = 10_000)  // timeout: 10000, linger: 5000
val s = Socket(linger = 10_000)   // timeout: 5000, linger: 10000

在创建类的实例时,您还可以使用命名参数。当许多参数具有相同的类型时,这特别有用,如本比较所示

// option 1
val s = new Socket(10_000, 10_000)

// option 2
val s = new Socket(
  timeout = 10_000,
  linger = 10_000
)
// option 1
val s = Socket(10_000, 10_000)

// option 2
val s = Socket(
  timeout = 10_000,
  linger = 10_000
)

辅助构造函数

您可以定义一个类以具有多个构造函数,以便您的类的使用者可以以不同的方式构建它。例如,假设您需要编写一些代码来模拟大学录取系统中的学生。在分析需求时,您已经看到您需要能够以三种方式构建 Student 实例

  • 使用姓名和政府 ID,用于他们首次开始录取流程时
  • 使用姓名、政府 ID 和额外的申请日期,用于他们提交申请时
  • 使用姓名、政府 ID 和他们的学生 ID,用于他们被录取后

使用 OOP 样式处理此情况的一种方法是使用此代码

import java.time._

// [1] the primary constructor
class Student(
  var name: String,
  var govtId: String
) {
  private var _applicationDate: Option[LocalDate] = None
  private var _studentId: Int = 0

  // [2] a constructor for when the student has completed
  // their application
  def this(
    name: String,
    govtId: String,
    applicationDate: LocalDate
  ) = {
    this(name, govtId)
    _applicationDate = Some(applicationDate)
  }

  // [3] a constructor for when the student is approved
  // and now has a student id
  def this(
    name: String,
    govtId: String,
    studentId: Int
  ) = {
    this(name, govtId)
    _studentId = studentId
  }
}
import java.time.*

// [1] the primary constructor
class Student(
  var name: String,
  var govtId: String
):
  private var _applicationDate: Option[LocalDate] = None
  private var _studentId: Int = 0

  // [2] a constructor for when the student has completed
  // their application
  def this(
    name: String,
    govtId: String,
    applicationDate: LocalDate
  ) =
    this(name, govtId)
    _applicationDate = Some(applicationDate)

  // [3] a constructor for when the student is approved
  // and now has a student id
  def this(
    name: String,
    govtId: String,
    studentId: Int
  ) =
    this(name, govtId)
    _studentId = studentId

该类有三个构造函数,由代码中的带编号注释给出

  1. 主构造函数,由类定义中的 namegovtId 给出
  2. 具有参数 namegovtIdapplicationDate 的辅助构造函数
  3. 另一个具有参数 namegovtIdstudentId 的辅助构造函数

可以像这样调用这些构造函数

val s1 = new Student("Mary", "123")
val s2 = new Student("Mary", "123", LocalDate.now)
val s3 = new Student("Mary", "123", 456)
val s1 = Student("Mary", "123")
val s2 = Student("Mary", "123", LocalDate.now)
val s3 = Student("Mary", "123", 456)

虽然可以使用此技术,但请记住,构造函数参数也可以具有默认值,这使得一个类看起来有多个构造函数。这在前面的 Socket 示例中有所体现。

对象

对象是一个恰好有一个实例的类。当引用其成员时,它会延迟初始化,类似于 lazy val。Scala 中的对象允许在单个命名空间下对方法和字段进行分组,类似于您在 Java、Javascript(ES6)或 Python 中的 @staticmethod 中使用 static 成员的方式。

声明一个 object 与声明一个 class 类似。这里有一个“字符串实用工具”对象的示例,其中包含一组用于处理字符串的方法

object StringUtils {
  def truncate(s: String, length: Int): String = s.take(length)
  def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
  def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty
}
object StringUtils:
  def truncate(s: String, length: Int): String = s.take(length)
  def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
  def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty

我们可以按如下方式使用该对象

StringUtils.truncate("Chuck Bartowski", 5)  // "Chuck"

Scala 中的导入非常灵活,允许我们导入一个对象的所有成员

import StringUtils._
truncate("Chuck Bartowski", 5)       // "Chuck"
containsWhitespace("Sarah Walker")   // true
isNullOrEmpty("John Casey")          // false
import StringUtils.*
truncate("Chuck Bartowski", 5)       // "Chuck"
containsWhitespace("Sarah Walker")   // true
isNullOrEmpty("John Casey")          // false

或仅导入一些成员

import StringUtils.{truncate, containsWhitespace}
truncate("Charles Carmichael", 7)       // "Charles"
containsWhitespace("Captain Awesome")   // true
isNullOrEmpty("Morgan Grimes")          // Not found: isNullOrEmpty (error)

对象还可以包含字段,这些字段也可以像静态成员一样访问

object MathConstants {
  val PI = 3.14159
  val E = 2.71828
}

println(MathConstants.PI)   // 3.14159
object MathConstants:
  val PI = 3.14159
  val E = 2.71828

println(MathConstants.PI)   // 3.14159

伴生对象

与类同名且在与类相同的文件中声明的 object 称为“伴生对象”。类似地,相应的类称为对象的伴生类。伴生类或对象可以访问其伴生的私有成员。

伴生对象用于不特定于伴生类实例的方法和值。例如,在以下示例中,类 Circle 有一个名为 area 的成员,该成员特定于每个实例,其伴生对象有一个名为 calculateArea 的方法,该方法 (a) 不特定于某个实例,并且 (b) 可用于每个实例

import scala.math._

class Circle(val radius: Double) {
  def area: Double = Circle.calculateArea(radius)
}

object Circle {
  private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
}

val circle1 = new Circle(5.0)
circle1.area
import scala.math.*

class Circle(val radius: Double):
  def area: Double = Circle.calculateArea(radius)

object Circle:
  private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)

val circle1 = Circle(5.0)
circle1.area

在此示例中,可用于每个实例的 area 方法使用伴生对象中定义的 calculateArea 方法。再次强调,calculateArea 类似于 Java 中的静态方法。此外,由于 calculateArea 是私有的,因此其他代码无法访问它,但如所示,Circle 类的实例可以看到它。

其他用途

伴生对象可用于多种用途

  • 如所示,它们可用于在命名空间下对“静态”方法进行分组
    • 这些方法可以是公共的或私有的
    • 如果 calculateArea 是公共的,则它将被访问为 Circle.calculateArea
  • 它们可以包含 apply 方法,这些方法(感谢一些语法糖)作为工厂方法来构造新实例
  • 它们可以包含 unapply 方法,这些方法用于解构对象,例如使用模式匹配

以下快速了解如何使用 apply 方法作为工厂方法来创建新对象

class Person {
  var name = ""
  var age = 0
  override def toString = s"$name is $age years old"
}

object Person {
  // a one-arg factory method
  def apply(name: String): Person = {
    var p = new Person
    p.name = name
    p
  }

  // a two-arg factory method
  def apply(name: String, age: Int): Person = {
    var p = new Person
    p.name = name
    p.age = age
    p
  }
}

val joe = Person("Joe")
val fred = Person("Fred", 29)

//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old

此处未介绍 unapply 方法,但 语言规范 中对此进行了介绍。

class Person:
  var name = ""
  var age = 0
  override def toString = s"$name is $age years old"

object Person:

  // a one-arg factory method
  def apply(name: String): Person =
    var p = new Person
    p.name = name
    p

  // a two-arg factory method
  def apply(name: String, age: Int): Person =
    var p = new Person
    p.name = name
    p.age = age
    p

end Person

val joe = Person("Joe")
val fred = Person("Fred", 29)

//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old

此处未介绍 unapply 方法,但 参考文档 中对此进行了介绍。

特质

如果你熟悉 Java,Scala 特征类似于 Java 8+ 中的接口。特征可以包含

  • 抽象方法和字段
  • 具体方法和字段

在基本用法中,特征可以用作接口,仅定义将由其他类实现的抽象成员

trait Employee {
  def id: Int
  def firstName: String
  def lastName: String
}
trait Employee:
  def id: Int
  def firstName: String
  def lastName: String

但是,特征还可以包含具体成员。例如,以下特征定义了两个抽象成员(numLegswalk()),还具体实现了 stop() 方法

trait HasLegs {
  def numLegs: Int
  def walk(): Unit
  def stop() = println("Stopped walking")
}
trait HasLegs:
  def numLegs: Int
  def walk(): Unit
  def stop() = println("Stopped walking")

以下是另一个具有一个抽象成员和两个具体实现的特征

trait HasTail {
  def tailColor: String
  def wagTail() = println("Tail is wagging")
  def stopTail() = println("Tail is stopped")
}
trait HasTail:
  def tailColor: String
  def wagTail() = println("Tail is wagging")
  def stopTail() = println("Tail is stopped")

请注意,每个特征仅处理非常具体的属性和行为:HasLegs 仅处理腿,HasTail 仅处理与尾巴相关的功能。特征允许你构建这样的小型模块。

在代码的后面部分,类可以混合多个特征来构建更大的组件

class IrishSetter(name: String) extends HasLegs with HasTail {
  val numLegs = 4
  val tailColor = "Red"
  def walk() = println("I’m walking")
  override def toString = s"$name is a Dog"
}
class IrishSetter(name: String) extends HasLegs, HasTail:
  val numLegs = 4
  val tailColor = "Red"
  def walk() = println("I’m walking")
  override def toString = s"$name is a Dog"

请注意,IrishSetter 类实现了在 HasLegsHasTail 中定义的抽象成员。现在,你可以创建新的 IrishSetter 实例

val d = new IrishSetter("Big Red")   // "Big Red is a Dog"
val d = IrishSetter("Big Red")   // "Big Red is a Dog"

这只是使用特征可以实现的功能的一小部分。有关更多详细信息,请参阅这些建模课程的其余部分。

抽象类

当你想要编写一个类,但你知道它将具有抽象成员时,你可以创建一个特征或一个抽象类。在大多数情况下,你将使用特征,但历史上在两种情况下使用抽象类比使用特征更好

  • 你想要创建一个采用构造函数参数的基类
  • 代码将从 Java 代码调用

采用构造函数参数的基类

在 Scala 3 之前,当基类需要采用构造函数参数时,你会将其声明为 abstract class

abstract class Pet(name: String) {
  def greeting: String
  def age: Int
  override def toString = s"My name is $name, I say $greeting, and I’m $age"
}

class Dog(name: String, var age: Int) extends Pet(name) {
  val greeting = "Woof"
}

val d = new Dog("Fido", 1)
abstract class Pet(name: String):
  def greeting: String
  def age: Int
  override def toString = s"My name is $name, I say $greeting, and I’m $age"

class Dog(name: String, var age: Int) extends Pet(name):
  val greeting = "Woof"

val d = Dog("Fido", 1)

特征参数 仅限 Scala 3

但是,在 Scala 3 中,特征现在可以具有 参数,因此你现在可以在相同的情况下使用特征

trait Pet(name: String):
  def greeting: String
  def age: Int
  override def toString = s"My name is $name, I say $greeting, and I’m $age"

class Dog(name: String, var age: Int) extends Pet(name):
  val greeting = "Woof"

val d = Dog("Fido", 1)

特征在组合方面更灵活——你可以混合多个特征,但只能扩展一个类——并且在大多数情况下应该优先于类和抽象类。经验法则是,当你想要创建特定类型的实例时使用类,当你想要分解和重用行为时使用特征。

枚举 仅限 Scala 3

枚举可用于定义由一组有限的命名值组成的类型(在FP 建模部分,我们将看到枚举比这灵活得多)。基本枚举用于定义常量集,如一年中的月份、一周中的天数、北/南/东/西等方向,等等。

例如,这些枚举定义了与披萨相关的属性集

enum CrustSize:
  case Small, Medium, Large

enum CrustType:
  case Thin, Thick, Regular

enum Topping:
  case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions

要在其他代码中使用它们,首先导入它们,然后使用它们

import CrustSize.*
val currentCrustSize = Small

可以使用 equals (==) 比较枚举值,也可以匹配

// if/then
if currentCrustSize == Large then
  println("You get a prize!")

// match
currentCrustSize match
  case Small => println("small")
  case Medium => println("medium")
  case Large => println("large")

其他枚举特性

枚举也可以参数化

enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)

它们还可以有成员(如字段和方法)

enum Planet(mass: Double, radius: Double):
  private final val G = 6.67300E-11
  def surfaceGravity = G * mass / (radius * radius)
  def surfaceWeight(otherMass: Double) =
    otherMass * surfaceGravity

  case Mercury extends Planet(3.303e+23, 2.4397e6)
  case Earth   extends Planet(5.976e+24, 6.37814e6)
  // more planets here ...

与 Java 枚举的兼容性

如果你想将 Scala 定义的枚举用作 Java 枚举,你可以通过扩展类 java.lang.Enum(默认导入)来实现,如下所示

enum Color extends Enum[Color] { case Red, Green, Blue }

类型参数来自 Java enum 定义,并且应该与枚举的类型相同。在扩展 java.lang.Enum 时,无需向其提供构造函数参数(如 Java API 文档中定义的)——编译器会自动生成它们。

在像这样定义 Color 之后,你可以像使用 Java 枚举一样使用它

scala> Color.Red.compareTo(Color.Green)
val res0: Int = -1

代数数据类型参考文档部分更详细地介绍了枚举。

样例类

用例类用于对不可变数据结构进行建模。看以下示例

case class Person(name: String, relation: String)

由于我们将 Person 声明为 case 类,因此字段 namerelation 默认情况下是公有且不可变的。我们可以按照如下方式创建 case 类的实例

val christina = Person("Christina", "niece")

请注意,字段无法变异

christina.name = "Fred"   // error: reassignment to val

由于假设 case 类的字段不可变,因此 Scala 编译器可以为您生成许多有用的方法

  • 将生成一个 unapply 方法,它允许您对 case 类执行模式匹配(即,case Person(n, r) => ...)。
  • 将在类中生成一个 copy 方法,该方法对于创建实例的修改副本非常有用。
  • 将使用结构相等生成 equalshashCode 方法,从而允许您在 Map 中使用 case 类的实例。
  • 将生成一个默认 toString 方法,这有助于调试。

以下示例演示了这些附加功能

// Case classes can be used as patterns
christina match {
  case Person(n, r) => println("name is " + n)
}

// `equals` and `hashCode` methods generated for you
val hannah = Person("Hannah", "niece")
christina == hannah       // false

// `toString` method
println(christina)        // Person(Christina,niece)

// built-in `copy` method
case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
// result:
// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)

// Case classes can be used as patterns
christina match
  case Person(n, r) => println("name is " + n)

// `equals` and `hashCode` methods generated for you
val hannah = Person("Hannah", "niece")
christina == hannah       // false

// `toString` method
println(christina)        // Person(Christina,niece)

// built-in `copy` method
case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
// result:
// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)

支持函数式编程

如上所述,case 类支持函数式编程 (FP)

  • 在 FP 中,您尝试避免变异数据结构。因此,构造函数字段默认为 val 是有道理的。由于 case 类的实例无法更改,因此可以轻松共享它们,而无需担心变异或竞争条件。
  • 您可以使用 copy 方法作为模板来创建新的(可能已更改)实例,而不是变异实例。此过程可以称为“在复制时更新”。
  • 自动为您生成 unapply 方法还允许在模式匹配中以高级方式使用 case 类。

样例对象

Case 对象对于对象来说就像 case 类对于类一样:它们提供许多自动生成的方法来使它们更强大。每当您需要一个需要一些额外功能的单例对象时,它们特别有用,例如在 match 表达式中与模式匹配一起使用。

当您需要传递不可变消息时,case 对象很有用。例如,如果您正在从事音乐播放器项目,您将创建一组命令或消息,如下所示

sealed trait Message
case class PlaySong(name: String) extends Message
case class IncreaseVolume(amount: Int) extends Message
case class DecreaseVolume(amount: Int) extends Message
case object StopPlaying extends Message

然后,在代码的其他部分,您可以编写如下方法,该方法使用模式匹配来处理传入的消息(假设方法 playSongchangeVolumestopPlayingSong 在其他地方定义)

def handleMessages(message: Message): Unit = message match {
  case PlaySong(name)         => playSong(name)
  case IncreaseVolume(amount) => changeVolume(amount)
  case DecreaseVolume(amount) => changeVolume(-amount)
  case StopPlaying            => stopPlayingSong()
}
def handleMessages(message: Message): Unit = message match
  case PlaySong(name)         => playSong(name)
  case IncreaseVolume(amount) => changeVolume(amount)
  case DecreaseVolume(amount) => changeVolume(-amount)
  case StopPlaying            => stopPlayingSong()

本页的贡献者