Kotlin笔记

Kotlin笔记

要理解Java与Kotlin的区别,就要从最根本的上来理解。Java是解释型语言(边解释成二进制文件,边执行),虽然Java有所谓的编译,但并不是如同C等编译型语言一样编译为二进制文件,而是编译成class文件,供JVM进行解释。而Kotlin之所以是基于Java的一种编程语言,是因为他跟Java都是将Code编译成class文件,供JVM进行解释。所以是不是可以将Java与Kotlin理解为同一人物的不同的皮肤呢?无非Java皮肤用起来手感差,看起来比较粗糙复杂,而Kotlin用起来手感细腻,看起来比较精致简洁。

继承与构造

继承

Kotlin中任何一个非抽象类默认都是不可被继承的,相当于Java中给类声明了final关键字。是因为遵循着这样的一条原则:如果一个类不是专门为继承而设计的,那么就应该主动将它加上final声明,禁止它可以被继承。加上open以声明可以被继承。

open class Person{
    ...
}

构造

Kotlin中构造函数分为主构造函数与次构造函数。

每个类都默认会有一个不带参数的主构造函数。主构造函数特点是没有函数体,直接定义在类名的后面即可。

class sutdent(val sno:String,val grade:Int):Person(){
    init{//由于主构造函数没有函数体,所有主构造函数中的逻辑都可以在init结构体内
        ...
    }
}

在Student类的主构造函数中增加name和age这两个字段时,不能再将它们声明为val,因为在主构造函数中声明为val或者var的参数会自动成为该类的字段,这就会导致和父类中同名的name和age字段造成冲突。因此,这里的name和age参数前面不用再加任何关键字,让它的作用域限定在主构造函数中即可。

class Student(val sno:String,val grade:Int,name:String,age:Int):Person(name,age){
    //Person(name,age)会调用父类的构造函数
    ...
}

任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以用于实例化一个类,只不过是有函数体的。

Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数必须调用主构造函数(包括间接调用)。次构造函数是通过constructor关键字来定义的。

class Student(val sno:String,val grade:Int,name:String,age:Int):Person(name,age){
    constructor(name:String,age:Int):this("",0,name,age){
        //通过this关键字来调用主构造函数
    }
    constructor():this("",0){
         //通过this关键字来调用上一个次构造函数,间接调用主构造函数
    }
}

还有一种特殊情况:类中只有次构造函数,没有主构造函数。(当一个类没有显式地定义主构造函数且定义了次构造函数时,他就是没有主构造函数的)

class Student:Person{
    constructor(name:String,age:Int):super(name,age){
        //由于没有主构造函数,次构造函数只能直接调用父类的构造函数
    }
}

函数可见性修饰

Java:public、private、protected、default(默认)

-> protected:对当前类、子类和同一包路径下的类可见

-> default:同一包路径下的类可见

Kotlin:public(默认)、private、protected、internal

-> protected:只对当前类和子类可见

-> internal:只对同一模块中的类可见

数据类与单例类

数据类

数据类通常需要重写equals()、hashCode()、toString()这几个方法。

data class Cellphone(val brand:String,val price:Double)

kotlin中数据类会根据主构造函数中的参数自动生成equals()、hashCode()、toString()这几个方法。

单例类

Java中的单例:

public class Singleton{
    private static Singleton instance;
    
    private Singleton(){}//将Singleton的构造函数私有化
    
    public synchronized static Singleton getInstance(){
        if(instance==null)
            instance=new Singleton();
        return instance;
    }//给外部提供getInstance()静态方法用于获取实例
    
    public void singletonTest(){
        System.out.println("singletonTest is called");
    }
}
//调用方法
Singleton.getInstance().singletonTest();

kotlin中的单例:

object Singleton{
    fun singletonTest(){
        println("singletonTest is called")
    }
}
//调用方法
Singleton.singletonTest()

集合

val list=ArrayList<String>()
list.add("apple")
list.add("banana")
list.add("orange")
list.add("pear")
list.add("grape")

等同于

val list=listof("apple","banana","orange","pear","grape")

不过listof()函数创建的是一个不可变的集合。指的是该集合只能用于读取,无法对集合进行添加、修改或删除操作。

val list=mutableListof("apple","banana","orange","pear")
list.add("grape")

mutableListof()函数创建一个可变的集合

val set=setof("apple","banana","orange","pear","grape")

Set集合的用法与List几乎一致,只不过是换成了setof()和mutableSetof()。需要注意的是,Set集合底层是使用hash映射机制来存放数据的,因此集合中的元素无法保证有序,这是和List集合最大的不同之处。

val map=HashMap<String,Int>()
map.put("apple",1)
map.put("banana",2)
map.put("orange",3)
map.put("pear",4)
map.put("grape",5)

//不过Kotlin不建议使用put()和get()方法进行添加和读取
map["apple"]=1 //向map添加一条数据
val number=map["apple"] //从map中读取数据

//简洁写法mapof()和mutableMapof()
val map=mapof("apple" to 1,"banana" to 2,"orange" to 3,"pear" to 4,"grape" to 5)
for((fruit,number) in map)
    println("fruit is $fruit , number is $number")

集合的函数时API

val list=listof("apple","banana","orange","pear","grape")
val maxLengthFruit=list.maxBy{it.length}

//map函数可用于将集合中的每个元素都映射成一个另外的值,映射的规则再Lambda表达式中指定
val newList=list.map{it.toUpperCase()}//将所有水果名变成大写模式


//filter用于过滤集合中的数据
val newList1=list.filter{it.lenght<=5}.map{it.toUpperCase()}//先过滤再转换效率更高

val anyResult=list.any{it.length<=5}//判断集合中是否至少存在一个元素满足指定条件(返回值为true/false)
val allResult=list.all{it.length<=5}//判断集合中是否全部元素满足指定条件(返回值为true/false)

Java函数式API使用

使用java创建一个线程

new Thread(new Runnable(){
    @Override
    public void run(){
        System.out.println("Thread is running");
    }
}).start()

使用kotlin创建一个线程

Thread(object:Runnable{
    override fun run(){
        println("Thread is running")
    }
}).start()

//等价于
Thread(Runnable{
    println("Thread is running")
}).start()

//等价于
Thread{
    println("Thread is running")
}.start()

类似Android中设置button的点击事件

Java:

button.setOnClickListener(new View.onClickListener(){
    @Override
    public void onClick(View v){
        ...
    }
})

Kotlin:

button.setOnClickListener{
    ...
}

