Kotlin核心語法(五):運算符重載以及其它的約定

博客主頁java

java在標準庫中有一些與特定的類相關聯的語言特性,如實現了java.lang.Iterable接口的對象能夠在for循環中使用,實現了java.lang.AutoCloseable接口的對象能夠在try-with-resources語句中使用。算法

但在kotlin中,一些功能是與特定的函數名相關,而不是與特定的類型綁定。kotlin使用約定的原則,不像java依賴類型。kotlin能夠經過擴展函數機制來爲現有的類增添新的方法,能夠把任意約定方法定義爲擴展函數。數據庫

一. 重載算術運算符

java中,算術運算符只能用於基本數據類型,+運算符能夠與String值一塊兒使用。若是給集合添加元素時,想要可以用 += 運算符就完美。在kotlin中,是能夠這樣作的。segmentfault

1. 重載二元算術運算

先來看一個例子:定義Point類(表明一個點),把點的(X, Y)座標分別加到一塊兒。數組

data class Point(val x: Int, val y: Int) {

    // 定義一個名爲 "plus" 的方法
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}

val p1 = Point(1, 2)
val p2 = Point(3, 4)
println(p1 + p2) // 經過使用 + 號 來調用 "plus" 方法
//輸出結果>>> Point(x=4, y=6)

operator關鍵字聲明plus函數。全部的重載運算符函數都須要使用該關鍵字標記,表示這個函數做爲約定實現。安全

使用operator修飾符聲明plus函數後,能夠直接使用 + 號來求和。其實就是調用plus函數。app

除了能夠把運算符聲明爲一個成員函數外,還能夠把它定義爲一個擴展函數ide

operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

kotlin限定了可以重載哪些運算符,以及在類中定義對應名字函數。下表就是可重載的二元運算符:函數

表達式 函數名
a * b times
a / b div
a % b mod
a + b plus
a - b minus

在定義運算符時,兩個運算數能夠是不一樣的類型工具

operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

val p1 = Point(10, 20)
println(p1 * 1.5) // 不會自定支持交換性,不能 1.5 * p1
//輸出結果>>> Point(x=15, y=30)

kotlin運算符不會自定支持交換性,不能 1.5 * p1。若是但願能夠,須要單獨定義一個運算符

operator fun Double.times(p: Point): Point {...}

運算符函數的返回類型也能夠是任意一個運算數類型。
這個運算符,接收一個Char做爲左值,Int做爲右值,而後返回一個String類型。

operator fun Char.times(count: Int) : String {
    return toString().repeat(count)
}

println('b' * 3)
//輸出結果>>> bbb

2. 重載複合賦值運算符

+= 、-=等這些運算符稱爲複合賦值運算符。

var p = Point(1, 2)
p += Point(3, 4) // 等同於 p = p + Point(3, 4)寫法
println(p)

//輸出結果>>> Point(x=4, y=6)

+=運算符能夠修改變量所引用的對象,但不會從新分配引用,如:將一個元素添加到可變集合中

val numbers = ArrayList<Int>()
numbers += 12
println(numbers[0])

//輸出結果>>> 12

若是定義了一個返回值爲Unit,名爲plusAssign函數,kotlin會在用到 += 運算符的地方調用它。二元運算符對應函數,如:minusAssign、timesAssign

kotlin標準庫爲可變集合定義了plusAssign函數:

operator fun <T> MutableCollection<T>.plusAssign(element: T) {
    this.add(element)
}

在代碼中使用 += 時,理論上 plus 和 plusAssign都有可能被調用,因此儘可能不要同時給一個類添加 plus 和 plusAssign 運算。

如:例子中的Point類,是一個不可變的,那麼應該只提供返回一個新值plus運算,若是一個類是可變的,那麼只須要提供plusAssign和相似的運算。

kotlin標準庫支持集合的兩種方法,+ 和 - 運算符老是返回一個新的集合,+= 和 -= 運算符用於可變集合時,始終在一個地方修改它們。而用於只讀集合時,返回一個修改過的副本,意味着只有當引用只讀集合的變量聲明爲var時,才能使用+=和-=。

