Kotlin要点笔记

2023-03-05
11分钟阅读时长

实际上学习kotlin的动力一直不足,因为java is enough. 不过最近打算写一个QQ机器人玩,现在流行的框架是基于kotlin的,虽然可以用HTTP API进行桥接,但是这样部署太麻烦,而且性能有影响,索性简单的学习一下kotlin,然后直接扩展。用的教材是阿里的《kotlin核心编程》。

语法要点

  1. var声明变量,val声明常量(即java里面的final,注意不是immutable),格式类似golang,除了类型前面要加冒号;

  2. val声明的变量可以不立即赋值;

  3. 支持通过fun定义函数,也支持函数闭包,格式也类似golang,不过返回类型之前要加:

  4. 类似C++,函数支持默认参数;

  5. 函数类型声明的格式和函数不一样,返回类型之前要用->;特别的,没有返回值也要写->Unit

  6. 返回值同样可以是函数,所以这个格式有点像haskell,如(Int) ->((Int)->Unit),可以简化为(Int)->(Int)->Unit,这样就更像Haskell了;

  7. 通过class::func_memeber_name来引用函数或者成员变量;

  8. lambda表达式的格式非常类似函数声明:func f (x:Int,y:Int)->Int={x,y->x+y},或者更简单点val f ={x:Int,y:Int: x+y};即用{}括起来的表达式;

  9. 单个参数可以不声明,隐式的为it

  10. func与lambda的区别:

    1. fun在没有等号,只有花括号时,就是最常见的函数实现,必须带return;
    2. fun有等号,没有花括号时,表示单表达式函数体,此时无须带return;
    3. 如果是等号同时有花括号,无论使用val还是fun,都表示lambda表达式;
    4. lambda可以定义接收者,语法是val sum: Int.(int) -> Int = {other->plus(other)};
  11. kotlin的闭包是可以修改外部变量的,比java合理;

  12. 如果一个函数只有一个参数,且参数是函数,则调用该函数时,无须传入外层的括号:

func t(block: ()->Unit){
    block()
}
t{
    println("hello world")
}
  1. 如果一个函数有多个参数,最后一个参数是函数时,也可以省略最后一个参数直接传入内容,如:
func t(s:String, block:(String)->Unit){
    block(s)
}
t("hello world"){
    s->println(s)
}
  1. 与C++类似,kotlin中很多流程控制语句是一个表达式,即会返回值,所以他可以用类似Python的方式实现3目运算:val t = if(x>3) 5 else 7
  2. kotlin设计Unit代替void,就是为了让所有函数调用都有返回值(即成为表达式);
  3. 枚举是类,语法有点像C++11:enum class Day{}; 如果要加方法的话,最后一个枚举要加一个;
  4. kotlin支持模式匹配,语法有点像rust,关键词是when,当然它也是一个表达式:
val k = when(day){
    Day.MON -> 1
    Day.THU -> 2
    else -> 3
}
  1. 内置range表达式,类似rust,如for (k in 1..10 step 2),倒着用10 downTo 1 step -2;全闭区间;
  2. 如果想用左闭右开区间,用1 until 10
  3. 数组遍历,如果想带上下标,用array.withIndex(),类似Python的enumerate
  4. 支持中缀函数,类似Haskell,形式是infix func。声明条件比较苛刻:
    1. 必须是成员函数或者扩展函数,这样函数左侧的对象是确定的
    2. 只能有一个参数,即函数右侧的参数
    3. 该参数不能有默认值
  5. kotlin的可变参数用vararg来表示,比较特别的是,它不需要一定是最后一个参数;有点像Python的(*args, **kwargs)
  6. kotlin的字符串支持下标访问;支持三引号(类似Python);支持字符串模板(类似bash);
  7. 支持==判断结构相等(相当于java里面的equal),===判断引用相等(相当于java的==);
  8. kotlin支持内联函数,但是设计的很复杂,一般情况下尽量不要使用,除了泛型注入类型;
  9. inline可以让lambda表达式非局部返回,即直接return父函数的结果;语法糖是return @func

面向对象

上面可以看出kotlin实际上是支持函数式编程的,所以不是纯OO语言,但是也支持类。

  1. 默认函数是public的,但是默认成员是private的;
  2. 所有变量必须显式初始化;
  3. 接口中的属性不能直接复制,需要写成val c get()=100而不是val c =100
  4. 声明对象无须new,类似Python;
  5. 可以使用init函数块来进行初始化,属于构造函数的一部分,而且可以有多个;
  6. 构造函数可以放在类名后面声明:
