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
列舉值可以使用等於 (==
) 進行比較,也可以進行匹配
// 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
為案例類別,欄位 name
和 relation
預設為公開且不可變。我們可以如下建立案例類別的實例
val christina = Person("Christina", "niece")
請注意欄位無法變異
christina.name = "Fred" // error: reassignment to val
由於案例類別的欄位假設為不可變,Scala 編譯器可以為您產生許多有用的方法
- 產生一個
unapply
方法,讓您可以在案例類別上執行模式比對(也就是case Person(n, r) => ...
)。 - 在類別中產生一個
copy
方法,這對於建立實例的修改副本非常有用。 - 產生使用結構相等性的
equals
和hashCode
方法,讓您可以在Map
中使用案例類別的實例。 - 產生一個預設的
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)
支援函式程式設計
如前所述,案例類別支援函式程式設計 (FP)
- 在 FP 中,您會嘗試避免變異資料結構。因此,建構函式欄位預設為
val
是合理的。由於案例類別的實例無法變更,因此可以輕鬆地共用它們,而不用擔心變異或競爭條件。 - 您可以使用
copy
方法作為範本,建立新的(可能已變更)實例,而不是變異實例。這個程序可以稱為「複製時更新」。 - 自動為您產生
unapply
方法,也讓案例類別可以透過模式比對以進階的方式使用。
案例物件
案例物件對物件來說,就像案例類別對類別一樣:它們提供許多自動產生的方法來讓它們更強大。它們特別有用於您需要單例物件,而且需要一些額外功能時,例如在 match
表達式中使用模式比對。
當您需要傳遞不可變訊息時,案例物件很有用。例如,如果您正在進行音樂播放器專案,您會建立一組命令或訊息,如下所示
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()