val list = arrayListOf(1, 2)
list += 3 // += 修改list
val newList = list + listOf(4, 5)  // 返回一個包含全部元素的新列表

println(list)
//輸出結果>>> [1, 2, 3]

println(newList)
//輸出結果>>> [1, 2, 3, 4, 5]

3. 重載一元運算符

預先定義一個名稱來聲明函數(成員函數或者擴展函數),並用修飾符operator標記。

// 一元運算符無參數
operator fun Point.unaryMinus(): Point {
    return Point(-x, -y) // 座標取反,而後返回
}

可重載的一元算法的運算符:

表達式 函數名
+a unaryPlus
-a unaryMinus
!a not
++a,a++ inc
--a, a-- dec

自增運算符案例:

operator fun BigDecimal.inc() = this + BigDecimal.ONE

var bd = BigDecimal.ZERO
println(bd++) //後綴運算:在執行後增長(先返回bd變量當前值,而後執行++)
//輸出結果>>> 0

println(++bd) //前綴運算:在執行前增長(與後綴運算相反)
//輸出結果>>> 2

二. 重載比較運算符

在kotlin中,能夠對於任何對象使用比較運算符(==、!=、>、< 等),不只僅限於基本數據類型,能夠直接使用比較運算符。不像java須要調用equals或者compareTo函數。

1. 等號運算符:"equals"

若是在kotlin中使用 == 運算符,會將被轉換成equals方法的調用。

== 和 != 能夠用於可空運算符,由於這些運算符事實上會檢查運算數是否爲null。比較 a == b 會檢查a是否爲非空,若是不是,就調用a.equals(b),不然,只有兩個參數都是空引用,結果纔是true

案例中Point類,被標記爲數據類(data),equals的實現會由編譯器自動生成。若是須要手動實現,以下:

class Point(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean {
        // 優化:檢查參數是否與this是同一個對象
        if (this === other) return true
        // 檢查參數類型
        if (other !is Point) return false
        // other智能轉換爲Point來訪問x,y屬性
        return other.x == x && other.y == y
    }
}

println(Point(1, 2) == Point(1, 2)) //輸出結果>>> true
println(Point(2, 3) != Point(3, 4)) //輸出結果>>> true
println(null == Point(1, 2)) //輸出結果>>> false

恆等運算符(===)來檢查兩個參數是不是同一個對象的引用(若是是基本數據類型,檢查是不是相同的值)。在實現了equals函數後,一般使用這個(===)運算符來優化調用代碼,可是===運算符不能被重載。

equals方法是在Any類中定義的,因此equals方法不須要標記爲operator,由於Any類中基本方法已經標記了。可是equals不能實現爲擴展方法,由於繼承自Any類的實現始終優先於擴展函數。

public open class Any { 
    // ...
    public open operator fun equals(other: Any?): Boolean
}

!=運算符也會轉換爲equals方法調用,編譯器會自動對返回值取反。

2. 排序運算符:compareTo

在java中,類能夠實現Comparable接口,接口中定義的compareTo方法用於肯定一個對象是否大於另外一個對象。可是在java中,只有基本數據類型可使用< 和 > 來比較,其它類型都須要element1.compareTo(element2)。

而在kotlin中,可使用比較運算符(< 、> 、<=、>=),會被轉換爲compareTo,compareTo的返回類型必須爲Int。

定義Person類實現compareTo方法:先比較firstName,若是相同,再比較lastName

class Person(
    val firstName: String, val lastName: String
) : Comparable<Person> {
    override fun compareTo(other: Person): Int {
        return compareValuesBy( // 按順序調用給定的方法,並比較它們的值
            this, other,
            Person::firstName, Person::lastName
        )
    }
}

val p1 = Person("a", "b");
val p2 = Person("a", "c");
println(p1 < p2)
//輸出結果>>> true

