侧边栏壁纸
博主头像
Z同学博主等级

工作磨平激情前,坚持技术的热忱。 欢迎光临Z同学的技术小站。 分享最新的互联网知识。

  • 累计撰写 274 篇文章
  • 累计创建 55 个标签
  • 累计收到 74 条评论

Kotlin学习笔记-对象的表达与委托

Z同学
2021-07-18 / 0 评论 / 0 点赞 / 507 阅读 / 7,830 字
温馨提示:
本文最后更新于 2021-12-03,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

对象表达式

介绍:Kotlin之中用对象表达式和对象声明来实现创建一个对于某个类做了轻微改变的类的对象。并且不用声明一个新的子类。

扩展函数也是针对对象做轻微改变,并且不用声明一个新的继承关系。

实例1:

open class A(x: Int) {
    public open val y: Int = x
}

interface B {

}

//可以在对象表达式时指定对象的集成接口或者父类。
val ba: A = object : A(1), B {
    override val y = 12
}

结合上面的例子,我们能理解。可以通过对象表达式直接越过类的定义,直接得到一个新的对象。

如果父类有一个构造函数,则必须传递参数给他。多个超类型和接口,可以用,逗号分割。

这个和我们写类的继承与接口继承的时候语法是一样的。

实例2:

fun main(arr: Array<String>) {
    val site = object {
        var name: String = "Zinyan"
        var url: String = "zinyan.com"
    }
    println(site.name)
    println(site.url)
}

输出:

Zinyan
zinyan.com

我们可以看到。这些例子都使用了关键字:object

注意:匿名对象可以用作只在本地和私有作用域中声明的类型。

如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或者属性的实际类型会是匿名对象类的超类。如果没有任何声明,那么就是Any。在匿名对象中添加的成员将无法访问。

切合代码,我们来理解一下上面的意思。

实例3:

class C {
    //私有函数, 调用它返回的类型就是匿名对象类型
    private fun test() = object {
        val x: String = "test-->x"
    }

    //公有函数,调用它返回的类型就是Any对象。
    fun test2() = object {
        val x: String = "test2-->X"
    }
    
    fun outValue(){
        val x1= test().x //可以写,能够拿到对象
        val x2= test2().x  // 不可以这么写,因为test2()拿不到对象,
    }

}

在对象表达式中可以方便的访问到作用域中的其他变量

实例4:

fun countClicks(windos:JComponent){
    var clickCount=0
    var enterCount=0
    windos.addMouseListener(object:MouseAdapter(){
        override fun mouseClicked(e: MouseEvent?) {
            super.mouseClicked(e)
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent?) {
            super.mouseEntered(e)
            enterCount++
        }
    })
}

对象声明(关键字:object)

介绍:Kotlin之中使用object来声明一个对象,我们在前面介绍对象表达式时,使用过object关键字。

我们可以通过object关键字很方便的获取一个单例

实例1: 创建了一个单例对象。

object TestManager {
    var name: String = ""
    fun test1() {

    }

    fun test2() {

    }
}

fun main(arr: Array<String>) {
    var tt = TestManager
    var t2 = TestManager
    tt.name = "Z同学"
    t2.name = "zinyan.com"
    println(tt.name)
    println(t2.name)
}

输出:

zinyan.com
zinyan.com

同时,使用object创建单例对象的时候也可以进行继承。

实例2:

object Zinyan : C() {
    override fun test2() {
        
    }
}

相较于对象表达式,当对象声明在另一个类的内部时,这个对象并不能通过外部类的实例访问到该对象,而只能通过类名来访问,同时该对象也不能直接访问到外部类的方法和变量。

切合下面的代码可以直观理解这段话表达的意义。

实例3:

class  Site{
    val  name ="Z同学"
    object DeskTop{
        var url ="zinyan.com"
        fun showUrl(){
            //这种写法错误,因为声明的对象不能访问外部类的方法和变量
            print("desk legs $name") 
        }
    }
}

fun main(arr: Array<String>) {
    var ss = Site()
    ss.DeskTop.url// 错误写法, 不能通过实例对象访问 类内部声明的对象。
    Site.DeskTop.url //正确写法,所以其实这个类就是一个单例对象了。
}

伴生对象(关键字:commpanion)

介绍:类内部的对象声明可以用commpanion关键字修饰。添加了这个关键字修饰的对象,它将与外部类关联在一起。我们可以直接通过外部类访问到对象的内部元素了。

这种关系我们叫做,类的伴生对象。

实例1:

class D {
    companion object Test {
        fun create(): D = D()
    }
}

fun main(arr: Array<String>) {
    var ss = D.create() // 可以访问到对象的内部元素了
}

