更多的類別!

前面我們使用class關鍵字宣告了我們自訂的類別,然而在Kotlin裡面可以運用的東西不只有如此而已。我們接下來會一個一個介紹

object

我們在寫程式的時候會需要一個地方來暫存我們讀取到的資料,這樣我們就不需要每次用到資料的時候就需要重新讀檔,這樣可以節省時間也能加快運行速度,一舉兩得。在這樣的想法之下我們自然會希望暫存資料的物件不管我們在哪裡呼叫都會是同一個,這樣我們存取到的資料才會是一樣的。我們可以利用object關鍵字來定義一個不管在哪裡呼叫都會是同一個實體的類別,我們稱這種“不管在哪裡呼叫都會是同一個實體的方式”叫做單例。

object ClassRoom { }

這就是一個物件的最簡單定義,我們也可以在物件裡面新增一些屬性與函數,跟類別一樣

object ClassRoom {
    init {
        println("歡迎來到Kotlin教室!")
    }
    
    private val _students = mutableMapOf<String, String>()
    val student: Map<String, String>
        get() = _students

    fun addStudent(id: String, name: String) {
        _students[id] = name
    }

    fun removeStudent(id: String) {
        _students.remove(id)
    }
}

使用已經定義好的物件也很簡單

ClassRoom.addStudent("TIP101-01", "小黑")
ClassRoom.addStudent("TIP101-02", "小固")
println(ClassRoom.student.size)

物件也可以繼承一個類別,特性跟一般的類別是一樣的,所以這裡就不贅述了

伴生物件

如果需要把物件的初始化關聯到某一個類別的話,我們就可以使用伴生物件我們來看看伴生物件要怎麼建立:

class Foo {
    // ......
    companion object {
        fun a() { 
            println("Companion object of Foo!")
        }
    }
}

fun main() {
    Foo.a()
}

伴生物件常常會使用在單例模式

class Foo private constructor(a: Int) {
    val map = mutableMapOf<String, String>()
    
    companion object {
        private var _instance: Foo? = null
        val instance: Foo
            get() = _instance!!
        
        fun getInstance(a: Int): Foo {
            if (_instance == null) {
                _instance = Foo(a)
            }
            return _instance!!
        }
    }
}

fun main() {
    Foo.instance.map["TIP101-01"] = "小黑"
    println(Foo.instance.map["TIP101-01"])
}

或是用來封裝一些與類別相關的常數

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        val data = intent.getStringExtra(DATA_KEY) ?: ""
        // ......
    }

    companion object {
        private const val DATA_KEY = "data"
        fun createIntent(context: Context, data: String): Intent {
            return Intent(context, MainActivity::class.java).apply {
                putExtra(DATA_KEY, data)
            }
        }
    }
}

// 在其他地方呼叫會長這樣
val intent = MainActivity.createIntent(this, "123")
startActivity(intent)

匿名物件

我們可以使用object關鍵字來創建一個沒有名字的物件

val s = object {
    fun a() { println("This is a anonymous object.") }
}
s.a()

相較於在Kotlin裡面大量被使用的lambda函數,匿名物件就比較不常被使用,通常都是臨時要繼承物件或是實作介面的時候才會使用到他(例如gson在把字串轉換為集合的時候)

內部類別

我們可以在類別裡面宣告另一個類別,這個寫在裡面的類別我們叫他內部類別。內部類別可以取用外部類別的屬性與函數(包含其伴生物件),常見於建造者模式

class ClassRoom private constructor(val classId: String){
    // ......
    var teacher: String? = null
    // ......
    class Builder(private val classId: String) {
        private var _teacher: String? = null

        fun setTeacher(teacher: String): Builder {
            _teacher = teacher
            return this
        }
        fun build(): ClassRoom = _instance ?: ClassRoom(classId).also {
            it.teacher = _teacher
            _instance = it
        }
    }
}

fun main() {
    ClassRoom.Builder("403")
        .setTeacher("小黑")
        .build()
    println("教室編號:${ClassRoom.instance.classId}")
    println("任課教師:${ClassRoom.instance.teacher}")
}

資料類別(data class)

資料類別是一個很適合拿來處理資料的類別,在Kotlin裡面幫資料類別預設實作了一般類別沒有的東西。

首先我們來看看資料類別的宣告

data class Coordinate(val x: Int, val y : Int)

我們看到一個跟宣告一般類別很類似的方式但是資料類別有一些限制:

  • 資料類別一定要自訂主建構式

  • 資料類別的主建構式裡只能宣告屬性

宣告完後我們來使用看看

val c1 = Coordinate(1, 0)
val c2 = Coordinate(1, 0)
println(c1 == c2)
println(c1)