可使用kotlin標準庫中的compareValuesBy函數來簡潔地實現compareTo方法。全部java中實現了Comparable接口的類,均可以在kotlin使用簡潔的運算符語法,不用再增長擴展函數。如:

println("abc" < "cba")
//輸出結果>>> true

三. 集合與區間的約定

集合的操做一般都是經過下標。kotlin中全部這些操做都支持運算符語法:經過下標獲取或者設置元素,可使用語法a[b](稱爲下標運算符);可使用in運算符來檢查元素是否在集合區間內,也能夠迭代集合。

1. 經過下標來訪問元素:「get」和「set」

kotlin中,訪問map中元素,能夠經過方括號的方式:

val value = map[key]

也能夠用一樣的運算符來改變一個可變map的元素

mutable[key] = newValue

如何工做的呢?
在kotlin中,下標運算符是一個約定。使用下標運算符讀取元素會被轉換爲get運算符方法的調用,寫入元素調用set。Map和MutableMap的接口都已經定義了這些方法。

如何給自定義的類添加相似的方法呢?

實現get約定:仍是以自定義Point類爲例,使用方括號來引用點的座標,p[0]訪問X座標,p[1]訪問Y座標

operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

val  p = Point(10, 20)
println(p[1])
//輸出結果>>> 20

只須要定義一個get函數,並標記operator後,p[1]就會被轉換爲get方法的調用。

注意:get的參數能夠是任意類型,而不僅是Int。還能夠定義具備多個參數的get方法。若是須要使用不一樣的健類型訪問集合,也可使用不一樣的參數類型定義多個重載的get方法。

實現set約定:上例中Point類是不可變的(變量是val修改),因此實現set約定沒有意義。
接下來定義一個可變的點MutablePoint

data class MutablePoint(var x: Int, var y: Int)

operator fun MutablePoint.set(index: Int, value: Int) {
    when(index) {
        0 -> x = value
        1 -> y = value
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

val p = MutablePoint(10, 23)
p[1] = 24
println(p)
//輸出結果>>> MutablePoint(x=10, y=24)

只須要定義一個set函數,並標記operator後,p[1]=24就會被轉換爲set方法的調用。

2. "in"的約定

集合支持的另外一個運算符是in運算符:用來檢查某個對象是否屬於集合,對於的函數是contains。

實現in的約定:檢查點是否屬於一個矩形

data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    // 使用until函數來構建一個區間
    return p.x in upperLeft.x until lowerRight.x &&
            p.y in upperLeft.y until lowerRight.y
}

val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect)
//輸出結果>>> true

in右邊的對象將會調用contains函數,in左邊的對象將會做爲函數入參。

3. rangeTo的約定

建立一個區間,使用 .. 語法。如:1..10表示從1到10的數字。 ..運算符是調用rangeTo函數的一個簡潔方法。

rangeTo函數返回一個區間。能夠爲自定義的類定義這個運算符,可是若是該類實現了Comparable接口,就不須要了。能夠經過kotlin標準庫建立一個任意可比較元素的區間:

operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>

例如:

val now = LocalDate.now();
val vacation = now..now.plusDays(10) // 建立一個從今天開始的10天的區間
println(now.plusWeeks(1) in vacation) // 檢測一個特定的日期是否屬於這個區間
//輸出結果>>> true

now..now.plusDays(10)會被編譯器轉換爲now.rangeTo(now.plusDays(10))。其中rangeTo並非LocalDate的成員函數,而是Comparable的一個擴展函數。

rangeTo運算符的優先級低於算術運算符,最好把參數擴起來以避免混淆:

val n = 9
println(1..(n + 1)) // 能夠寫成1..n + 1,但括起來更清晰一點
//輸出結果>>> 1..10

表達式1..n.forEach { print(it) }不會被編譯,必須把區間表達式括起來才能調用forEach方法

val n = 9
(1..n).forEach { print(it) }
//輸出結果>>> 123456789