class Bird(weight: Double, age: Int, color: String="blue"){
    val weight = weight
    val age = age
    val color: String
    init{
        this.color = color
    }
}
  1. 延迟初始化:不可变成员的初始化延迟到对象被创建出来之后,如:
val color: String by lazy{
    if(age > 10) "blue" else "green"
}

默认线程安全,仅能用来初始化不可变变量;第一次被调用时,才会被初始化。

  1. 对于var变量,可以用lateinit来延迟初始化,但是不能用于基础数据类,只能用包装类:
lateinit var color: String
  1. 可以通过constructor显式声明构造方法,这被称为从构造方法,主构造方法就是类名后面那个,语法类似C++的初始化列表:
constructor(other: Some)this(some.color){
    //...
}
  1. 继承的语法类似C++而不是java,用:,默认类并不能被继承,需要加上open关键字;
  2. kotlin的protect不能被package访问,而是使用关键字internal
  3. sealed表示密封类,它其实是枚举的一种扩展:当值为有限情况时,主要用于状态匹配;
  4. kotlin一个文件里可以定义多个类,如果用private修饰类,表示类的作用域就是当前文件;
  5. 指定某个具体的父类:super<Base>.func()
  6. kotlin的接口可以定义成员,覆盖的时候同样要带上overrite
  7. 如果要在kotlin中声明一个内部类,必须加上inner修饰符。这和java的设计是相反的:java需要加上static关键字才是嵌套类,默认就是内部类;
  8. 内部类可以访问外部类的成员,嵌套类则是完全独立的;可以用内部类解决多重继承问题;
  9. 类似C#,kotlin支持委托,语法上就是上面提到的by
class Bird(flyer: Flyer, animal: Animal): CanFly by flyer, CanEat by animal{}

其实有点像组合,或者Go里面的结构体嵌套;

  1. kotlin支持数据类,类似lombok中的@Data注解,语法是data class,java后续版本中有record关键字;
  2. kotlin支持自动解包,类似val(a,b,c)=o1;数组、数据类都支持自动解包;
  3. 内置PairTriple
  4. 移除了static关键字,但是引入了object关键字;
  5. 引入伴生对象概念,就是属于类的对象,使用companion object+花括号声明,类似java中的所有static变量+static初始化;
  6. 使用object可以直接声明单例;
  7. 类似Haskell,kotlin引入了ADT的概念,这主要通过sealed class实现,配合模式匹配和解构来使用;
  8. kotlin的when可以嵌套;
  9. kotlin的类型默认都是非空的,需要加上?来表示可为null,如var k: String?
  10. 使用这些变量时,也可以在后面加上?表示非空判定(类似java里面的Optional,Swift和Rust里面有类似的设计);默认值使用?:来赋予(可以使用表达式);
  11. 非空断言:!!,断言失败抛出NPE异常;
  12. 使用isas来做类型判断和转换,这个语法和Rust也很类似;
  13. 可以用as?来尝试转换,失败则返回null;
  14. 所有类型的父类是Any;java中所有的对象转到kotlin都被视为平台类型,即不知道是否非空的;
  15. Nothing是没有实例的类型,位于最底层,实际上只能为null;
  16. 可以对类的实例进行方法扩展,语法是:
fun ClassA.toJson():String{
    
}

扩展本质上注入静态方法;

  1. kotlin支持运算符重载,函数名有点像Python,语法:
operator func ClassA.plus(rhs:ClassA):classA{
    
}
  1. 同名的情况下,扩展方法的优先级低于类成员;
  2. 当在扩展函数里调用this时,指代的是接收者类型的实例;如果想要强行指定类的实例,使用this@ClassA的语法;
  3. 扩展函数始终是静态调度,不支持多态(因为是静态实现);
  4. 标准库中有with, apply, run, let, takeIf等扩展函数;其中let一般用于非空判断的执行逻辑;
  5. 不要滥用扩展功能;

数据结构

  1. 不含有内置数组,直接用Array类,使用arrayOf<>来生成;建议优先使用intArrayOf之类的特化,性能更好;
  2. 一般的集合和java中一样,只是分为可变和不可变两种了;默认是不可变的,Mutable前缀的才是可变的;
  3. mapOf(1 to 2),这里的to分割键值对;
  4. 直接使用函数式接口处理数据会创建很多临时数据结构(因为函数式编程讲究的是不可变);需要用asSequence()转成序列再处理;
  5. sequence是惰性求值的,类似Haskell中的设计。当然java中的stream也是惰性求值的;
  6. 因为支持惰性求值,所以也支持无限长序列,如val naturalNum=generateSequences(0){it + 1}

