iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
1

泛型 Generic 介紹

在 Collections 章節中,我們有提到 ListSet 等集合用法,眼尖的朋友可能會發現到,在宣告一個新集合時,我們都必須使用 < > 和設定型態來進行宣告, 而這樣的方法其實就是一種泛型(Generic)應用,集合只是一個容器,為我們提供迭代元素、新增元素、刪除元素等操作方法,當我們需求上需要儲存什麼樣別的資料,再為這個容器加上型別即可使用。

可能有朋友會好奇,為什麼會需要使用泛型?假設今天我們有一個集合物件,裡面充滿著各種型別的資料,那我們在引用這個集合時,必然會在程式撰寫上處理許多型別轉型的工作,而型別轉型工作會讓我們程式多了一層轉換工作,所以為了減少不必要的轉型工作,我們可以提前在編譯期間(Compile Time)利用泛型告知此集合或方法屬於哪種型別,也是方便我們在開發時清楚要使用什麼類型的資料進行溝通,所以在型別安全檢查、程式碼品質、開發效率等都會帶來好處。

泛型使用

泛型可以讓我們使用在類別、介面、函數上,我們可以直接使用下面範例來觀察,我們定義一個使用泛型的Person 類別,再定義其他資料類別,

fun main() {
    val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345"))
    val student: Person<Student> = Person(Student("Devin", "B991"))
    
    println("老師姓名: ${teacher.data.name}, 職員編號: ${teacher.data.employeeNumber}")
    println("學生姓名: ${student.data.name}, 學號: ${student.data.studentID}")

	// 印出以下結果:
	// 老師姓名: Eric, 職員編號: A12345
	// 學生姓名: Devin, 學號: B991
}

// 建立一個使用泛型物件的類別
class Person<T>(person: T) {
    var data: T = person
}

// 建立一個老師類別,具有name、employeeNumber參數
class Teacher(val name: String, val employeeNumber: String)

// 建立一個學生類別,具有name、studentID參數
class Student(val name: String, val studentID: String)

泛型參數我們通常會利用字母 T(英文 Type)表示,若要使用其他名稱也可以,但在支援泛型的程式語言中大多使用 T 來表示,這樣可以讓其他開發者更容易了解我們的程式碼,而泛型還有其他常用的命名,如下:

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • R - Return
  • S, U, V etc. - 2nd, 3rd, 4th types

多泛型參數

泛型也允許使用多個泛型參數,參數名稱建議可參考上面常見規範,我們可以將上面的範例進行修改,在原本的 Person 類別中加入一個支援多種泛型的函數,如下範例:

fun main() {
    val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345"))
    val student: Person<Student> = Person(Student("Devin", "B991"))

    println("老師姓名: ${teacher.data.name}, 職員編號: ${teacher.data.employeeNumber}")
    println("學生姓名: ${student.data.name}, 學號: ${student.data.studentID}")

    teacher.speak { println("${teacher.data.name}: 開始上課")}
    student.speak { println("${student.data.name}: 老師好")}
}

// 建立一個使用泛型物件的類別
class Person<T>(person: T) {
    var data: T = person
	
	// 定義一個支援 lambda 運算式的函數,R為新增的泛型參數,作為函數的返回類型
    fun <R> speak(func: (T) -> R): R? {
        return func(data)
    }
}

data class Teacher(val name: String, val employeeNumber: String)

data class Student(val name: String, val studentID: String)

多泛型實例操作

上面範例我們都只使用一個資料進行操作,若我們想要一次使用多筆資料,此時可以使用 vararg 關鍵字,讓泛型類別可支援多個參數,參數即為元素陣列,而既然是陣列資料,我們就可以使用索引進行取值,我們可以搭配 get 運算函數進行索引取值動作,如下範例:

fun main() {
    val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345"))
    val student: Person<Student> = Person(Student("Devin", "B991"), Student("Jack", "B992"))

    println("老師姓名: ${teacher[0]?.name}, 職員編號: ${teacher[0]?.employeeNumber}")
    println("學生姓名: ${student[0]?.name}, 學號: ${student[0]?.studentID}")
    println("學生姓名: ${student[1]?.name}, 學號: ${student[1]?.studentID}")

    teacher.speak(0) { println("${teacher[0]?.name}: 開始上課")}
    student.speak(0) { println("${student[0]?.name}: 老師好")}
    student.speak(0) { println("${student[1]?.name}: 老師好")}
}