4. 在"for"循環中使用"iterator"的約定

在kotlin中,for循環中也可使用in運算符,和作區間檢查同樣。可是在這種狀況下它的含義是不一樣的:它被用來執行迭代。如:for(x in list) {...} 將被轉換成list.iterator()的調用。

在kotlin中,iterator方法能夠被定義爲擴展函數,因此能夠遍歷一個常規的java字符串,標準庫已經爲CharSequence定義了一個擴展函數iterator

operator fun CharSequence.iterator(): CharIterator

for(c in "abc"){}

能夠爲自定義的類定義iterator方法:實現日期區間的迭代器

operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
    // 這個對象實現了遍歷LocalDate元素的Iterator
    object : Iterator<LocalDate> {
        var current = start

        // 日期用到了compareTo約定
        override fun hasNext() =
            current <= endInclusive

        // 在修改前返回當前日期做爲結果
        override fun next() = current.apply {
            // 把當前日期增長一天
            current = plusDays(1)
        }
    }

val newYear = LocalDate.ofYearDay(2017, 1)
val daysOff = newYear.minusDays(1)..newYear

for (dayOff in daysOff) { println(dayOff) }
//輸出結果>>> 2016-12-31
//          2017-01-01

四. 解構聲明和組件函數

相信你們對數據類已經很熟悉了。

接下來解構聲明,它是怎麼工做的?

// 數據類
data class Point(val x: Int, val y: Int) 

val p = Point(10, 20)
val (x, y) = p  // 聲明變量x,y,而後用p的組件來初始化
println(x)
//輸出結果>>> 10

println(y)
//輸出結果>>> 20

解構聲明就像普通的變量聲明,但它在括號中有多個變量。

解構聲明也用到了約定原理。要在解構聲明中初始化每一個變量,將調用名爲componentN的函數,其中N是聲明中變量的位置。

對於數據類,編譯器爲每一個在主構造方法中聲明的屬性生成一個componentN函數。

咱們也能夠手動爲非數據類型聲明這些功能:

class Point(val x: Int, val y: Int) {
    operator fun component1() = x;
    operator fun component2() = y;
}

講這麼多,那解構聲明有哪些使用場景呢?
解構聲明主要使用場景之一:是從一個函數返回多個值,能夠定義一個數據類來保存返回所需的值,並將它做爲函數的返回類型。而後用解構聲明的方式,就能夠輕鬆的展開它,使用其中的值。

舉一個例子:將文件名分割成文件名和擴展名

// 聲明一個數據類來持有值
data class NameComponents(
    val name: String,
    val extension: String
)

fun splitFilename(fullName: String): NameComponents {
    val result = fullName.split(".", limit = 2)
    // 返回一個數據類型的實例
    return NameComponents(result[0], result[1])
}

val (name, ext) = splitFilename("example.kt")
println(name)
//輸出結果>>> example

println(ext)
//輸出結果>>> kt

componentN函數在數組和集合中也有定義。當已知大小的集合時,可使用解構聲明來處理集合。
改造一下splitFilename函數:

fun splitFilename(fullName: String): NameComponents {
    val (name, ext) = fullName.split(".", limit = 2)
    return NameComponents(name, ext)
}

componentN在標準庫只容許使用此語法來訪問一個對象的前五個元素。

接收一個函數返回多個值,可使用標準庫中的 PairTriple 類。

1. 解構聲明和循環

解構聲明不只能夠用做函數中的頂層語句,還能夠在其它能夠聲明變量的地方,如:in 循環

fun printEntries(map: Map<String, String>) {
    // 在in 循環中用解構聲明
    for ((key, value) in map) {
        println("$key -> $value")
    }
}

val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
printEntries(map)
//輸出結果>>> Oracle -> Java
//          JetBrains -> Kotlin

其中Map.Entry上擴展函數component1和component2,分別返回它們的健和值

for (entry in map.entries) {
    val key = entry.component1()
    val value = entry.component2()
    // ...
}

