【基础篇】Kotlin第六讲-委托类和属性

委托类

实现一个接口,可以使用by关键字将接口实现委托给另一个对象。

interface OnClickListener{
    fun onClick()

    fun onLongClick()
}

class ViewClickDelegate : OnClickListener{

    override fun onClick(){
        println("ViewClickDelegate onClick")
    }

    override fun onLongClick() {
        println("ViewClickDelegate onLongClick")
    }
}

class View(val name: String, onClickListener: OnClickListener) : OnClickListener by onClickListener{

    override fun onLongClick() {
        println("$name onLongClick")
    }
}

类委托后我们依然可以通过重写的方式来覆盖委托类的实现,这里View实现onLongClick方法,覆盖重写了ViewClickDelegate类里的onLongClick方法。

类委托的本质是:把抽象方法的实现交给了by后的委托对象

延迟初始化和委托属性

延迟初始化属性

不在对象创建的时候初始化,而是在第一次使用时初始化。完成后像普通属性一样使用

open class Food(val name: String) {

    override fun toString(): String {
        return "[$name]"
    }
}

class Container(val name: String) {

    lateinit var foodList: List<Food>

}

惰性初始化属性

第一次使用该属性时才初始化,且只初始化一次。用旗号标示是否初始化过,旗号有多种选择和实现方式。

在代码定义处执行初始化,有助于代码维护。

对指令式语言,这个模式可能潜藏着危险,尤其是使用共享状态的程式习惯。

普通实现

class Container2(val name: String) {
    private var _foodList: List<Food>? = null
    val foodList: List<Food>
        get() {
            if (_foodList == null) {
                _foodList = arrayListOf(Food("米糊"))
            }
            return _foodList!!
        }
}

by lazy(){}实现惰性初始化

class Container4(val name: String) {
    val food: Food by lazy{
        Food("米糊")
    }
}

//指定锁
class Container5(val name: String) {
    val food: Food by lazy(Container5::class){
        Food("米糊")
    }
}

//默认 线程安全 SYNCHRONIZED
//PUBLICATION,同步锁不是必需的,允许多个线程同时执行
class Container6(val name: String) {
    val food: Food by lazy(LazyThreadSafetyMode.SYNCHRONIZED){
        Food("米糊")
    }
}

在JavaBean的设计中,按照属性的不同作用又细分为四类:单值属性,索引属性;关联属性,限制属性。接下来看下Kotlin如何实现关联属性和限制属性的

关联属性(可观察属性)

通过PropertyChangeSupport代码实现属性监听

class Shelf(val name: String, _book: Book) {

    private val propertyChange: PropertyChangeSupport = PropertyChangeSupport(this)
    var book: Book = _book
        set(value) {
            val oldBook = field
            field = value
            propertyChange.firePropertyChange("book", oldBook, value)
        }


    fun addBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        propertyChange.addPropertyChangeListener("book", propertyChangeListener)
    }

    fun removeBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        propertyChange.removePropertyChangeListener("book", propertyChangeListener)
    }
}

把逻辑封装,抽取出基类

open class BasePropertyChange {

    val propertyChange = PropertyChangeSupport(this)

    protected fun addChangeListener(key: String, propertyChangeListener: PropertyChangeListener) {
        propertyChange.addPropertyChangeListener(key, propertyChangeListener)
    }

    protected fun removeChangeListener(key: String, propertyChangeListener: PropertyChangeListener) {
        propertyChange.removePropertyChangeListener(key, propertyChangeListener)
    }
}

class Shelf_2(val name: String, _book: Book) : BasePropertyChange() {

    var book: Book = _book
        set(value) {
            val oldBook = field
            field = value
            propertyChange.firePropertyChange("book", oldBook, value)
        }

    fun addBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        addChangeListener("book", propertyChangeListener)
    }

    fun removeBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        removeChangeListener("book", propertyChangeListener)
    }
}

把book里的set访问器的逻辑封装成一个类

class BookDelegate(_book: Book, val propertyChange: PropertyChangeSupport) {

    var field: Book = _book

    fun getValue(): Book = field

    fun setValue(value: Book) {
        val oldBook = field
        field = value
        propertyChange.firePropertyChange("book", oldBook, value)
    }
}

class Shelf2(val name: String, _book: Book) : BasePropertyChange() {

    val _bookDelegate: BookDelegate = BookDelegate(_book, propertyChange)
    var book: Book
        set(value) {
            _bookDelegate.setValue(value)
        }
        get() = _bookDelegate.getValue()

    fun addBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        addChangeListener("book", propertyChangeListener)
    }

    fun removeBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        removeChangeListener("book", propertyChangeListener)
    }
}

至此,我们用Kotlin手工实现了可观察属性变化的功能,测试下

fun testObserverField() {
    val shelf = Shelf2("书架", Book("Think in java"))
    shelf.addBookChangeListener(object : PropertyChangeListener {
        override fun propertyChange(evt: PropertyChangeEvent?) {
            val oldBook = evt?.oldValue as Book
            val newBook = evt.newValue as Book

            println("old book = $oldBook , new book = $newBook")
        }
    })

    shelf.book = Book("Kotlin in action")
}

运行上述代码结果如下:

old book = Book(name=Think in java) , new book = Book(name=Kotlin in action)

使用Kotlin委托实现

Kotlin的委托属性在语言层面提供了在属性的读访问器里调用委托类里operator修饰的两参数getValue方法,属性写访问器调用operator修饰setValue三个参数方法

class BookDelegate2(_book: Book, val propertyChange: PropertyChangeSupport) {

    var field: Book = _book

    operator fun getValue(shelf3: Shelf3, prop: KProperty<*>): Book = field