标准函数

let函数与if判空

let函数是可以处理全局变量的判空问题的,而if判断语句则无法做到这一点。

var study:Study?=null
fun doStudy(){
    if(study!=null){
        study.readBooks()//study会报错
        study.doHomework()//study会报错
    }
}

之所以这里会报错,是因为全局变量的值随时都可能被其他线程所修改,即使做了判空处理,也无法保证if语句中的study变量没有空指针风险。而let函数可以保证。

with函数

with函数接受两个参数:1.可以是一个任意类型的对象。2.是一个Lambda表达式。

with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。

val result=with(obj){
    //这里是obj的上下文
    "value" //with函数的返回值
}

样例:

val list=listof("apple","banana","orange","pear","grape")
val builder=StringBuilder()
builder.append("Start eating fruits.\n")
for(fruit in list){
    builder.append(fruit).append("\n")
}
builder.append("Ate all fruits")
val result=builder.toString()
println(result)

等同于

val list=listof("apple","banana","orange","pear","grape")
val result=with(StringBuilder()){
    append("Start eating fruits.\n")
    for(fruit in list){
        append(fruit).append("\n")
    }
    append("Ate all fruits")
    toString()
}
println(result)

两段代码的执行结果都一样,无非第二段代码写法更加简洁,这就是with函数的作用。

run函数

run函数的用法和使用场景和with函数是非常相似的,只是稍微做了一些语法修改而已。首先run函数是不能直接调用的,而是一定要调用某个对象的run函数才行。其次run函数只接受一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。其他方面和with函数是一样的,包括也会使用Lambda表达式中的最后一行代码作为返回值返回。

val result=obj.run{
    //这里是obj的上下文
    "value" //run函数的返回值
}

使用run修改上述例子:

val list=listof("apple","banana","orange","pear","grape")
val result=StringBuilder().run{
    append("Start eating fruits.\n")
    for(fruit in list){
        append(fruit).append("\n")
    }
    append("Ate all fruits")
    toString()
}
println(result)

apply函数

apply函数只能在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。

val result=obj.apply{
    //这里是obj的上下文
}
//result == obj

再次修改案例:

val list=listof("apple","banana","orange","pear","grape")
val result=StringBuilder().apply{
    append("Start eating fruits.\n")
    for(fruit in list){
        append(fruit).append("\n")
    }
    append("Ate all fruits")
}
println(result.toString())

定义静态方法

companion object——伴生对象

class Util{
    fun doAction1(){
        println("do action1")
    }
    companion object{
        fun doAction2(){
            println("do action2")
        }
    }
}

//实际调用
Util().doAction1()
Util.doAction2()

不过,doAction2()方法其实也并不是静态方法,companion object 这个关键字实际上会在Util类的内部创建一个伴生类,而doAction2()方法就是定义在这个伴生类里面的实例方法。只是Kotlin会保证Util类始终只会存在一个伴生类对象,因此调用Util.doAction2()方法实际上就是调用Util类中伴生对象的doAction2()方法。

因此可以看出,Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。

真.静态方法

*注解

如果我们给单例类或者companion object 中的方法加上@JvmStatic 注解,那么Kotlin编译器会将这些方法编译成真正的静态方法:

class Util{
    fun doAction1(){
        println("do action1")
    }
    companion object{
        @JvmStatic
        fun doAction2(){
            println("do action2")
        }
    }
}

注意:@JvmStatic 注解只能加载单例类或companion object中的方法上,如果尝试加载一个普通方法上,会直接提示语法错误。

*顶层方法

顶层方法是指那些没有定义在任何类中的方法,比如我们在上一节中编写的main()方法。Kotlin编译器会将所有的顶层方法全部编译成静态方法,因此只要你定义了一个顶层方法,那么它一定是静态方法。

直接在Kotlin文件中直接定义顶层方法。

如果是在Kotlin代码中调用,所有顶层方法都可以在任何位置被直接调用,不用管包名路径,也不用创建实例,直接键入该顶层方法即可。

如果是在Java代码中调用是找不到对应方法的,因为Java中没有顶层方法这个概念,所有的方法必须定义在类中。该方法隐藏在Kotlin创建的一个对应类中。例如Kotlin存放顶层方法的文件为Helper.kt,于是Kotlin编译器会自动创建一个叫做HelperKt的Java类,顶层方法就存放于其中。

延迟初始化

延迟初始化使用lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了。

class MainActivity:AppCompatActivity(),View.OnClickListener{
    private lateinit var adapter :MsgAdapter
    override fun onCreate(savedInstanceState:Bundle?){
        ...
        adapter=MsgAdapter(msgList)
        ...
    }
    override fun onClick(v:View?){
        ...
        adapter.notifyItemInserted(msgList.size-1)
        ...
    }
}

当然,使用lateinit关键字也不是没有任何风险,如果我们在adapter变量还没有初始化的情况下就直接使用它,那么程序就一定会崩溃,并且抛出一个UninitializedPropertyAccessException异常。所以当你对一个全局变量使用lateinit关键字时。要确保它能够初始化。

另外我们可以通过代码来判断一个全局变量是否完成初试化:

class MainActivity:AppCompatActivity(),View.OnClickListener{
    private lateinit var adapter :MsgAdapter
     override fun onCreate(savedInstanceState:Bundle?){
        ...
        if(!::adapter.isInitialized){ //::adapter.isInitialized可用于判断变量是否已经完成初始化
            adapter=MsgAdapter(msgList)
        }
        ...
    }
}

密封类

原代码:

interface Result
class Success(val msg:String) :Result
class Failure(val error:Exception) :Result

fun getResultMsg(result:Result)=when(result){
    is Suceess -> result.msg
    is Failure -> result.error.message
    else -> throw IllegalArgumentException()
}

密封类的关键字是sealed class:

sealed class Result
class Success(val msg:String) :Result()
class Failure(val error:Exception) :Result()

fun getResultMsg(result:Result)=when(result){
    is Suceess -> result.msg
    is Failure -> "Error is ${result.error.message}"
}

采用密封类的好处就是when中else条件已经不再需要了。因为在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该封闭类有哪些子类,并强制要求你将每一个子类对应的条件全部处理。这样就可以保证,即使没有编写else条件,也不可能会出现漏写条件分支的情况。

密封类及其子类之恶能定义在同一个文件的顶层位置,不能嵌套在其他类中,这是被密封类底层实现机制所限制的。

扩展函数

fun ClassName.methodName(param1:Int,param2:Int):Int{
    return 0
}