注意:一个类里面只能声明一个内部关联对象。也就是说关键字companion 只能在class中使用一次。

伴生对象除了是某个类以外,还可以是其他的。例如接口对象的实例作为伴生对象。

实例2:

/创建一个接口,该接口传参T泛型
interface Factoory<T> {
    fun create(): T
}

class E {
    companion object : Factoory<E> {
        override fun create(): E = E()
    }
}

实例2的写法很像对象表达式。

对象表达式和对象声明之间的语义差异:

  • 对象表达式是在使用他们的地方立即执行的。
  • 对象声明是在第一次被访问到时延迟初始化的。
  • 伴生对象的初始化是在外部类被加载(解析)时,与Java静态初始化器的语义相匹配。

委托(关键字:by)

介绍:Kotlin直接支持委托模式,并直接提供了关键字by来实现。

什么是委托模式?

委托模式是软件设计模式的一种。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。

类委托

介绍:即一个类中定义的方法,实际是调用另外一个类的方法来实现的。

实例1:

//创建接口
interface Base {
    fun test()
}

//实现接口
class BaseImpl(val x: String) : Base {
    override fun test() {
        println(x)
    }
}

//通过关键字by 建立委托类
class Derived(b: Base) : Base by b

fun main(arr: Array<String>) {
    val tt = BaseImpl("这是一个委托类示例")
    Derived(tt).test()
}

输出:

这是一个委托类示例

解释: 在上面的Derived声明中by子句表示:将Base委托给b实例对象。编译器会在编译时自动将接口Base的所有方法调用转发给b。

其实就是将例子中的tt的实例对象全部委托给Derived。她可以直接调动Base的实例方法。

属性委托

介绍:属性委托是指一个类的某个属性值不是在类中直接定义。而是将其委托给一个代理类,从而实现该类的属性统一管理。

语法: val/var <属性名>:<类型> by <表达式>

表达式就是委托主体了,属性的get方法和set方法将会被委托给这个对象的getValues()setValue()方法。

属性委托不必实现任何接口,但是必须提供getValue()函数,而对于标注未来var的属性,还要实现setValue()函数

实例1:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef,这里委托了${property.name}属性"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$thisRef 的${property.name}属性赋值为$value")
    }
}

//定义一个属性委托的类
class Example {
    //属性委托
    var p: String by Delegate()
}

fun main(arr: Array<String>) {
    val e = Example()
    println(e.p)
    e.p = "Z同学的小站"
    println(e.p)
}

输出:

cn.zinyan.demo.kotlin.Example@21b8d17c,这里委托了p属性
cn.zinyan.demo.kotlin.Example@21b8d17c 的p属性赋值为Z同学的小站
cn.zinyan.demo.kotlin.Example@21b8d17c,这里委托了p属性

如果对于委托还比较迷茫,没有关系。我们可以参考Kotlin之中的内置的委托类来进行理解。

标准委托

介绍:Kotlin的标准库之中已经内置了很多工厂方法来实现属性的委托。

延迟属性 Lazy

介绍lazy是一个函数,接受一个Lambda表达式作为参数,返回一个Lazy<T>实例的函数,返回的实例可以作为实现延迟属性的委托,第一次调用get()会执行已传递给lazy()de lamda表达式并记录结果。后续调用get()只是返回记录的结果。

实例:

val lazyValue: String by lazy {
    println("computed!") //第一次调用输出,第二次调用不执行
    "Hello"
}

fun main(arr: Array<String>) {
    println(lazyValue)
    println("第二次调用")
    println(lazyValue)
}

输出:

computed!
Hello
第二次调用
Hello

可观察属性 Observable

介绍:observable可以用于实现观察者模式。

Delegates.observable()函数接受两个参数:第一个是初始化值,第二个是属性值变化事件的响应器(handler),在属性赋值后会执行事件的响应器,响应器有三个参数:被赋值的属性,旧值,新值。

结合实例代码,可以方便我们理解

实例:

class User {
    var name: String by Delegates.observable("初始化值") { property, oldValue, newValue ->
        println("旧值: $oldValue -->新值: $newValue")
    }
}

fun main(arr: Array<String>) {
    var ss = User()
    ss.name="第一次赋值"
    ss.name="第二次赋值"
}

输出:

旧值: 初始化值 -->新值: 第一次赋值
旧值: 第一次赋值 -->新值: 第二次赋值

属性值映射

介绍:将属性存储在一个映射中。

常见的用例是在一个映射(map)里面存储属性的值。

通常使用场景是解析JSON或者做其他'动态'事情的应用之中。

在这种需求驱动下,我们可以使用映射实例自身作为委托来实现属性委托。