    operator fun setValue(shelf3: Shelf3, prop: KProperty<*>, newValue: Book) {
        val oldBook = field
        field = newValue
        propertyChange.firePropertyChange("book", oldBook, newValue)
    }
}

class Shelf3(val name: String, _book: Book) : BasePropertyChange() {

    var book: Book by BookDelegate2(_book, propertyChange)

    fun addBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        addChangeListener("book", propertyChangeListener)
    }

    fun removeBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        removeChangeListener("book", propertyChangeListener)
    }
}

通常借助ReadWriteProperty接口能方便我们实现委托

class BookDelegate3(var field: Book, val propertyChange: PropertyChangeSupport) : ReadWriteProperty<Shelf3_1, Book> {

    override fun getValue(thisRef: Shelf3_1, property: KProperty<*>): Book {
        return field
    }

    override fun setValue(thisRef: Shelf3_1, property: KProperty<*>, value: Book) {
        val oldBook = field
        field = value
        propertyChange.firePropertyChange("book", oldBook, value)
    }
}

class Shelf3_1(val name: String, _book: Book) : BasePropertyChange() {

    var book: Book by BookDelegate3(_book, propertyChange)

    fun addBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        addChangeListener("book", propertyChangeListener)
    }

    fun removeBookChangeListener(propertyChangeListener: PropertyChangeListener) {
        removeChangeListener("book", propertyChangeListener)
    }
}

测试上述代码:

fun testDelegateFieldForKotlin() {
    val shelf = Shelf3_1("书架", Book("Think in java"))

    shelf.addBookChangeListener(object : PropertyChangeListener {
        override fun propertyChange(evt: PropertyChangeEvent?) {
            val oldBook = evt?.oldValue as Book
            val newBook = evt?.newValue as Book

            println("Kotlin委托 old book is $oldBook, and new book is $newBook")
        }
    })

    shelf.book = Book("Kotlin in action!")
}

运行结果如下:

Kotlin委托 old book is Book(name=Think in java), and new book is Book(name=Kotlin in action!)

委托属性的本质:把属性访问器的实现交给了by后的委托对象

使用Kotlin的自带的实现可观察属性

其实,Delegate.observable()类实现了上面提到的所有逻辑了。

我们看下Delegate.observable方法的源码

public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
        ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
            override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
        }

该方法返回ObservableProperty对象,看下ObservableProperty对象源码

public abstract class ObservableProperty<T>(initialValue: T) : ReadWriteProperty<Any?, T> {
    private var value = initialValue

    protected open fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = true

    protected open fun afterChange (property: KProperty<*>, oldValue: T, newValue: T): Unit {}

    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return value
    }

    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        val oldValue = this.value
        if (!beforeChange(property, oldValue, value)) {
            return
        }
        this.value = value
        afterChange(property, oldValue, value)
    }
}

该对象有getValue和setValue方法,这和我们自己实现的BookDelegate3类里的getValue和setValue方法逻辑几乎相同。不同之处是,官方还多了beforeChange()控制,和afterChange()供实现类覆盖重写。

Kotlin标准库已经提供了可观察属性的属性委托实现了

class Shelf4(val name: String, _book: Book) {

    var book: Book by Delegates.observable(_book, {property, oldValue, newValue ->
        println("The old book's name is \"${oldValue.name}\", and the new book's name is \"${newValue.name}\"")
    })
}

测试下上述的代码

fun testObserverFieldForKotlin(){
    val shelf = Shelf4("书架", Book("think in java"))
    shelf.book = Book("Kotlin in action")
}

运行结果如下

The old book's name is "think in java", and the new book's name is "Kotlin in action"

限制属性

Kotlin也为我们提供了现成的委托类来实现限制属性

class Shelf5(val name: String, val book: Book ,_year: Int) {
    var year: Int by Delegates.vetoable(_year, {property, oldValue, newValue ->
        newValue <= 99
    })
}

测试上述代码

fun testVetoableFieldForKotlin(){
    val shelf = Shelf5("书架", Book("think in java"), 0)
    shelf.year = 200
    println("current book is ${shelf.year}")

    shelf.year = 20
    println("current book is ${shelf.year}")
}

运行结果如下:

current book is 0
current book is 20

注意:
上述用的是成员函数,事实上,扩展函数也能实现委托属性

使用Map实现委托属性

MapAccessors.kt文件里,有如下扩展函数源码

@kotlin.jvm.JvmName("getVarContravariant")
@kotlin.internal.LowPriorityInOverloadResolution
@kotlin.internal.InlineOnly
public inline fun <V> MutableMap<in String, in V>.getValue(thisRef: Any?, property: KProperty<*>): V
        = @Suppress("UNCHECKED_CAST") (getOrImplicitDefault(property.name) as V)


@kotlin.internal.InlineOnly
public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) {
    this.put(property.name, value)
}

由此可见, MutableMap存在getValue方法和setValue方法,那么就可以用于委托,事实上,也确实如此。

举个例子:

class Fruit(name: String) : Food(name){

    private val attributeMap = HashMap<String, String>()

    val color: String by attributeMap

    val size: String by attributeMap

    fun setAttributeMap(name: String, value: String){
        attributeMap.put(name, value)
    }
}

fun testDelegateMap(){
    val fruit = Fruit("西瓜")

    fruit.setAttributeMap("color", "绿色")
    fruit.setAttributeMap("size", "2kg")

    println("color = ${fruit.color}, size = ${fruit.size}")
}

运行结果

color = 绿色, size = 2kg

小结

类委托的本质是:把抽象方法的实现交给了by后的委托对象

属性委托的本质是:把属性访问器的实现交给了by后的委托对象

扩展函数也能实现属性委托

参考资料

维基百科:惰性初始化模式

维基百科:惰性求值

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容