例如向String类中添加一个统计一个字符串内部字母的数量的函数,可以使用扩展函数:

fun String.lettersCount():Int{
    var count=0
    for (char in this){
        if(char.isLetter()){
            count++
        }
    }
}
//run
val count="ABC123xyz!@#".lettersCount()

我们将lettersCount()方法定义成String类的扩展函数,那么函数中就自动拥有了String实例的上下文。因此lettersCount()函数就不再需要接收一个字符串参数了,而是直接遍历this即可,因为现在this就代表着字符串本身。

扩展函数在很多情况下可以让API变得更加简洁、丰富,更加面向对象。

String类是一个final类,任何一个类都不可以继承它,也就是说它的API只有固定的那些而已,至少在Java中就是这样。然而到了Kotlin中就不一样了,我们可以向String类中扩展任何函数,使它的API变得更加丰富。

运算符重载

运算符重载使用的是operator关键字,只要在指定函数的前面加上operator关键字,就可以实现运算符重载的功能了。但问题在于这个指定函数是什么?因为不同的运算符对应的重载函数也是不同的。

//以+为例
class obj{
    operator fun plus(obj:Obj):Obj{
        //处理相加的逻辑
    }
}

在上述代码中,关键字operator和函数名plus都是固定不变的,而接受的参数和函数返回值可以根据你的逻辑自行设定。

实例化为:

class Money(val vaule:Int){
    operator fun plus(money:Money):Money{
        val sum=value+money.value
        return Money(sum)
    }
    //可以重载
    operator fun plus(newValue:Int):Money{
        val sum=value+newValue
        return Money(sum)
    }
}

常用的运算符对应重载函数名为:

a+b                    a.plus(b)
a-b                    a.minus(b)
a*b                    a.times(b)
a/b                    a.div(b)
a%b                    a.rem(b)
a++                    a.inc()
a--                    a.dec()
a==b                   a.equals(b)
a>=b                   a.compareTo(b)
a in b                 b.contains(a)

高阶函数

高阶函数:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。Kotlin添加了一个函数类型的概念。

(String,Int) -> Unit

->左边部分,即(String,Int)用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了。->右边部分,即Unit用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit,大致相当于Java的void。

fun example(func:(String,Int)->Unit){
    func("hello",123)
}

高阶函数允许让函数类型的参数来决定函数的执行逻辑。即使是同一个高阶函数,只要传入不同的函数参数,那么它的执行逻辑和最终的返回结果就可能是完全不同的。

例子1:

fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int)->Int):Int{
    val result=operation(num1,num2)
    return result
}
fun plus(num1:Int,num2:Int):Int{
    return num1+num2
}
fun minus(num1:Int,num2:Int):Int{
    return num1-num2
}
//执行
fun main(){
    val num1=100
    val num2=80
    val result1=num1AndNum2(num1,num2,::plus)//::plus这种写法是一种函数引用方式的写法
    println("result1 is $result1")
    val result2=num1AndNum2(num1,num2,::minus)
    println("result2 is $result2")
}
//采用Lambda方式来调用
fun main(){
    val num1=100
    val num2=80
    val result1=num1AndNum2(num1,num2){ n1 , n2 ->
        n1 + n2
    }
    println("result1 is $result1")
    val result2=num1AndNum2(num1,num2){ n1 , n2 ->
        n1 - n2
    }
    println("result2 is $result2")
}

例子2:

fun StringBuilder.build(block:StringBuilder.() -> Unit):StringBuilder{
    block()
    return this
}//给StringBuilder类定义了一个build扩展函数,这个扩展函数接受一个返回类型是StringBuilder的函数类型参数

注意:这个函数类型声明有所不同,它在函数类型的前面加上了一个**StringBuilder. **的语法结构。这才是定义高阶函数完整的语法规则,在函数类型的前面加上ClassName,就表明这个函数类型是定义在哪个类当中的。

将函数类型定义到StringBuilder类中的好处是当我们调用build函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文,同时也是apply函数的实现方式。

fun main(){
    val list=listOf("apple","banana","orange","pear","grape")
    val result=StringBuilder().build{//this:StringBuilder
        append("Start eating fruits.\n")
        for(fruit in list){
            append(fruit).append("\n")
        }
        append("Ate all fruits.")
    }
    println(result.toString())
}

可以看到,build函数的用法和apply函数基本上是一样的,只不过我们编写的build函数目前只能作用在StringBuilder类上边,而apply函数可以作用在所有类上边。

高阶函数的应用

简化Sharedpareferences的用法

val editor=getSharedPreferences("data",Context.MODE_PRIVATE).edit()
editor.putString("name","Tom")
editor.putInt("age",28)
editor.putBoolean("married",false)
editor.apply()

简化用法:

//新建SharedPreferences.kt文件
fun SharedPreferences.open(block:SharedPreferences.Editor.()->Unit){
    val editor=edit()
    editor.block()
    editor.apply()
}

//调用
getSharedPreferences("data",Context.MODE_PRIVATE).open{
    putString("name","Tom")
    putInt("age",28)
    putBoolean("married",false)
}

首先我们通过扩展函数的方式向SharedPreferences类中添加了一个open函数,并且它还接受一个函数类型的参数,因此open函数自然就是一个高阶函数了。

由于open函数内拥有SharedPreferences的上下文,因此这里可以直接调用edit()方法来获取SharedPreferences.Editor对象。另外open函数接收的是一个SharedPreferences.Editor的函数类型参数,因此这里需要调用editor.block()对函数类型参数进行调用,我们就可以在函数类型参数的具体实现中添加数据了。最后还需要调用editor.apply()方法来进行提交数据,从而完成数据存储操作。

实际上Google提供的KTX扩展库中已经包含了上述的SharedPreferences的简化用法,我们可以直接使用:

getSharedPreferences("data",Context.MODE_PRIVATE).edit{
    putString("name","Tom")
    putInt("age",28)
    putBoolean("married",false)
}

简化ContentValues的用法

val values=ContentValues()
values.put("name","Game of Thrones")
values.put("author","George Martin")
db.insert("Book",null,values)

简化后:

//新建一个ContentValues.kt文件
fun cvOf(vararg pairs:Pair<String,Any?>):ContentValues{
    val cv=ContentValues()
    for(pair in pairs){
        val key=pair.first
        val value=pair.second
        when(value){
            is Int -> cv.put(key,value)
            is Long -> cv.put(key,value)
            is Short -> cv.put(key,value)
            is Float -> cv.put(key,value)
            is Double -> cv.put(key,value)
            is Boolean -> cv.put(key,value)
            is String -> cv.put(key,value)
            is Byte -> cv.put(key,value)
            is ByteArray -> cv.put(key,value)
            null -> cv.putNUll(key)
        }
    }
    return cv
}
//使用
val values=cvOf("name" to "Game of Thrones","author" to "George Martin")
db.insert("Book",null,values)

cvOf()方法接收了一个Pair参数,也就是使用A to B语法结构创建出来的参数类型,但是我们在参数前面加上了一个vararg关键字(vararg对应的就是Java中的可变参数列表,我们允许像这个方法传入0、1、2甚至任意多个Pair类型的参数,这些参数都会被赋值到使用vararg声明的这一个变量上面,然后使用for-in循环可以将传入的所有参数遍历出来)。

Pair类型是一种键值对的数据结构,因此需要通过泛型来指定它的键和值对应的参数类型。kotlin中的Any相当于Java中的Object。

核心思路就是先创建一个ContentValues对象,然后遍历pairs参数列表,取出其中的数据并填入ContentValues中,最终将ContentValues对象返回即可。

优化:

fun cvOf(vararg pairs:Pair<String,Any?>)=ContentValues().apply{
    val cv=ContentValues()
    for(pair in pairs){
        val key=pair.first
        val value=pair.second
        when(value){
            is Int -> put(key,value)
            is Long -> put(key,value)
            is Short -> put(key,value)
            is Float -> put(key,value)
            is Double -> put(key,value)
            is Boolean -> put(key,value)
            is String -> put(key,value)
            is Byte -> put(key,value)
            is ByteArray -> put(key,value)
            null -> putNUll(key)
        }
    }
    return cv
}

由于apply函数的返回值就是它的调用对象本身,因此这里可以使用单行代码函数的语法糖,用等号(=)替代返回值的声明。通过apply()实现高阶函数来进行简化代码。

同样,KTX库中也提供了一个具有同样共嗯那个的contentValueOf()方法:

val values=contentValueOf("name" to "Game of Thrones","author" to "George Martin")
db.insert("Book",null,values)

内联函数

内联函数能够使Lambda表达式带来的运行时开销完全消除。

inline fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int) -> Int):Int{
    val result=operation(num1,num2)
    return result
}

内联函数在需要在定义高阶函数时加上inline关键字即可。其原理是Koltin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样就不存在运行时的开销了。

noinline

如果一个高阶函数中接受了两个或者更多函数类型的参数,这时候我们给函数加上inline关键字,那么Kotlin编译器会自动将所有引用的Lambda表达式全部进行内联。如果我们只想内联其中一个Lambda表达式,就可以使用noinline关键字:

inline fun inlineTest(block1:()->Unit,noinline block2:()->Unit){
    
}

内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数类型。非内联的参数类型可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另一个内联函数,这是其最大的局限性。同时内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能就行局部返回。

例子:

fun printString(str:String,block:(String)->Unit){
    println("printString begin")
    block(str)
    println("printString end")
}
fun main(){
    println("main start")
    val str=""
    printString(str){ s -> 
        println("lambda start")
        if(s.isEmpty()) return@printString
        println(s)
        println("lambda end")
    }
    println("main end")
}

这里定义了一个叫做printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,就不需要打印。注意:Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。

如果将printString()的高阶函数声明为一个内联函数,情况就不一样了。

inline fun printString(str:String,block:(String)->Unit){
    println("printString begin")
    block(str)
    println("printString end")
}
fun main(){
    println("main start")
    val str=""
    printString(str){ s -> 
        println("lambda start")
        if(s.isEmpty()) return
        println(s)
        println("lambda end")
    }
    println("main end")
}

现在printString()函数百年成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时return代表的是返回外层的调用函数,即main()函数。

crossinline

如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。因为实际是在匿名类中调用了传入的函数参数类型。而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里救出会错。

inline fun runRunable(block:()->Unit){
    val runable=Runnable{
        block()//此处会报错
    }
    runable.run()
}

此时可以借助crossinline关键字来解决这个问题。

inline fun runRunable(crossinline block:()->Unit){
    val runable=Runnable{
        block()//此处会报错
    }
    runable.run()
}

之前的错误会出现是因为内联函数的Lambda表达式中允许使用return关键字和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像是一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题就解决了。

声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍可以使用return@printString的写法进行局部返回。总体来说,出了在return关键字使用上有区别,crossinline保留了内联函数的其他所有特征。

泛型

基本的泛型类:

class MyClass<T>{
    fun method(param:T):T{
        return param
    }
}
//调用
val myClass=MyClass<Int>()
val result=myClass.method(123)

泛型方法:

class MyClass{
    fun <T> method(param:T):T{
        return param
    }
}
//调用
val myClass=MyClass()
val result=myClass.method<Int>(123)

Kotlin还允许我们对泛型的类型进行限制:

class MyClass{
    fun <T : Number> method(param:T):T{
        return param
    }
}//这种写法只能将method方法的泛型指定成数字类型(Int、Float、Double等),如果传入其他就会报错

另外在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的时候,泛型上界默认是Any?。而如果想要泛型的类型不为空,只需要手动将泛型的上界指定成Any就可以了。

应用:

//原
fun StringBuilder.build(block:StringBuilder.() -> Unit):StringBuilder{
    block()
    return this
}

//泛型化
fun <T> T.build(block:T.() -> Unit):T{
    block()
    return this
}

//调用
contentResolver.query(uri,null,null,null,null)?.build{
    while(moveToNext()){
        ...
    }
    close()
}

原本build函数只能作用在StringBuilder类上面,通过泛型化可以实现同apply函数一样的效果,可适用于所有类。

委托

委托是一种设计模式:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。Java对于委托并没有语言层级的实现。Kotlin支持委托,并将委托分为类委托和委托属性。

类委托

类委托核心思想在于将一个类的具体实现委托给另一类去完成。

比如Set这种数据结构,由于Set是一个接口,如果要使用它的话,需要使用它具体的实现类,比如HashSet。而借助委托模式,我们可以实现一个自己的实现类。

class MySet<T>(val helperSet:HashSet<T>):Set<T>{
    override val size:Int 
        get()=helperSet.size
    
    override fun contains(element:T)=helperSet.contains(element)
    
    override fun containsAll(elements:Collection<T>)=helperSet.containsAll(elements)
    
    override fun isEmpty()=helperSet.isEmpty()
    