同時我們也可以把Coordinate前面的data拿掉讓他變成一般的類別看看執行後的結果有什麼差別。為什麼會有這樣的差別呢?那是因為資料類別會依照主函數裡面定義的屬性來做一些預設實作:

資料類別預設覆寫的函數

hashCode、equals

資料類別會覆寫hashCode跟equals,讓兩個不同的物件只要資料相同就能相等

toString

資料類別覆寫toString讓我們能輕鬆印出裡面的資料

資料類別額外實作的函數

componentN

我們看到Coordinate有component1、component2兩個函數,這是因為主函數裡面定義了兩個屬性, 主函數裡面有幾個屬性就會生成幾個。有了這一系列componentN的函數,我們就能幫資料類型“解構”

val c = Coordinate(1, 0)
val (x, y) = c
println(x)
println(y)

copy

在大多數情況下資料類別主函數裡面的屬性我們會宣告成val,因為我們不希望資料被隨意修改,可是資料還是會有需要變動的時候這時候使用copy就是一個不錯的方法

var c = Coordinate(1, 0)
println(c)
c = c.copy(x = 3)
println(c)
c = c.copy(y = 5)
println(c)

copy會複製一個新的物件,保留沒有傳入引數的屬性,並且把有傳入引數的屬性改成新的值,這樣我們就能方便的複製物件、更改屬性

列舉類別(Enum)

列舉顧名思義就是把會用到的東西一個一個列出來,比如說我們想到方向就會想到東西南北,那我們就可以這樣定義

enum class Direction {
    EAST,
    WEST,
    SOUTH,
    NORTH,
}

定義好了之後我們就可以這樣呼叫enum

val direction = Direction.NORTH

使用列舉類別作when判斷的時候,有一個特別的地方

when (direction) {
    Direction.EAST -> TODO()
    Direction.WEST -> TODO()
    Direction.SOUTH -> TODO()
    Direction.NORTH -> TODO()
}

因為enum限定了這個變數的可能性,所以當我們把可能性全部寫出來後,自然就不剩下其他可能了,自然也就不需要else。

我們也可以在enum裡面宣告屬性:

enum class Direction(private val coordinate: Coordinate) {
    EAST(Coordinate(1, 0)),
    WEST(Coordinate(-1, 0)),
    SOUTH(Coordinate(0, -1)),
    NORTH(Coordinate(0, 1)),
}

也可以在enum裡面寫函數:

enum class Direction(private val coordinate: Coordinate) {
    EAST(Coordinate(1, 0)),
    WEST(Coordinate(-1, 0)),
    SOUTH(Coordinate(0, -1)),
    NORTH(Coordinate(0, 1)), ;

    fun move(currentCoordinate: Coordinate): Coordinate = Coordinate(
        currentCoordinate.x + coordinate.x,
        currentCoordinate.y + coordinate.y
    )
}

這邊要記得,如果需要在enum裡面寫函數,必須在最後一個enum後面加一個“;”把兩邊隔開

運算子重載

在上面的例子,我們把兩個座標相加計算出新的座標。可是在程式裡面每個相加的地方都必須要這樣寫未免顯得有點笨重,這時候我們就可以善用運算子重載的功能。

data class Coordinate(val x: Int, val y : Int) {
    operator fun plus(other: Coordinate): Coordinate = Coordinate(
        x + other.x,
        y + other.y
    )
}

之前我們已經有說明過Kotlin裡面幾乎所有的運算子背後都有一個代表他的函數。只要實作了運算子函數,我們就可以使用相對應的運算子,我們在這邊以Coordinate的plus為例示範怎麼樣重載

有了運算子重載後的Coordinate在相加的時候就可以這樣使用了

fun move(currentCoordinate: Coordinate): Coordinate = coordinate + currentCoordinate

封裝類別與介面(sealed)

使用了sealed關鍵字來宣告的類別或是介面,無法在模組或是套件之外被繼承,所以可以在撰寫程式碼的時候就確實知道所有實作他的類別,所以我們在使用類型判斷的時候就可以獲得跟enum類似的用法

sealed class Error(val message: String) {
    class NetworkError : Error("Network failure") {
        override fun dealWithError() {
            TODO("Not yet implemented")
        }
    }

    class DatabaseError : Error("Database cannot be reached") {
        override fun dealWithError() {
            TODO("Not yet implemented")
        }
    }

    class UnknownError : Error("An unknown error has occurred") {
        override fun dealWithError() {
            TODO("Not yet implemented")
        }
    }

    abstract fun dealWithError()
}
fun main() {
    // connect API
    val responseCode = 1
    val error = when (responseCode) {
        1 -> Error.NetworkError()
        2 -> Error.DatabaseError()
        else -> Error.UnknownError()
    }
    
    error.dealWithError()
}

sealed class為抽象類別的一種,使用時可以視為抽象類別。

Last updated