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
该类有三个构造函数,由代码中的带编号注释给出
- 主构造函数,由类定义中的
name
和govtId
给出 - 具有参数
name
、govtId
和applicationDate
的辅助构造函数 - 另一个具有参数
name
、govtId
和studentId
的辅助构造函数
可以像这样调用这些构造函数
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
但是,特征还可以包含具体成员。例如,以下特征定义了两个抽象成员(numLegs
和 walk()
),还具体实现了 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
类实现了在 HasLegs
和 HasTail
中定义的抽象成员。现在,你可以创建新的 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 类,因此字段 name
和 relation
默认情况下是公有且不可变的。我们可以按照如下方式创建 case 类的实例
val christina = Person("Christina", "niece")
请注意,字段无法变异
christina.name = "Fred" // error: reassignment to val
由于假设 case 类的字段不可变,因此 Scala 编译器可以为您生成许多有用的方法
- 将生成一个
unapply
方法,它允许您对 case 类执行模式匹配(即,case Person(n, r) => ...
)。 - 将在类中生成一个
copy
方法,该方法对于创建实例的修改副本非常有用。 - 将使用结构相等生成
equals
和hashCode
方法,从而允许您在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
然后,在代码的其他部分,您可以编写如下方法,该方法使用模式匹配来处理传入的消息(假设方法 playSong
、changeVolume
和 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()
}
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()