apply() 與 unapply() 方法


如果你可以定義 案例類別(Case class),那麼你可以運用模式比對的特性,完成如下的變數指定動作:
case class Apple(price: Int, weight: Int)

val apple = Apple(10, 20)
val Apple(p, w) = apple
println(p) // 10
println(w) // 20

定義案例類別的問題之一,在於你必須實際定義出暴露出成員資訊的類別,有時這並不是你想要的,或者你沒辦法定義出這樣的類別。舉個例子來說,字串就是一個例子,如果你有一些學生資料,每筆是"B123456,Justin,Kaohsiung"的格式,你希望分別取得學號、名稱與出生地資訊,基本的作法是:
def separate(s: String) = {
val parts = s.split(",")
if(parts.length == 3) (parts(0), parts(1), parts(2)) else None
}

val (number, name, addr) = separate("B123456,Justin,Kaohsiung")
println(number) // B123456
println(name) // Justin
println(addr) // Kaohsiung

 separate()函式傳回三個元素的Tuple,你運用了Tuple模式比對的特性傳回分割後的個別字串。然而,如果你可以這麼作的話,程式看起來會更清楚:
// 這有可能嗎?
val Student(number, name, addr) = "B123456,Justin,Kaohsiung"

這看起來像是上面案例類別的模式比對,問題是字串根本不是案例類別,怎麼可能這麼作?事實上是可行的,你可以定義一個單例物件如下,就可以執行這樣的模式比對功能:
object Student {
def unapply(str: String): Option[(String, String, String)] = {
val parts = str.split(",")
if (parts.length == 3) Some(parts(0), parts(1), parts(2)) else None
}
}

val Student(number, name, addr) = "B123456,Justin,Kaohsiung"
println(number) // B123456
println(name) // Justin
println(addr) // Kaohsiung

unapply()方法可以接受你所提供的物件(在這邊是以字串為例,事實上可以是任何類型),經用你所定義的unapply()方法內容處理後傳回Option物件,事實上,在上例的例子中,編譯器會作如下的處理:
val Some((number, name, addr)) = Student.unapply("B123456,Justin,Kaohsiung")

unapply()方法稱之為提取方法(Extraction method),而像Student這樣只具備提取方法的物件稱之為提取器(Extractor),提取器讓你對非案例類別的實例,也可以進行模式比對,例如搭配match運算式的一個例子如下:
val students = List(
"B123456,Justin,Kaohsiung",
"B98765,Monica,Kaohsiung",
"B246819,Bush,Taipei"
)

students.foreach(_ match {
case Student(nb, name, addr) => println(nb + ", " + name + ", " + addr)
})

也可以進一步使用模式比對的各種特性,例如使用 模式防護(Pattern guard),找出住在高雄的學生姓名:
val students = List(
"B123456,Justin,Kaohsiung",
"B98765,Monica,Kaohsiung",
"B246819,Bush,Taipei"
)

students.foreach(_ match {
case Student(_, name, addr) if addr == "Kaohsiung" => println(name)
case _ =>
})

相對於unapply()方法,apply()方法則稱之為注入方法(Injection method),提取方法與注入方法通常同時存在(但非必要),apply()方法與unapply()方法的作用通常是相反的,例如:
object Student {
def apply(number: String, name: String, addr: String) = {
number + "," + name + "," + addr
}

def unapply(str: String) = {
val parts = str.split(",")
if (parts.length == 3) Some(parts(0), parts(1), parts(2)) else None
}
}

例如,你可以讀入文字檔案,使用提取方法來取出每筆資料的學號、名稱、出生地,反過來,如果輸入學生資料的學號、名稱、出生地,利用注入方法組合字串,再寫出至檔案中:
val number = readLine
val name = readLine
val addr = readLine

val data = Student(number, name, addr)
println(data) // 實際也許是寫入文字檔案