Kotlin Tutorial(12)繼承與函式覆寫 << 前情
應用程式與API的設計,經常使用繼承建立基礎的架構,在使用這些繼承架構設計各種應用與功能的時候,如果利用多型的特性,就可以讓程式碼大幅度的簡化,不論是設計或維護,都會容易多了。
多型的特性與規則
記事資料管理App的主要類別中,除了基本的記事資料Item類別外,為了增加App的功能,另外宣告兩個Item的子類別,可以儲存照片的ImageItem與儲存錄音的RecordItem類別:
/* net.macdidi5.kotlin.tutorial.ch13.Item.kt */
package net.macdidi5.kotlin.tutorial.ch13
// 記事資料類別
open class Item (val id: Long,
var title: String,
var content: String) {
fun getReduceContent(length: Int = 5) =
"${content.substring(0 until length)}..."
open fun getDetails() = "id=$id, title=$title, content=$content"
}
/* net.macdidi5.kotlin.tutorial.ch13.ImageItem.kt */
package net.macdidi5.kotlin.tutorial.ch13
// 照片記事資料類別
class ImageItem(id: Long,
title: String,
content: String,
var imageFile: String): Item(id, title, content){
// 使用super關鍵字呼叫父類別的函式
override fun getDetails() = "${super.getDetails()}, imageFile=$imageFile"
}
/* net.macdidi5.kotlin.tutorial.ch13.RecordItem.kt */
package net.macdidi5.kotlin.tutorial.ch13
// 錄音記事資料類別
class RecordItem(id: Long,
title: String,
content: String,
var recordFile: String): Item(id, title, content){
// 使用super關鍵字呼叫父類別的函式
override fun getDetails() = "${super.getDetails()}, recordFile=$recordFile"
}
使用類別宣告與建立物件的時候,依照應用程式的需求,分別建立各種需要的物件:
val item: Item =
Item(1, "Hello", "Hello Kotlin!")
val imageItem: ImageItem =
ImageItem(2, "Greeting", "Good morning", "kotlin.jpg")
val recordItem: RecordItem =
RecordItem(3, "Shopping", "Out of milk", "notify.mp3")
上面宣告與建立物件的作法,在一般的情況下,可以省略變數的型態,Kotlin會自動依照建立的物件判斷。使用多型的特性,可以使用下面的語法宣告與建立一個物件變數:
// 多型宣告
val 變數名稱: 父類別名稱 = 子類別名稱(...)
因為ImageItem與RecordItem都是Item的子類別,所以下面的宣告都是正確的:
// 宣告的型態是「Item」,建立的物件是「ImageItem」
val imageItem: Item =
ImageItem(2, "Greeting", "Good morning", "kotlin.jpg")
// 宣告的型態是「Item」,建立的物件是「RecordItem」
val recordItem: RecordItem =
RecordItem(3, "Shopping", "Out of milk", "notify.mp3")
使用多型特性宣告的物件變數,還是必須遵守變數型態的用法:
// 宣告的型態是「Item」,建立的物件是「ImageItem」
val imageItem: Item =
ImageItem(2, "Greeting", "Good morning", "kotlin.jpg")
// 正確,Item型態有id屬性
println("${item.id}")
// 編譯錯誤,Item型態並沒有imageFile屬性
//println("${item.imageFile}")
多型的使用前與使用後
要瞭解並使用多型的應用,最好還是透過比較的方式。如果應用程式需要處理多個不同的記事資料,使用陣列是常見的作法:
/* net.macdidi5.kotlin.tutorial.ch13.Demo02.kt */
package net.macdidi5.kotlin.tutorial.ch13
fun main(args: Array<String>) {
// 記事資料物件陣列
var items: Array<Item> = arrayOf(
Item(1, "A", "X"),
Item(2, "B", "Y"),
Item(3, "C", "Z")
)
for (i in items) {
println(i.getDetails())
}
// 照片記事資料物件陣列
var imageItems: Array<ImageItem> = arrayOf(
ImageItem(11, "IA", "IX", "IF1"),
ImageItem(12, "IB", "IY", "IF2"),
ImageItem(13, "IC", "IZ", "IF3")
)
for (ii in imageItems) {
println(ii.getDetails())
}
// 錄音記事資料物件陣列
var recordItems: Array<RecordItem> = arrayOf(
RecordItem(21, "RA", "RX", "RF1"),
RecordItem(22, "RB", "RY", "RF2"),
RecordItem(23, "RC", "RZ", "RF3")
)
for (ri in recordItems) {
println(ri.getDetails())
}
}
因為要處理三種記事資料,所以上面的範例建立Item、ImageItem與RecordItem三個陣列,分別儲存不同種類的記事資料,再使用三個迴圈顯示這些陣列的資訊。這是沒有使用多型特性的作法。
執行上面範例的工作,如果使用多型特性的話,就可以大幅度簡化程式碼的設計:
/* net.macdidi5.kotlin.tutorial.ch13.Demo03.kt */
package net.macdidi5.kotlin.tutorial.ch13
fun main(args: Array<String>) {
// 因為父類別型態(Item)的變數,
// 可以儲存子類別(ImageItem與RecordIem)的物件,
// 所以使用父類別Item型態宣告與建立陣列
var items: Array<Item> = arrayOf(
// 記事資料物件
Item(1, "A", "X"),
Item(2, "B", "Y"),
Item(3, "C", "Z"),
// 照片記事資料物件
ImageItem(11, "IA", "IX", "F1"),
ImageItem(12, "IB", "IY", "F2"),
ImageItem(13, "IC", "IZ", "F3"),
// 錄音記事資料物件
RecordItem(21, "RA", "RX", "RF1"),
RecordItem(22, "RB", "RY", "RF2"),
RecordItem(23, "RC", "RZ", "RF3")
)
// 使用一個迴圈處理所有記事資料物件
for (i in items) {
println(i.getDetails())
}
}
使用多型的特性決定參數型態
多型特性也經常使用在函式與建構式的參數型態宣告。例如下面的範例程式,是沒有使用多型特性設計的函式:
/* net.macdidi5.kotlin.tutorial.ch13.Demo04.kt */
package net.macdidi5.kotlin.tutorial.ch13
// 接收記事資料物件參數的函式
fun showItem(item: Item) {
println("Item: ${item.getDetails()}")
}
// 接收照片記事資料物件參數的函式
fun showItem(imageItem: ImageItem) {
println("ImageItem: ${imageItem.getDetails()}")
}
// 接收錄音記事資料物件參數的函式
fun showItem(recordItem: RecordItem) {
println("RecordItem: ${recordItem.getDetails()}")
}
fun main(args: Array<String>) {
val item = Item(1, "Hello", "Hello Kotlin!")
showItem(item)
val imageItem = ImageItem(2, "Hi", "Hello", "kotlin.jpg")
showItem(imageItem)
val recordItem = RecordItem(3, "Shopping", "Out of milk", "notify.mp3")
showItem(recordItem)
}
為不同記事物件宣告處理的函式,雖然在設計上是很清楚的,不過上面範例程式執行的工作,如果使用多型的特性,其實只要宣告一個參數型態是Item的函式就可以了:
/* net.macdidi5.kotlin.tutorial.ch13.Demo05.kt */
package net.macdidi5.kotlin.tutorial.ch13
// 使用父類別Item型態宣告參數,
// 這個函式可以接收Item物件,也可以接收Item子類別物件
fun showItem05(item: Item) {
println("Item: ${item.getDetails()}")
}
fun main(args: Array<String>) {
var items: Array<Item> = arrayOf(
Item(1, "A", "X"),
Item(2, "B", "Y"),
Item(3, "C", "Z"),
ImageItem(11, "IA", "IX", "F1"),
ImageItem(12, "IB", "IY", "F2"),
ImageItem(13, "IC", "IZ", "F3"),
RecordItem(21, "RA", "RX", "RF1"),
RecordItem(22, "RB", "RY", "RF2"),
RecordItem(23, "RC", "RZ", "RF3")
)
for (i in items) {
// 包含Item與它的子類別型態物件,都可以傳送給這個函式
showItem05(i)
}
// 顯示:
// Item: id=1, title=A, content=X
// Item: id=2, title=B, content=Y
// Item: id=3, title=C, content=Z
// Item: id=11, title=IA, content=IX, imageFile=F1
// Item: id=12, title=IB, content=IY, imageFile=F2
// Item: id=13, title=IC, content=IZ, imageFile=F3
// Item: id=21, title=RA, content=RX, recordFile=RF1
// Item: id=22, title=RB, content=RY, recordFile=RF2
// Item: id=23, title=RC, content=RZ, recordFile=RF3
}
上面的範例在執行以後顯示的結果是有一點問題的,顯示的記事資料雖然沒有錯,不過在最前面都會顯示Item,沒有辦法分辨是哪一種記事資料。
判斷是哪一種物件
多型特性應用在參數的時候,如果需要判斷接收的參數是哪一種物件,再根據物件的種類執行不同的工作,可以使用下面的語法執行判斷的工作:
如果需要判斷物件變數是否「不是」某個類別的物件,可以使用下面的語法:
下面的程式示範執行記事物件的判斷:
/* net.macdidi5.kotlin.tutorial.ch13.Demo09.kt */
package net.macdidi5.kotlin.tutorial.ch13
fun main(args: Array<String>) {
val item: Item =
Item(1, "Hello", "Hello Kotlin!")
val imageItem: Item =
ImageItem(2, "Greeting", "Good morning", "kotlin.jpg")
val recordItem: Item =
RecordItem(3, "Shopping", "Out of milk", "notify.mp3")
println("item is Item: ${item is Item}")
println("imageItem is ImageItem: ${imageItem is ImageItem}")
println("recordItem is RecordItem: ${recordItem is RecordItem}")
// 顯示:
// item is Item: true
// imageItem is ImageItem: true
// recordItem is RecordItem: true
println()
println("imageItem is Item: ${imageItem is Item}")
println("recordItem is Item: ${recordItem is Item}")
// 顯示:
// imageItem is Item: true
// recordItem is Item: true
println()
println("imageItem is RecordItem: ${imageItem is RecordItem}")
println("recordItem is ImageItem: ${recordItem is ImageItem}")
// 顯示:
// imageItem is RecordItem: false
// recordItem is ImageItem: false
}
瞭解物件的判斷以後,就可以更靈活的設計應用程式功能:
/* net.macdidi5.kotlin.tutorial.ch13.Demo06.kt */
package net.macdidi5.kotlin.tutorial.ch13
fun showItem06(item: Item) {
// 判斷是否為ImageItem物件
if (item is ImageItem) {
println("ImageItem: ${item.getDetails()}")
}
// 判斷是否為RecordItem物件
else if (item is RecordItem) {
println("RecordItem: ${item.getDetails()}")
}
// 不是ImageItem或RecordItem的話,就是Item物件
else {
println("Item: ${item.getDetails()}")
}
}
fun main(args: Array<String>) {
var items: Array<Item> = arrayOf(
Item(1, "A", "X"),
Item(2, "B", "Y"),
Item(3, "C", "Z"),
ImageItem(11, "IA", "IX", "F1"),
ImageItem(12, "IB", "IY", "F2"),
ImageItem(13, "IC", "IZ", "F3"),
RecordItem(21, "RA", "RX", "RF1"),
RecordItem(22, "RB", "RY", "RF2"),
RecordItem(23, "RC", "RZ", "RF3")
)
for (i in items) {
showItem06(i)
}
}
還原變數的型態-使用智慧轉型
使用多型特性設計應用程式需要的功能,可能需要執行一些比較特殊的工作,例如在判斷接收的參數物件以後,如果是照片或錄音記事資料,需要執行與檔案相關的工作。這樣的需求可以使用Kotlin提供的「智慧轉型、Smart Casts」:
/* net.macdidi5.kotlin.tutorial.ch13.Demo07.kt */
package net.macdidi5.kotlin.tutorial.ch13
fun showItem07(item: Item) {
// 編譯錯誤,Item型態並沒有imageFile屬性
//println("${item.imageFile}")
// 編譯錯誤,Item型態並沒有recordFile屬性
//println("${item.recordFile}")
// 先判斷是否為ImageItem物件
if (item is ImageItem) {
// 才可以使用ImageItem型態的imageFile屬性
println("ImageItem: ${item.id}, ${item.imageFile}")
}
// 先判斷是否為RecordItem物件
else if (item is RecordItem) {
// 才可以使用RecordItem型態的recordFile屬性
println("RecordItem: ${item.id}, ${item.recordFile}")
}
else {
println("Item: ${item.id}")
}
}
fun main(args: Array<String>) {
var items: Array<Item> = arrayOf(
Item(1, "A", "X"),
Item(2, "B", "Y"),
Item(3, "C", "Z"),
ImageItem(11, "IA", "IX", "F1"),
ImageItem(12, "IB", "IY", "F2"),
ImageItem(13, "IC", "IZ", "F3"),
RecordItem(21, "RA", "RX", "RF1"),
RecordItem(22, "RB", "RY", "RF2"),
RecordItem(23, "RC", "RZ", "RF3")
)
for (i in items) {
showItem07(i)
}
}
需要設計智慧轉型功能的時候,也可以使用when特別設計的語法:
/* net.macdidi5.kotlin.tutorial.ch13.Demo08.kt */
package net.macdidi5.kotlin.tutorial.ch13
fun showItem08(item: Item) {
// 準備判斷接收到的參數是哪一種物件
when (item) {
// 如果是ImageItem物件
is ImageItem ->
println("ImageItem: ${item.id}, ${item.imageFile}")
// 如果是RecordItem物件
is RecordItem ->
println("RecordItem: ${item.id}, ${item.recordFile}")
// 如果是Item物件
else ->
println("Item: ${item.id}")
}
}
fun main(args: Array<String>) {
var items: Array<Item> = arrayOf(
Item(1, "A", "X"),
Item(2, "B", "Y"),
Item(3, "C", "Z"),
ImageItem(11, "IA", "IX", "F1"),
ImageItem(12, "IB", "IY", "F2"),
ImageItem(13, "IC", "IZ", "F3"),
RecordItem(21, "RA", "RX", "RF1"),
RecordItem(22, "RB", "RY", "RF2"),
RecordItem(23, "RC", "RZ", "RF3")
)
for (i in items) {
showItem08(i)
}
}
使用智慧轉型的作法,就可以設計應用程式大部份的功能需求。不過在使用API或比較特殊的應用,可以使用下面的語法,自己執行轉換型態的工作:
下面的程式片段示範執行轉型的作法:
val i: Item = ImageItem(12, "A", "X", "IF.jpg")
val r: Item? = null
// 編譯錯誤,因為i變數的型態是Item,
// 所以不能指定給ImageItem型態的變數
// val ii: ImageItem = i
// 把i變數轉換型態為ImageItem,就可以指定給ImageItem型態的變數
val x: ImageItem = i as ImageItem
// 因為變數r是null值,執行以後會發生例外
// val y: RecordItem = r as RecordItem
val z: RecordItem? = r as RecordItem?
println(z?.getDetails())
// 顯示: null
下一步
瞭解物件導向封裝、繼承與多型,你已經認識物件導向基本的概念了。接下來準備認識介面(interface),不論是API、一般的應用程式,或是Andriod App的架構,都是一種很常見的設計。
相關的檔案都可以在GitHub瀏覽與下載。
http://github.com/macdidi5/Kotlin-Tutorial
後續 >> Kotlin Tutorial(14)列舉型態
|