    override fun iterator()=helperSet.iterator()
}

可以看到,MySet的构造函数中接收了一个HashSet参数,这就相当于一个辅助对象。然后再Set接口所有的方法实现中,我们都没有进行自己的实现,而是调用了辅助对象中对应的方法实现,这其实就是一种委托模式。

委托模式的意义在于,我们可以让大部分的方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么MySet就成为了一个全新的数据结构类。但适用于接口中待实现方法比较少的,如果多了都要自己一个个实现起来真的要死了。但是再Kotlin中可以使用类委托来解决。

class MySet<T>(val helperSet:HashSet<T>):Set<T> by helpterSet{
    fun helloWord()=println("Hello Word") //新定义
    override fun isEmpty()=false //重写
}

kotlin中委托使用的关键字是by,我们只需要再接口声明的后边使用by关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式代码了。

委托属性

委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。

class MyClass{
    var p by Delegate()
}

//示例Delegate类
class Delegate{
    var propValue:Any?=null
    
    operator fun getValue(myClass:MyClass , prop:KProperty<*>):Any?{
        return propValue
    }
    
    operator fun setValue(myClass:MyClass , prop:KProperty<*>,value:Any?){
        propValue=value
    }
}

将p属性具体实现委托给了Delegate类去完成。当调用p属性的时候会自动调用Delegate的getValue()方法,当给p赋值的时候会自动调用Delegate类的setValue()方法。

Delegate类中是一种标准的代码实现模板,两个方法必须都要用到operator关键字。getValue()方法要接收两个参数:第一个用于声明在什么类中使用,这里是MyClass;第二个参数KProperty<>是Kotlin中的一个属性操作类,可用于获取各种属性相关的值,虽然当前用不到,但是必须在方法参数上声明。<>这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已,类似于java中的<?>写法。返回值可以写成声明成任意类型,根据具体实现逻辑去写就行了。setValue()方法前两个参数同getValue()方法,最后一个参数表示具体要复制给委托属性的值,这个参数类型必须和getValue()方法返回值的类型保持一致。

整个委托属性的工作流程:当我们给MyClass的p属性赋值时,就会调用Delegate类的setValue()方法,当获取MyClass中p属性的值时,就会调用Delegate类的getValue()方法。

不过还存在一种情况可以不用在Delegate类中实现setValue()方法,那就是MyClass中的p属性是使用val关键字声明的。这一点也很好理解,如果是P属性是使用val关键字声明的表示其不可再初始化之后重新赋值,因此没必要实现setValue()。

by lazy

by lazy是一种懒加载技术。把想要延迟执行的代码放到by lazy 代码块中,这样代码块中的代码在一开始的时候就不会执行,只有当uriMatcher变量首次被调用的时候,代码块中的代码才会执行。

val p by lazy{ ... }

实际上,by lazy并不是连在一起的关键字,只有by才是Kotlin中的关键字,lazy在这里只是一个高阶函数而已。再lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,实际调用的是Delelgate对象的getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式,这样表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。

自己实现一个lazy函数:

//新建一个Later.kt文件
class Later<T>(val block:()->T){
    var value:Any?=null
    operator fun getValue(any:Any?,prop:KProperty<*>):T{
        if(value==null){
            value=block()
        }
        return value as T
    }
}

//定义一个顶层函数
fun <T> later(block:()->T)=Later(bolck)

这里将getValue()方法的第一个参数指定成了Any?类型,表示我们希望Later的委托功能再所有类中都可以使用。然后使用了一个value变量对值进行缓存,如果value为空就调用构造函数中传入的函数类型参数去获取值,否则就返回。

由于懒加载技术不会对属性进行赋值,因此就可以不用实现setValue()方法了。

later()这个顶层函数定义成了泛型,并且它也接收一个函数类型参数。其作用很简单:创建Later类的实例,并将接收的函数类型参数传给Later类的构造函数。

//使用
val uriMatcher by later{
    val mathcer=UriMatcher(UriMatcher.NO_MATCH)
    mathcer.addURI(authority,"book",bookDir)
    mathcer.addURI(authority,"book/#",bookItem)
    mathcer.addURI(authority,"category",categoryDir)
    mathcer.addURI(authority,"category/#",categoryItem)
    matcher
}

泛型实化

Java中泛型功能是通过类型擦除机制来进行实现的。泛型对于类型的约束只在编译时期存在,运行的时候仍然会按照JDK 1.5之前的机制(诸如List之类的数据结构可以存储任意类型的数据,取出数据也需要手动向下转型才行)来运行,JVM是识别不出来我们在代码中指定的泛型类型的。例如,假设我们创建了一个List<String>集合,虽然在编译时期只能向集合中添加字符串类型的元素,但是在运行时期JVM并不能知道它本来只打算包含哪种类型的元素,只能识别出是一个List。

所有基于JVM的语言,他们的泛型功能都是通过泛型擦除机制来实现的,其中当然也包括了Kotlin。这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T实际类型在运行的时候已经被擦除了。

然而不同的是Kotlin提供了一个内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样的话也就不存在什么泛型擦除的问题,因为代码在编译之后会直接使用实际的类型来替换内联函数中的泛型声明。这也就意味着,Kotlin中是可以将内联函数中的泛型进行实化的。

要实现实化,需要该函数必须是内联函数(用inline关键字来修饰该函数),在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化。

//获取泛型实际类型的功能
inline fun <reified T> getGenericType() = T::class.java

//测试
fun main(){
    val result1=getGenericType<String>()
    val result2=getGenericType<Byte>()
    println("result1 is $result1")
    println("result2 is $result2")
}

T.class在Java中是不合法的,而在Kotlin中,借助泛型实化就可以使用T::class.java这样的语法了。

泛型实化应用

//原
val intent=Intent(context,TestActivity::class.java)
intent.putExtra("param1","data")
intent.putExtra("param2",123)
context.startActivity(intent)

//新建reified.kt
inline fun <reified T> startActivity(context:Context,block:()->Unit){
    val intent=Intent(content,T::class.java)
    intent.block()
    context.startActivity(intent)
}

//test
startActivity<TestActivity>(context){
    putExtra("param1","data")
    putExtra("param2",123)
}

泛型的协变

假设定义了一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<A>又是MyClass<B>的子类型,那么我们可以称MyClass在T这个泛型上是协变的。

注意:如果一个泛型类型的数据上是只读的,那么它是没有类型转换安全隐患的。而要实现这一点,则需要让MyClass<T>类中的所有方法都不能接收T类型的参数。换句话说,T只能出现在out位置上,而不能出现在in位置上。