五. 重用屬性訪問的邏輯:委託屬性

1. 委託屬性的基本操做

委託屬性的基本語法:

class Foo {
  var p: Type by Delegate()
}

屬性p將它的訪問器邏輯委託給了另外一個對象,這裏是Delegate類的一個新的實例。經過關鍵字by對其後的表達式求值來獲取這個對象。

編譯器建立一個隱藏的輔助屬性,並使用委託對象的實例進行初始化,初始化屬性p會委託給實例

class Foo {
   // 編譯器會自動生成一個輔助屬性
   private val delegate = Detegate()
   // p的訪問都會調用對應的delegate的getValue和setValue
   var p: Type
      set(value: Type) = delegate.setValue(...,value)
      get() = delegate.getValue(...)
}

Detegate類必須具備setValue和getValue方法,能夠是成員函數,也能夠是擴展函數。

class Detegate {
   // getValue包含了實現getter的邏輯
   operator fun getValue(...) {...}
   // setValue包含了實現setter的邏輯
   operator fun setValue(..., value: Type) {...}
}

class Foo {
  // 關鍵字by把屬性關聯上委託對象
  var p: Type by Delegate()
}

val foo = Foo()
val oldValue = foo.p // 經過調用delegate.getValue(...)來實現屬性的修改
foo.p = newValue // 經過調用delegate.setValue(..., newValue)來實現屬性的修改

2. 使用委託屬性:惰性初始化和「by lazy()「

惰性初始化:當第一次訪問該屬性的時候,才根據須要建立對象的一部分。

例如:一個Person類,用來訪問一我的寫的郵件列表。郵件存儲在數據庫中,訪問耗時。可是隻但願在首次訪問時才加載郵件,並只執行一次

class Person {
    // _emails屬性用來保存數據,關聯委託
    private var _emails: List<String>? = null

    val emails: List<String>
        get() {
            if (_emails == null) {
                // 訪問時加載郵件
                _emails = loadEmails();
            }
            // 若是已經加載,直接返回
            return _emails!!
        }

    private fun loadEmails(): List<String>? {
        // 耗時
        return listOf("1", "2");
    }
}

val p = Person()
println(p.emails)
//輸出結果>>> [1, 2]

若是有幾個屬性怎麼辦呢?且這個實現也不是線程安全的。kotlin提供了更好的解決方案:
使用委託屬性會讓代碼變得簡單,能夠封裝用於存儲值的支持屬性和確保該值只被初始化一次的邏輯。
可使用標準庫函數lazy返回委託

使用委託屬性來實現惰性初始化:

class Person {
    val emails by lazy { loadEmails() }
}

lazy函數返回一個對象,該對象具備一個名爲getValue且簽名正確的方法,所以能夠把它與by關鍵字一塊兒使用來建立一個委託屬性。默認狀況下,lazy函數是線程安全的。

3. 實現委託屬性

在java中當一個對象的屬性發生更改時通知監聽器,具備用於此類通知的標準機制:PropertyChangeSupport和PropertyChangeEvent類。可是在kotlin不使用屬性委託,怎麼實現的呢?

PropertyChangeSupport類維護了一個監聽器列表,並向它們發送PropertyChangeEvent事件。要使用它,一般須要把這個類的一個實例存儲爲bean類的一個字段,並將屬性更改的處理委託給它。

爲了不在每一個類中都建立這個字段,建立一個工具類,而後bean類繼承這個工具類。

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this);

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

寫一個Person類,定一個只讀屬性name和一個可寫屬性age,當age發生改變時,通知它的監聽器

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    var age: Int = age
        set(newValue) {
            // field標識符容許訪問屬性背後支持字段
            val oldValue = field
            field = newValue
            // 當屬性變化時,通知監聽器
            changeSupport.firePropertyChange("age", oldValue, newValue)
        }
}

val p = Person("kerwin", 30)
p.addPropertyChangeListener(PropertyChangeListener { event ->
    println("Property ${event.propertyName} change from ${event.oldValue} to ${event.newValue}")
})

