Kotlin Tutorial(13)多型的特性與應用 by Michael | CodeData
top

Kotlin Tutorial(13)多型的特性與應用

分享:

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,沒有辦法分辨是哪一種記事資料。

判斷是哪一種物件

多型特性應用在參數的時候,如果需要判斷接收的參數是哪一種物件,再根據物件的種類執行不同的工作,可以使用下面的語法執行判斷的工作:

物件變數 is 類別名稱

如果需要判斷物件變數是否「不是」某個類別的物件,可以使用下面的語法:

物件變數 !is 類別名稱

下面的程式示範執行記事物件的判斷:

/* 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或比較特殊的應用,可以使用下面的語法,自己執行轉換型態的工作:

物件變數 as 要轉換的類別名稱

下面的程式片段示範執行轉型的作法:

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瀏覽與下載。

https://github.com/macdidi5/Kotlin-Tutorial

後續 >> Kotlin Tutorial(14)列舉型態

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言