泛型编程

  1. kotlin泛型的语法和java区别不大,只是类型约束这里用:而不是extend
  2. 可以使用内联函数避免泛型擦除,语法是:
inline fun<reified T> getType(){
    return T::class.java
}

这种函数无法在java中被调用;

  1. 如果在定义的泛型类和泛型⽅法的泛型参数前⾯加上out关键词,说明这个泛型类及泛型⽅法是协变,简单来说类型A是类 型B的子类型,那么Generic<A>也是Generic<B>的⼦类型;
  2. 关键词in用于实现泛型逆变;作用和上面正好相反;

元编程

对于kotlin而言,由于不支持宏或者模板,剩下能讨论的元编程其实就只剩反射了。

  1. 反射这块的内容用的时候再查,这里不做记录,因为平时用的不多;值得一提的是kotlin有个KClass,里面是kotlin特有的类型;
  2. 使用annotation创建注解,用法和java类似;

异步支持

  1. kotlin支持协程,且协程的设计比较复杂;

  2. 使用runBlocking会阻塞等待作用域内的协程执行完毕,一般用于main函数;

  3. 使用coroutineScope关键字创建协程作用域,在所有已启动子协程执行完毕之前该作用域不会结束;

  4. 使用GlobalScope创建全局作用域;

  5. 使用launch在上面创建的作用域里创建一个新协程;

  6. launch内的函数可以单独提取出来,但是需要增加suspend关键字;

  7. launch返回一个Job对象,可以被取消掉;

  8. 但是无法强制取消,必须在代码里做取消检测(如检测isActive);

  9. 一般需要在launch代码块里进行finally检测,从而释放资源;

  10. 注意finally的代码不一定能执行完毕(主要是不能调用delay),如果需要确保这一点,使用withContext(NonCancellable)来强制保证;

  11. 一般取消协程是为了避免超时,在最外围加上withTimeout或者withTimeoutOrNull即可;

  12. 除了launch之外,还可以使用async启动协程,与前者不同的是,它返回一个Deferred,类似其他语言中的promise,可以使用.await等待结果;

  13. 换句话说launch有点类似返回void的函数,async则是有返回值的函数;

  14. launch会立刻启动,但是async可以加上(start = CoroutineStart.LAZY)改为惰性启动,即需要手动调用.start来启动;

  15. 很容易看出,kotlin与go协程的区别之一,就是协程作用域的设计。协程作用域主要是为了将相关工作分组;

  16. 协程作用域中某个协程抛出异常,会导致其他协程立刻被取消;当一个父协程被取消的时候,所有它的子协程也会被递归的取消;一个父协程总是等待所有的子协程执行结束,无须显式join;

  17. kotlin并不推荐直接是使用全局async函数(像其他语言那种设计),即GlobalScope.async,因为全局作用域中的协程并不满足上面那条规则;

  18. 使用协程作用域+协程,被称为结构化并发;

  19. asynclaunch都可以显式指定调度策略,这包括:

    1. 不传参数:从启动它的coroutineScope中继承;
    2. Dispatchers.Unconfined,非受限调度器,可能会使用不同的线程来调度协程中不同的部分(每当遇到被挂起的调用,就视为一个新的部分),高级特性,一般不会使用;
    3. Dispatchers.Default,默认调度器,使用共享的线程池;
    4. newSingleThreadContext("MyOwnThread"),启动一个新线程;
  20. 给jvm加上-Dkotlinx.coroutines.debug的参数,打印日志时就会打印出协程的名称,方便调试;

  21. 可以给async或者launch加上CoroutineName参数来给协程命名;

  22. async/launch加多个参数,需要使用+来连接,例如:launch(Dispatchers.Default + CoroutineName("test"))

  23. kotlin支持异步流,将函数返回值改为Flow,然后通过emit逐个抛出数据;调用端使用collect收集值;

  24. kotlin支持Channel抽象,用法很像go中的channel;

  25. kotlin内置了actor支持,但是官方已经不再推荐使用;

  26. 有类似go的select支持,但是目前还是实验性质的;

总的来说,相比于go,kotlin的协程设计的有点过于复杂了,废案了好几次。所以设计一门语言还是没那么容易的。

Avatar

个人介绍

兴趣使然的程序员,博而不精,乐学不倦