open class Person(val name:String,val age:Int)
class Student(name:String,age:Int):Person(name,age)
class Teacher(name:String,age:Int):Person(name,age)

class SimpleData<out T>(val data:T?){
    fun get():T?{
        return data
    }
}

//Test
fun main(){
    val student=Student("Tom",19)
    val data=SimpleData<Student>(student)
    handleMyData(data)
    val studentData=data.get()
}

fun handleMyData(data:SimpleData<Person>){
    val personData=data.get()
}

out关键字表明泛型T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上是协变的。因此就不能用set()方法为data赋值了,所以改成了使用构造参数的方式来赋值。虽然构造函数初T也是在in位置上,但是加上val就可以使得data参数不可修改。另外,其实使用var,也可以配合private修饰符来保证只读。

由于SimpleData类已经进行了协变声明,那么SimpleData<Student>自然就是SimpleData<Person>的子类了,所以这里可以安全地向handleMyData()传递参数。

然后再handleMyData()方法中去获取SimpleData封装的数据,虽然这里泛型声明的是Person类型,实际获得的会是一个Student的实例,但由于Person是Student的父类,向上转型是完全安全的,所以这段代码是没问题的。

其实Kotlin已经默认给许多内置的API都加上了协变声明,其中就包括各种集合的类与接口。例如List,List是只读的,意味着天然就是可以协变的。

泛型的逆变

逆变与协变完全相反。假设定义了一个MyClass<T>的泛型类,其中A是B的子类i型,同时MyClass<B>是MyClass<A>的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。

open class Person(val name:String,val age:Int)
class Student(name:String,age:Int):Person(name,age)
class Teacher(name:String,age:Int):Person(name,age)

interface Transformer<T>{
    fun transform(t:T):String
}