p.age = 31;
//輸出結果>>> Property age change from 30 to 31

接下來經過輔助類實現屬性變化的通知

class ObservableProperty(
    val propertyName: String,
    var propertyValue: Int,
    val changeSupport: PropertyChangeSupport
) {
    fun getValue() = propertyValue
    fun setValue(newValue: Int) {
        val oldValue = propertyValue
        propertyValue = newValue
        changeSupport.firePropertyChange(propertyName, oldValue, newValue)
    }
}

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    val _age = ObservableProperty("age", age, changeSupport)
    var age: Int
        get() = _age.getValue()
        set(newValue) = _age.setValue(newValue)
}

這樣咱們仍是須要爲每一個屬性建立ObservableProperty實例,並把setter和getter委託給它。kotlin中的委託功能不用這樣寫,可是須要更改下ObservableProperty方法的簽名,匹配kotlin約定所需的方法

class ObservableProperty(
    var propertyValue: Int,
    val changeSupport: PropertyChangeSupport
) {
    operator fun getValue(p: Person, prop: KProperty<*>) = propertyValue
    operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
        val oldValue = propertyValue
        propertyValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}

ObservableProperty這個類作了更改的地方:

  • getValue和setValue函數都被標記了operator
  • 這些函數增長了兩個參數:一個用於接收屬性的實例,用來設置或讀取屬性;另外一個用於表示屬性自己,這個屬性類型爲KProperty
  • 把propertyName屬性從主構造中移除

而後使用委託屬性來綁定更該通知:

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    var age: Int by ObservableProperty(age, changeSupport)
}

經過by關鍵字,kotlin編譯器會自動執行以前手動編寫的代碼。右邊的對象被稱爲委託。kotlin會自動將委託存儲在隱藏的屬性中,並在訪問或修改屬性時調用委託的getValue和setValue。

你不用手動去實現可觀察的屬性邏輯。kotlin標準庫中已經包含相似ObservableProperty的類。標準庫與PropertyChangeSupport類沒有耦合。

使用Delegates.observable來實現屬性修改的通知:

class Person(
    val name: String,
    age: Int
) : PropertyChangeAware() {

    private val observer = { property: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(property.name, oldValue, newValue)
    }
    var age: Int by Delegates.observable(age, observer)
}

by右邊的表達式不必定是新建立的實例。也能夠是函數調用,另外一個屬性或者任何其它表達式。只要這個表達式的值,是可以被編譯器用正確的參數類型來調用getValue和setValue的對象。

4. 委託屬性的變化規則

接下來總結一下委託屬性是怎麼工做的?
假設有一個委託屬性的類:

class C {
  val prop: Type by MyDelegate()
}

MyDelegate實例會被保存到一個隱藏的屬性中,它被稱爲<delegate>。編譯器也將用一個KProperty類型的對象來表明這個屬性,它被稱爲<property>

class C {
  private val <delegate> = MyDelegate()

  val prop: Type
     get() = <delegate>.getValue(this, <property>)
     set(value: Type) = <delegate>.setValue(this, <property>, value)
}

5. 在map中保存屬性值

委託屬性另外一種常見用法,是用在有動態定義的屬性集的對象中,這種對象有時被稱爲自訂對象。

舉一個例子:定義一個屬性,把值存到map中

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, arrtValue: String) {
        _attributes[attrName] = arrtValue
    }

    val name: String
        get() = _attributes["name"]!!  // 從map中手動檢索屬性
}

那麼把它修改成委託屬性很是簡單,能夠直接將map放在by關鍵字後面

class Person {
    private val _attributes = hashMapOf<String, String>()

    fun setAttribute(attrName: String, arrtValue: String) {
        _attributes[attrName] = arrtValue
    }

    //  將map做爲委託屬性
    val name: String by _attributes
}

由於標準庫已經在標準Map和MutableMap接口上定義了getValue和setValue擴展函數。

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索