Kotlin疑难概念汇总

 

(图片出处:Kotlin 语言入门宝典 | Android 开发者 FAQ Vol.5

该文章发首次发表于CSDN,其中引用了我在参考菜鸟教程学习时的一些感悟。

这不是一篇从头到尾系统学习Kotlin的文章,只适合那些在其它地方进行系统学习的过程中遇到困难的同学查询用,如果需要系统的从头到尾学习Kotlin可以参考文章最后的参考文献部分。

1、backing field

backing field可以说是Kotlin初学者遇到第一个难以理解的概念了,这个关键字用在一个属性的getter方法和setter方法中,要弄明白这个关键字是干什么用的就要先清楚Kotlin的运行机制,那就是:Kotlin对每个变量的引用都是调用这个变量的getter方法(哪怕在定义变量的类内部也是如此),对每个变量的赋值都是调用这个变量的setter方法(哪怕你只是用等于号给这个变量赋值),举个例子吧:

class MyClass {
    var myint: Int
        get() = field// 这里用到了field关键字
        set(value) {
            field = value
        }
}

以上写法是正确的写法,为什么getter方法不直接写成get() = myint,而setter方法不直接写成set(value) {myint = value}呢?为什么要用field这个关键字来代替myint呢?如果不用field会怎么样?比如下面这样:

class MyClass {
    var myint: Int
        get() = myint // 这里直接引用myint变量,相当于调用了myint变量的getter方法
        set(value) {
            myint = value // 这里直接使用myint = xxx,相当于调用了myint的setter方法
        }
}

看上面注释里的解释就明白了,在get()方法中出现了myint变量,相当于又调用了myint变量的get方法,这样就形成了递归调用,以上方法翻译成Java代码相当于:

public class MyClass {
    public int myint;

    public int getMyint() {
        return getMyint();
    }

    public void setMyint(int value) {
        setMyint(value)
    }
}

不用field关键字的Kotlin代码翻译成Java代码其实就十分直观了,这样的代码完全没有意义,虽然编译可以通过,但是运行起来就内存溢出了。

为什么Kotlin要设置这么不直观、这么奇怪的方式引用变量和给变量赋值呢?其实在Java中如果我们想在给一个变量赋值的同时做点其它工作,比如发一个通知告诉别的类我这个变量的值改变了,或者我这个变量被引用到了,这时这种机制就方便多了。

其实field关键字只是一个临时变量而已,我们完全可以不用field关键字,自己定义一个变量,一样可以,比如像下面这样:

class MyClass {
    var tempMyint: Int = 0 // 我们用这个变量代替backing field

    var myint: Int
        get() = tempMyint
        set(value) {
            tempMyint = value
        }

}

看到了吧,其实这样做也可以,但是如果变量比较多的话,我们就要为每个变量都设置一个临时中间变量,这样不太方便,而且容易被不明情况的其它同事乱改,容易出错,既然系统为每个变量都提供了一个field,我们拿来用就可以了,每一个变量都有一个属于它自己的field变量,一个属性对应的field变量的作用域仅限于这个属性的定义部分以及getter和setter方法。

最后,还有一个小细节需要注意一下,如果我们在自定义一个变量的getter方法和setter方法时又想同时给这个变量赋初值,则getter方法和setter方法中至少需要出现一次field关键字(哪怕我们没用到field关键字),比如下面这样:

class MyClass {
    var myint: Int = 0 // 这里给变量赋初值
        get() {
            field // 必须让field关键字出现一下,否则会提示变量没有field,无法赋初值
            return 0
        }
        set(value) {
            println("set value $value")
        }
}

我们在定义myint时给myint赋初值为0,但是如果你把get()方法中的field这一行去掉就会报错。

2、密封类

相信每一个从Java过来的同行都被这个问题困扰过,如果你看过其它的文章却依然是一头雾水,或许这里可以帮到你。密封类是一个Java中没有概念,那么首先要明确,密封类是什么?或者说它是干什么用的?那么首先我们就明确一下密封类的使用场合:密封类是一种专门用来配合when语句使用的类。或许这么表述不严谨,但是目前我只发现它的这个功能(我只是初学者-_-|||)。众所周知when语句就相当于Java中的swich-case语句,举个例子,假如在Android中我们有一个view,我们现在想通过when语句设置针对view进行两种操作:显示和隐藏,那么就可以这样做:

sealed class UiOp {
    object Show: UiOp()
    object Hide: UiOp()
}

fun execute(view: View, op: UiOp) = when (op) {
    UiOp.Show -> view.visibility = View.VISIBLE
    UiOp.Hide -> view.visibility = View.GONE
}

有同学可能要问了,这不是可以用枚举实现吗?你费这劲干嘛?确实,以上代码完全可以用枚举实现,比如在Java中我们可以这样写:

enum UiOp {
        Show, Hide
    }
    
private static void execute(View view, UiOp op) {
    switch (op) {
        case Show: {
            view.visibility = View.VISIBLE
            break;
        }
        case Hide: {
            view.visibility = View.GONE
            break;
        }
        default: {
            break;
        }
    }
}

用了枚举好像还更简单,但是如果我们现在想加两个操作:水平平移和纵向平移,并且还要携带一些数据,比如平移了多少距离,平移过程的动画类型等数据,用枚举显然就不太好办了,这时密封类的优势就可以发挥了,例如:

sealed class UiOp {
    object Show: UiOp()
    object Hide: UiOp()
    class TranslateX(val px: Float): UiOp()
    class TranslateY(val px: Float): UiOp()
}

fun execute(view: View, op: UiOp) = when (op) {
    UiOp.Show -> view.visibility = View.VISIBLE
    UiOp.Hide -> view.visibility = View.GONE
    is UiOp.TranslateX -> view.translationX = op.px // 这个when语句分支不仅告诉view要水平移动,还告诉view需要移动多少距离,这是枚举等Java传统思想不容易实现的
    is UiOp.TranslateY -> view.translationY = op.px
}

以上代码中,TranslateX是一个类,它可以携带多于一个的信息,比如除了告诉view需要水平平移之外,还可以告诉view平移多少像素,甚至还可以告诉view平移的动画类型等信息,我想这大概就是密封类出现的意义吧。

除此之外,大家可能注意到,如果when语句的分支不需要携带除“显示或隐藏view之外的其它信息”时(即只需要表明when语句分支,不需要携带额外数据时),用object关键字创建单例就可以了,并且此时when子句不需要使用is关键字。只有需要携带额外信息时才定义密封类的子类,而且使用了密封类就不需要使用else子句,每当我们多增加一个密封类的子类或单例,编译器就会在when语句中给出提示,可以在编译阶段就及时发现错误,这也是以往switch-case语句和枚举不具备的功能。

3、属性委托

(1)定义委托类委托属性的getter、setter方法

class MyClass {
    var str: String by Delegate()
}

class Delegate {
    var temp: String = "开始时没值" // 代理类中没有backing field,所以我们需要自己定义一个

    operator fun getValue(myClass: MyClass, property: KProperty<*>): String {
        println("$myClass, 获取${property.name}属性的值")
        // return myClass.str // 这里不能直接调用myClass.str,因为这个方法就相当于是str变量的getter方法,原理同前边提到的field关键字一样,直接引用myClass.str的getter方法,也就是现在所在的方法,进而引发无限递归,最后内存溢出
        return temp
    }

    operator fun setValue(myClass: MyClass, property: KProperty<*>, value: String) {
        println("$example 的 ${property.name} 属性赋值为 $value")
        temp = value
    }

这里我只说一点,委托类中不能使用field关键字,所以如果需要在委托类中返回该变量本来的值,或者引用该变量的值,那么就需要自己定义一个临时变量,这种做法跟我们之前用自己定义的临时中间变量代替field关键字一个原理。

(2)扩展委托类功能

我在参考Kotlin委托 | 菜鸟教程学习的时候遇到这个概念,当时不是很理解,后来自己写了一个简单的例子,大概理解了一点点,我理解作者大概想说的是通过这种方式的属性代理,可以在调用一个属性的getter方法之前做一些工作,而不是在getter方法内部做这些工作,代码分享出来大家可以参考一下:

class MyReadOnlyPropertyImpl : ReadOnlyProperty<MyTestClass, String> {
    override fun getValue(thisRef: MyTestClass, property: KProperty<*>): String {
        val s  = "aaa"
        return s;
    }
}

class MyProvider {
    operator fun provideDelegate(thisRef: MyTestClass, prop: KProperty<*>): ReadOnlyProperty<MyTestClass, String> {

        println("do something")// 这行代码是在getValue方法之外调用的

        val myReadOnlyPropertyImpl = MyReadOnlyPropertyImpl()// 这里才是调用getValue方法的地方
        return myReadOnlyPropetyImpl
    }
}


class MyTestClass {
    val myProvider = MyProvider()
    val myField1: String by myProvider
}

以上代码,println("do something")并没有放在myField1变量对应的getValue方法里边去调用,我理解是假如这段代码很长,并且跟属性的getter方法本身逻辑没什么联系,只是做为获取该属性的值的前提,那么就可以利用“提供委托”的方式扩展getter方法,在getter方法运行之前就做一些工作,其实以上代码还可以简化一下,由于MyReadOnlyPropertyImpl类只使用一次,我们可以用匿名类来代替,简化后的代码:

class MyProvider {
    operator fun provideDelegate(thisRef: MyTestClass, prop: KProperty<*>): ReadOnlyProperty<MyTestClass, String> {

        println("do something")// 这行代码是在getValue方法之外调用的

        return object: ReadOnlyProperty<MyTestClass, String> {
            // 这里才是调用getValue方法的地方
            override fun getValue(thisRef: MyTestClass, property: KProperty<*>): String {
                return "aaa"
            }
        }
    }
}


class MyTestClass {
    val myProvider = MyProvider()
    val myField1: String by myProvider
}

这样可以更直观看到println("do something")在getValue方法外边,这里说一下ReadOnlyProperty<R, T>接口需要的两个范型的意义:第一个范型R代表被委托的变量所在的类(ReadOnlyProperty需要知道去哪个类当中取这个变量来代理),第二个范型T代表变量本身的类型。最后来测试一下:

fun main(args: Array<String>) {

    val myTestClass = MyTestClass()
    println(myTestClass.myField1)

}

/* 输出:
do something
aaa
*/

4、Lambda表达式与Kotlin内联函数

据说Java 8也引入了Lambda表达式,但是我相信现在是2018年,目前来说如果有人看到这篇文章应该都是Android开发人员,并且大多数人应该还在使用Java 6、Java 7,对lambda表达式应该还不太熟悉,其实关于lambda表达式我也是刚刚接触,很担心自己讲不好,不如推荐一篇文章给大家,写的相当不错:Kotlin学习笔记(九)函数,Lambda表达式,看完这篇文章大家应该就会对lambda表达式有一个系统的理解了。

我理解lambda表达式其实就是一种可以用变量来表示的函数,既然变量可以当做函数的参数进行传递、变量可以当做函数的返回值,那么函数也可以(在Java中实现类似的功能要使用到接口,不如直接把一个函数当做参数传递给另外一个函数来的方便),并且搞清楚lambda表达式之后,Kotlin中的那些内联函数run、apply、let、also、with理解起来就容易多了,根本不需要到网上找他们那些文章,自己看函数定义原型就知道怎么用了,我用run方法的其中一种定义形式举个例子:

先看run方法的其中一种定义:

public inline fun <T, R> T.run(block: T.() -> R): R = block()

先来解释一下,run方法的这种定义形式允许一个T类型的对象调用它,这样的话在run方法内部就可以用this关键字引用这个T类型的对象,run方法的返回值是R类型。run方法的参数block本身又是一个函数(或者叫lambda表达式),这个函数没有参数(括号中为空),返回值也是R类型(跟run方法的返回值一样),然后run方法的方法返回值就赋值为block函数的返回值。

那么在实际使用时,由于run方法只有一个参数就是block,所以根据lambda表达式的规则,在调用run方法时可以省略run方法的圆括号,直接把lambda表达式的引用或内容放在run方法的方法体花括号内,例如:

fun main(args: Array<String>) {

    val mystr: String = "aaa"

    // 用一个String类型的对象mystr调用run方法
    val a = mystr.run {
        println(this) // this就代表调用run方法的字符串"aaa"
        1 // lambda表达式的返回值是Int类型,所在run方法的返回值也是Int类型
    }
    println(a)
}

/*
输出:
aaa
1
*/

先来说在这个例子中run方法声明中的范型T和R分别代表什么:这里由String类的mystr对象调用run方法,所以run方法中的范型T就代表String类型,lambda表达式的返回值(lambda表达式如果有返回值,则其内容中的最后一行代表返回值)是Int类型,所以R是Int类型,再由于从run方法的声明中来看lambda表达式和run方法的返回值相同,所以run方法的返回值也是Int类型。

再来看this关键字,由于是mystr调用了run方法,所以run方法和lambda表达式中的this关键字指的就是mystr变量。

其它的几个函数大家自己去看吧,我相信看懂了lambda表达式再看这几个函数应该能看懂。

5、写在最后

最后有几句话不知当讲不当讲,或许说出来会让很多人不舒服。最近在学习Kotlin的过程中遇到了很多自己不理解的问题,到网上一搜几乎全是一模一样的答案,都是互相抄来抄去,很没意思(或许有些是某些网站用爬虫自动爬过去的文章),其实也不是不让你抄,我觉得任何文章你在抄之前自己先读一读,自己理解了,觉得确实好了再抄,还可以加一些自己的理解,帮助更多的人,这并不是什么坏事,但是如果只是无脑的抄来抄去,人云亦去,只会给网上增加很多重复内容,那些真正能帮助到别人的文章反而被埋没了,到时候也是给自己挖坑。

这几天大多数问题的答案都是在StackOverFlow这样的网站或者一些翻译老外的文章中找到的,国内的也有,比如那篇讲lambda表达式的文章我就非常喜欢,很佩服这样的作者,脚踏实地的研究学术。

其实有时候我们总报怨国内技术不行,研发不出来自己的芯片。其实我觉得不是技术不行,而是我们非常缺乏老外那种脚踏实地、认真做事的态度。研究芯片不行,那么我们能不能认认真真把代码写好、架构写整齐,让别人维护起来更容易?我们能不能在抄别人文章的时候自己先读一读,读懂了再抄?比起研究芯片来说,这些并不难吧?

当我们做为程序员的时候能不能以一个认真的态度对待问题?写代码之前先想想怎样写才容易维护、才容易被别人读懂(也让自己容易维护,这样自己也可以少加点班,不要做一个纯屌丝);当我们做为团队负责人时能不能不要那么急功近利?一周发一个版?代码量超过一个月的工作?赶鸭子上架?没有任何架构、接口的设计?这样做的结果往往是代码质量极差,最后BUG满天飞,到最后改一个小小的功能都会引发大规模BUG(尤其是前期的代码交给逻辑思维不好的人来写),这样的项目不死都难啊。

最后,引用请标明出处:https://blog.csdn.net/aplixy/article/details/81296267

最后声明我也是个菜鸟,大家有什么想法也可以联系我,共同进步:

orgli@126.com

aplixy@gmail.com

 

参考文献:

Kotlin教程 | 菜鸟教程

Kotlin学习笔记(九)函数,Lambda表达式

Kotlin的密封(Sealed)类:超强的枚举(KAD 28)(翻译文章)

Sealed classes in Kotlin: enums with super-powers (KAD 28)

android - What's Kotlin Backing Field For? - Stack Overflow

Kotlin学习笔记(2):run、apply、let、also、with的用法和区别

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值