//子类实现由T -> String
fun main(){
    val trans=object:Transformer<Person>:String{
        override fun transform(t:Person):String{
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(trans) //会报错
}

fun handleTransformer(trans:Transformer<Student>){
    val student=Student("tom",19)
    val result=trans.transform(student)
}

//修改
interface Transformer<in T>{
    fun transform(t:T):String
}

handleTransformer()方法接收的是一个Transformer<Student>类型的参数,这里handleTransformer()方法中创建了一个Student对象,并调用了参数的transform()方法将Student对象转成一个字符串。

报错原因是因为Transformer<Person>并不是Transformer<Student>的子类。

而解决办法是在泛型T前加上一个in关键字,这就意味着T只能出现在in位置上,同时也意味着Transformer在泛型上是逆变的。此时Transformer<Person>成为了Transformer<Student>的子类。

逆变功能在Kotlin内置API中的应用,比较典型的例子就是Comparable的使用。Comparable是比较两个对象大小的例接口,其源码为:

interface Comparable<in T>{
    operator fun compareTo(other :T):Int
}

Comparable在T这个泛型上就是逆变的,compareTo()方法用于实现具体的比较逻辑。

高级语法糖

infix函数

首先说明一下,A to B中的to并不是关键字。之所有有这样的语法结构,是因为Kotlin提供了一种高级语法糖特性:infix函数,原理是把编程语言函数调用的语法规则调整了一下而已。A to B等价于A.to(B)。

//原版 String类中的startsWith()函数
if("Hello Kotlin".startsWith("Hello")){ ... }

//借助infix函数
infix fun String.beginsWith(prefix:String)=startsWith(Prefix)

//调用
if("Hello Kotlin" beginsWith "Hello"){ ... }

infix由于其语法糖格式的特殊性,有两个比较严格的限制:首先,infix函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;其次,infix函数必须接收且只能接收一个参数,至于参数类型是没有限制的。只有同时满足这两个点,infix语法糖才具备使用的条件。

//原版 Collection类中的contains()函数
val list=listOf("apple","banana","orange","pear","grape")
if(list.contains("banana")){ ... }

//借助infix函数
infix fun <T> Collection<T>.has(element:T)=contains(element)

//调用
val list=listOf("apple","banana","orange","pear","grape")
if(list has "banana"){ ... }

Collection是Java已经Kotlin所有集合的总接口,因此可以给Collection加上一个has()函数,所有集合的子类都可以使用这个函数了。

模仿A to B自己实现“:

//新建infix.kt文件
infix fun <A,B> A.with(that:B):Pair<A,B>=Pair(this,that)

//调用
val map=mapOf("apple" with 1,"banana" with 1,"orange" with 1,"pear" with 1,"grape" with 1)

协程

协程是一种轻量级的线程。之前学习的线程是非常重量级的,需要依靠操作系统的调度才能实现不同协程之间的切换。而使用协程却可以仅在编译语言层面就可以实现不同协程之间的切换,从而大大提升了并发编程的运行效率。

协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。这种特性使得高并发程序的运行效率得到了极大的提升。

Kotlin没有把协程纳入标准库的API,而是以依赖库的形式提供。因此需要添加依赖:

dependencies{
    ...
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1" //Android才需要
}

GlobalScope.launch

//新建Coroutines.kt
fun main(){
    //开启协程
    GlobalScope.launch{
        println("codes run in coroutine scope")
    }
    Thread.sleep(1000)//GlobalScope.launch函数每次创建都是一个顶层协程,这种携程当应用程序运行结束时也会跟着一起结束,所以打印的日志出不来,需要让程序延迟一段时间在结束才能打印
}

但是如果代码块中的代码在一秒内不能运行结束,那么就会被强制中断。

runBlocking

fun main(){
    runBlocking{
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }
}

runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。需要注意的是,runBlocking函数通常只应该在测试环境下使用,正式环境中使用会出现性能上的问题。

fun main(){
    runBlocking{
        launch{
            println("launch1")
            delay(1500)
            println("launch1 finished")
        }
        launch{
            println("launch2")
            delay(1500)
            println("launch2 finished")
        }
    }
}

suspend关键字

随着launch函数中的逻辑越来越复杂,可能你需要将部分代码提取到一个单独的函数中。此时出现一个问题:我们在launch函数中编写的代码是拥有协程作用域的,但是提取到一个单独的函数中就没有协程作用域了,要想调用delay()这样的挂起函数,需要使用Kotlin提供的一个suspend关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以互相调用的。

suspend fun printDot{
    println(".")
    delay(1000)
}

这样就可以在printDot()函数中调用delay()函数了。

coroutineScope

但是suspend关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。比如在printDot内调用launch函数是无法调用成功的,因为launch要求必须在协程作用域中才能调用。这个问题可以借助coroutineScope函数来解决。coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用,它的特点是可以继承外部的协程作用域并创建一个子作用域,借助这个特性,我们就可以给任意挂起函数提供协程作用域了。

suspend fun printDot()=coroutineScope{
    launch{
        println(".")
        delay(1000)
    }
}

//示例代码
fun main(){
    runBlocking{
        coroutineScope{
            launch{
                for (i in 1..10){
                    println(i)
                    delay(1000)
                }
            }
        }
        println("coroutineScope finished")
    }
    println("runBlocking finished")
}

coroutineScope函数和runBlocking函数类似,它可以保证其作用域内的所有代码和子线程全部执行完之前,会一直阻塞当前协程。

但coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此不会造成任何性能上的问题。而runBlocking函数由于会阻塞当前线程,如果又在主线程中调用的话,就会初夏问题,所以不推荐在实际项目中使用。

总结:GlobalScope.launch、runBlocking、launch、coroutineScope这几种作用域构造器都可以创建一个新的协程作用域,只不过GlobalScope.launch、runBlocking函数是可以在任意地方调用的,coroutineScope可以在协程作用域或者挂起函数中调用,而launch函数只能在协程作用域中调用。

同时,runBlocking由于会阻塞线程,因此只建议在测试环境下使用。而GlobalScope.launch由于每次创建的都是顶层协程,一般也不太建议使用,除非你非常明确就要创建顶层协程。顶层协程管理成本太高。

//取消协程
val job=GlobalScope.launch{
    //处理具体的逻辑
}
job.cancel()

比较实用的协程写法:

val job=Job()
val scope=CoriutineScope(job)
scope.launch{
    //处理具体逻辑
}
job.cancel()

以上代码适用于实际项目中,测试或者仅仅在main()中,使用runBlocking即可。

async函数

async函数可以获取协程的执行结果,async函数必须在协程作用域中才能调用,他会创建一个新的子协程并返回一个Deferred对象,如果我们想要获取async函数代码块的执行结果,只需要调用Deferred对象的await()方法即可,代码如下:

fun main(){
    runBlocking{
        val result=async{
            5+5
        }.await()
        println(result)
    }
}//在async函数代码块中进行了一个简单的数学运算,然后调用await()方法获取运算结果,最终打印出来

事实上,在调用了async函数之后,代码块中的代码就会立刻开始执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法会将当前协程阻塞掉,知道可以获取async函数的执行结果。

我们不必再每次调用async函数之后就立刻使用await()方法获取结果了,而是仅在需要用到async函数的执行结果时才调用await()方法进行获取,这样多个async函数就变成了并行关系。

fun main(){
    runBlocking{
        val result1=async{
            delazy(1000)
            5+5
        }
        val result2=async{
            delazy(1000)
            5+3
        }
        println("${result1.await()} / ${result2.await()}")
    }
}

withContext()函数(线程参数)

withContext()函数:是一个挂起函数,大体可以理解成async函数的一种简化版写法。

fun main(){
    runBlocking{
        val result=withContext(Dispatchers.Defult){
            5+5
        }
        println(result)
    }
}

调用withContext()函数之后 ,会立即执行代码块中的代码,同时将当前协程阻塞掉。当代码块中的代码全部执行完之后,会将最后一行的执行结果做为withContext()函数的返回值返回,因此基本上相当于val result=async{5+5}.await()的写法。唯一不同的是withContext()函数强制要求我们指定一个线程参数。

虽然协程是一种轻量级线程,但并不意味着我们就永远不需要开启线程了。比如Android中要求网络请求必须在子线程中进行,如果在主线程开启协程进行网络请求依然会出错。

线程参数有:Dispatchers.Defult、Dispatchers.IO、Dispatchers.Main。

Dispatchers.Defult表示会使用一种默认低并发的线程策略。Dispatchers.IO表示会使用一种较高并发的线程策略(网络请求)。Dispatchers.Main表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。

事实上,之前所学的除了coroutineScope函数之外都可指定线程参数。

回调

回调机制基本上是依靠匿名类来实现的,但是匿名类的写法通常比较复杂:

HttpUtil.sendHttpRequest(address,object:HttpCallbackListener){
    override fun onFinish(response:String){
        //得到服务器返回的具体内容
    }
    override fun onError(e:Exception){
        //对异常情况进行处理
    }
}

可以借助suspendCoroutine函数来进行简化。suspendCoroutine函数必须在协程作用域或者挂起函数中才能调用,它接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。Lambda表达式的参数列表会传入一个Continuation参数,调用它的resume()方法或resumeWithException()可以让协程恢复执行。

suspend fun request(address:String):String{
    return suspendCoroutine{ continuation ->
        HttpUtil.sendHttpRequest(address,object:HttpCallbackListener){
           override fun onFinish(response:String){
                continuation.resume(response)
            }
            override fun onError(e:Exception){
                continuation.resumeWithException(e)
            }
        }
    }
}

//调用
suspend fun getBaiduResponse(){
    try{
        val response=request("http://www.baidu.com")//对服务器响应的数据进行处理
    } catch(e:Exception){
        //对异常情况进行处理
    }
}

request()函数是一个挂起函数,并且接受一个address参数,在request()函数的内部,调用了suspendCoroutine函数,这样当前协程会立即挂起,而Lambda表达式中的代码则会在普通线程中执行。接着我们在Lambda表达式中调用HttpUtil.sendHttpRequest()方法发起网络请求,并通过传统回调的方式监听请求结果。如果请求成功就调用Continuation的resume()方法,传入服务器响应的数据,该值会成为suspendCoroutine函数的返回值,失败就调用resumeWithException()方法恢复被挂起的协程,并传入具体的异常原因。

事实上,suspendCoroutine函数几乎可以用于简化任何回调的写法,比如之前使用Retrofit来发起网络请求:

val appService=ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object:Callback<List<App>>{
    override fun onResponse(call:Call<List<App>>,response:Response<List<App>>){
        //得到服务器返回的数据
    }
    override fun onFailure(call:Call<List<App>>,t:Throwable){
        //在这里对异常情况进行处理
    }
})

//简化 由于Service接口返回的数据类型也不同,所以需要使用泛型
suspend fun <T> Call<T>.await():T{
    return suspendCoroutine{ continuation ->
        enqueue(object:Callback<T>{
            override fun onResponse(call:Call<T>,response:Response<T>){
                val body=response.body()
                if(body!=null) continuation.resume(body)
                else continuation.resumeWithException(RuntimeException("response body is null"))
            }
            override fun onFailure(call:Call<T>,t:Throwable){
                continuation.resumeWithException(t)
            }
        })
    }
}

suspend fun getAppData(){
    try{
        val appList=ServiceCreator.create<AppService>().getAppData().await()
    } catch(e:Exception){
        //...
    }
}

await()函数仍是一个挂起函数,然后我们给它声明了一个泛型T,并将await()函数定义成了一个Call<T>的扩展函数这样返回值是Call类型的Retrofit网络请求接口就可以都直接调用await()函数了。

接着await()函数中使用了suspendCoroutine函数来挂起当前协程,并且由于扩展函数原因,现在拥有了Call对象的上下文,那么这里就可以直接调用enqueue()方法让Retrofit发起网络请求。接下来使用同样的方式对Retrofit响应的数据或者失败情况进行处理。

常用工具

求N个数的最大最小值

fun max(vararg nums:Int):Int{
    var maxNum=Int.MIN_VALUE
    for(num in nums){
        maxNum=max(maxNum,num)
    }
    return maxNum
}

//Test
val a=10
val b=15
val c=5
val largest=max(a,b,c)

vararg关键字允许方法接收任意多个同类型的参数。

Java 中,所有类型的数字都是可以比较的,因此必须实现Comparable接口,这个规则在Kotlin中同样成立。那么可以借助泛型,将max()函数修改成接收任意多个实现Comparable接口的参数。

fun <T:Comparable<T>> max<vararg nums:T>:T{
    if(nums.isEmpty()) throw RuntimeException("Params can not be empty")
    var maxNum=nums[0]
    for(num in nums){
        if(num>maxNUm){
            maxNum=num
        }
    }
    return maxNum
}

//Test
val a=3.5
val b=3.8
val c=1.4
val largest=max(a,b,c)

这里将泛型T的上界规定为了Comparable<T>,那么参数T就必然是Comparable<T>的子类型。

简化Toast

fun String.showToast(context:Context,duration:Int=Toast.LENGTH_SHORT){
    Toast.makeText(context,this,duration).show()
}
fun Int.showToast(context:Context,duration:Int=Toast.LENGTH_SHORT){
    Toast.makeText(context,this,duration).show()
}

//Test
"this is toast".showToast(context)
R.string.app_name.showToast(context)

简化Snackbar

//原
Snackbar.make(view,"This is Snackbar",Snackbar.LENGTH_SHORT).setAction("Action"){
    //处理逻辑
}.show()

//简化
fun View.showSnackbar(text:String,actionText:String?=null,
                      duration:Int=Snackbar.LENGTH_SHORT,block:()->Unit)?=null{
    val snackbar=Snackbar.make(this,text,duration)
    if(actionText!=null&&block!=null){
        snackbar.setAction(acitonText){
            block()
        }
    }
    snackbar.show()
}
fun View.showSnackbar(resId:Int,actionText:String?=null,
                      duration:Int=Snackbar.LENGTH_SHORT,block:()->Unit)?=null{
    val snackbar=Snackbar.make(this,resId,duration)
    if(actionText!=null&&block!=null){
        snackbar.setAction(acitonText){
            block()
        }
    }
    snackbar.show()
}
//Test
view.showSnackbar("this is Snackbar","Action"){
    //处理逻辑
}

DSL

DSL的全称是领域特定语言,它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。

class Dependency {
    val libraries=ArrayList<String>() //保存所有依赖库
    
    fun implementation(lib:String){ //用于向List集合中添加依赖库
        libraries.add(lib)
    }
}

//定义一个高阶函数
fun dependencies(block:Dependency.()->Unit):List<String>{
    val dependency=Dependency()
    dependency.block()
    return dependency.libraries
}

dependencies函数接收一个函数类型参数,并且该参数是定义到Dependency类中的,因此调用它的时候需要创建一个Dependency的实例,然后再通过该实例调用函数类型参数,这样传入的Lambda表达式就能得到执行。最后将Dependency类中保存的依赖库集合返回即可。

经过这样的DSL设计之后就可以使用如下语法结构了:

dependencies{
    implementation("com.squareup.retrofit2:retrofit:2.6.1")
    implementation("com.squareup.retrofit2:converter-gson:2.6.1")
}
class Td {
    var content=""
    
    fun html()="\n\t\t<td>$content</td>"//<td>表示一个单元格,其中必然是要包含内容
}

class Tr {
    private val children=ArrayList<Td>()

    fun td(block:Td.()->String){
        val td=Td()
        td.content=td.block()
        children.add(td)
    }

    fun html():String{
        val builder=StringBuilder()
        builder.append("\n\t<tr>")//<tr>表示表格的行,可以包含多个<td>标签
        for (childTag in children) {
            builder.append(childTag.html())
        }
        builder.append("\n\t</tr>")
        return builder.toString()
    }
}

class Table {
    private val childern=ArrayList<Tr>()
    
    fun tr(block:Tr.()->Unit){
        val tr=Tr()
        tr.block()
        childern.add(tr)
    }
    
    fun html():String{
        val builder=StringBuilder()
        builder.append("<table>")
        for (childTag in childern) {
            builder.append(childTag.html())
        }
        builder.append("\n</table>")
        return builder.toString()
    }
}

Tr类中提供了一个td()函数,它接收一个定义到Td类中并返回值是String的函数参数类型。当调用td()函数时,会先创建一个Td对象,接着调用函数类型参数并获取它的返回值,然后赋值到Td类的content字段中,这样就可以将调用td()函数时传入的Lambda表达式的返回值赋值给content字段了。既然创建了一个Td对象,就一定要将它添加到children集合中。

Tr类中也定义了了一个html()方法,它的作用时将所有子Td拼接到<tr>标签当中。

最后就可以如下调用了:

val table=Table()
table.tr {
    td { "Apple" }
    td { "Grape" }
    td { "Orange" }
}
table.tr {
    td { "Pear" }
    td { "Banana" }
    td { "Watermelon" }
}

继续精简:

fun table(block:Table.()->Unit):String{
    val table=Table()
    table.block()
    return table.html()
}

//Test
val html= table {
    tr {
        td { "Apple" }
        td { "Grape" }
        td { "Orange" }
    }
    tr {
        td { "Pear" }
        td { "Banana" }
        td { "Watermelon" }
    }
}

还可以利用Korlin其他语法特性:

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