实例:

class F(val map: Map<String, Any>) {
    val name: String by map
    val url: String by map
}

fun main(arr: Array<String>) {
    val f = F(
        mapOf(
            "name" to "Z同学的小站",
            "url" to "zinyan.com"
        )
    )
    println(f.name)
    println(f.url)
}

输出:

Z同学的小站
zinyan.com

Not Null

介绍:not null 适用于那些无法在初始化阶段就确定属性值的场合。

实例:

class Foo {
    var notNullBar: String by notNull<String>()
}

fun main(arr: Array<String>) {
    var f = Foo()
    f.notNullBar = "bar"
    println(f.notNullBar)
}

输出

bar

注意:如果属性值在被赋值之前就被访问的话,将会抛出异常。

局部委托属性

介绍:Kotlin之中可以将局部变量声明为委托属性。

可以使得一个局部变量惰性初始化

语法示例:

fun example(computerFoo:()->Foo){
    val memoizedFoo by lazy(computerFoo)
    if(someCondition && memoizedFoo.isValid()){
        memoizedFoo.doSomething()
    }
}

属性委托要求

对于只读属性(val),它的委托必须提供一个名为getValue()的函数。该函数接受以下参数:

  • thisRef :必须与属性所有者类型相同,或者是它的超类型。
  • property:必须是类型KProperty<*>或者其超类。

在上面的例子之中也有过相应代码。

翻译规则

在每个委托属性的实现背后,kotlin编译器都会生成辅助属性并委托给它。例如,对于属性prop,生成隐藏属性prop$delegate。而访问器的代码只是简单的委托给这个附件属性:

语法示例:

class H {
    var prop: Type by MyDelegate()
}
//下面是编译器编译后,自动生成的相应代码;
class H{
    private  val prop$delegate=MyDelegate()
    var prop:Type
        get() =prop$delegate.getValue(this,this::prop)
        set(valuee:Type)=prop@delegate.setValue(this,this::prop,value)    
}

Kotlin编译器在参数中提供了关于prop的所有必要信息:第一个阐述this引用到外部H的实例,而this::propKProperty类型的反射对象,该对象描述prop自身。

提供委托

介绍:通过定义provideDelegate操作符,可以扩展创建属性实现所有委托对象的逻辑。

如果 by右侧所使用的对象将provideDelegate定义为成员或者扩展函数,那么将会调用该函数来,创建属性委托实例。

例如:如果要在绑定之前,检测属性的名称。

实例1:

class ResourceID() {
    val image_id: String = "101"
    val text_id: String = "102"
}
class ResourceLoader(id: ResourceID) {
    val d: ResourceID = id
    //表明该函数返回的值是ReadOnlyProperty<MyUI, String>
    //在实例化MyUI的时候,立即做这里的检查
    operator fun provideDelegate( thisRef: MyUI, prop: KProperty<*>): ReadOnlyProperty<MyUI, String> {
        if(checkProperty(thisRef, prop.name)){
            //匹配则返回相应的委托
            return DellImpl(d)
        }else{
            //不匹配则抛出异常
            throw Exception("Error ${prop.name}")
        }
    }

    //检查名字是否匹配
    private fun checkProperty(thisRef: MyUI, name: String):Boolean {
        return name.equals("image") || name.equals("text")
    }
}

//ReadOnlyProperty<MyUI, String>泛型参数:第一个大概和thisRef一样,第二个参数是getValue()的返回值
class DellImpl(d: ResourceID) : ReadOnlyProperty<MyUI, String> {
    val id: ResourceID = d
    override fun getValue(thisRef: MyUI, property: KProperty<*>): String {
        //这里就是调用ui.image得到的值
        if(property.name.equals("image"))
            return property.name+"  "+id.image_id
        else
            return property.name+"  "+id.text_id
    }
}

fun  bindResource(id: ResourceID): ResourceLoader {
    var res = ResourceLoader(id);
    return res
}

class MyUI {
    //这里的bindResource(ResourceID())最终会返回一个委托或者抛出异常
    val image : String by bindResource(ResourceID())
    val text : String by bindResource(ResourceID())
    //val webview by bindResource(ResourceID())
}
fun main(args: Array<String>) {
    try{
        var ui = MyUI()
        println(ui.image)
        println(ui.text)
    }catch(e: Exception) {
        println(e.message)
    }
}

输出:

image  101
text  102

委托这一块有些复杂。后续等学习更明白后,补充一些关于属性委托的实例代码吧。

附录
上面例子的Demo代码。
https://zinyan.com/upload/2021/07/Demo11-f008bcd65f85460ca94cada3d1241b2e.kt

0

评论区