// 建立一個使用泛型物件的類別
class Person<T>(vararg person: T) {
	// 修改 data 資料型別為 Array<out T>, out 代表我們要將泛型 T 作為內部函數的返回值
    var data: Array<out T> = person

    operator fun get(index: Int): T? = data[index]

    fun <R> speak(index: Int, func: (T) -> R): R? {
        return func(data[index])
    }
}

data class Teacher(val name: String, val employeeNumber: String)

data class Student(val name: String, val studentID: String)

in & out

在前一個範例中我們有用到 out 關鍵字,我們發現若在泛型類別中要將泛型用在內部函數的返回值上,必須加上 out 關鍵字,而 out 關鍵字其實有一個夥伴- in 關鍵字,in 則是將泛型用在函數參數值上。

而泛型參數其實扮演兩種角色:生產者(producer)消費者(consumer),若身為生產者時,只能讀不能寫;消費者則相反,不能讀只能寫,而生產者為 out 關鍵字,消費者則為 in 關鍵字。

接下來,我們利用範例來觀察 in & out 的實際狀況,首先介紹 out 關鍵字, out 泛型可以讓我們將子類別的泛型物件賦值給父類別泛型物件:

fun main() {
    // 正常情況
	// 食品商店屬於食品商店
    val producer1 : Production<Food> = FoodStore()
	// 速食商品屬於食品商店
    val producer2 : Production<Food> = FastFoodStore()   
	// 漢堡商店也屬於食品商店
    val producer3 : Production<Food> = InOutBurger()     

	  // 錯誤情況
	  // 食品商店不見得屬於漢堡商店  
//    val producer1 : Production<Burger> = FoodStore()   
      // 速食商店也不見得屬於漢堡商店
//    val producer2 : Production<Burger> = FastFoodStore() 
	  // 漢堡商店屬於漢堡商店
//    val producer3 : Production<Burger> = InOutBurger()
}

open class Food
open class FastFood : Food()
class Burger : FastFood()

// 定義一個生產者介面,運用 out 關鍵字
interface Production<out T> {
	// 將泛型 T 作為回傳值
    fun produce(): T
}

// 定義一個 FoodStore 類別,並利用 Food 類別實作生產者介面
class FoodStore : Production<Food> {
    override fun produce(): Food {
        println("食品商店")
        return Food()
    }
}

// 定義一個 FastFoodStore 類別,並利用 FastFood 類別實作生產者介面
class FastFoodStore : Production<FastFood> {
    override fun produce(): FastFood {
        println("速食商店")
        return FastFood()
    }
}

// 定義一個 FastFoodStore 類別,並利用 FastFood 類別實作生產者介面
class InOutBurger : Production<Burger> {
    override fun produce(): Burger {
        println("漢堡商店")
        return Burger()
    }
}

再來是 in 關鍵字的用法,in 泛型可以讓我們將父類別泛型物件賦值給子類別泛型物件,以下是範例:

fun main() {
    // 正常情況
    // 想購買肉品食物的消費者可能也會想買漢堡
    val consumer1 : Consumer<Burger> = PurchaseFood()
    // 想購買速食食物的消費者可能也會想買漢堡
    val consumer2 : Consumer<Burger> = EatFastFood()  
    val consumer3 : Consumer<Burger> = EatBurger()

    // 錯誤情況
//    val consumer1 : Consumer<Food> = PurchaseFood()
      // 直接想購買速食商品的消費者通常不會想買肉品
//    val consumer2 : Consumer<Food> = EatFastFood()
      // 直接想購買漢堡商品的消費者通常不見想買肉品
//    val consumer3 : Consumer<Food> = EatBurger()    
}

// 利用 in 關鍵字配合泛型
interface Consumer<in T> {
	// 將泛型 T 作為函數參數
    fun consume(item: T)
}

// 想購買食品商品
class PurchaseFood : Consumer<Food> {
    override fun consume(item: Food) {
        println("購買食品商品")
    }
}

// 想購買速食食物
class EatFastFood : Consumer<FastFood> {
    override fun consume(item: FastFood) {
        println("購買速食食物")
    }
}

// 想購買漢堡食物
class EatBurger : Consumer<Burger> {
    override fun consume(item: Burger) {
        println("購買漢堡食物")
    }
}

Reference


上一篇
[Day 09] 遠征 Kotlin × 例外處理
下一篇
[Day 11] 遠征 Kotlin × 函數式程式設計
系列文
30天從零撰寫 Kotlin 語言並應用於 Spring Boot 開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言