用Kotlin写Kotlin(一)

1,342 阅读8分钟

用Kotlin写Kotlin(一)

最近在看一些Kotlin相关的书和文档,希望可以不再是用Kotlin写Java,遂整理成笔记。

基础语法

var与val

var用来声明可变变量,val用来声明值变量(实现等同于Java的final修饰的变量)。Kotlin编程的一个原则是尽可能采用val来声明变量,以避免因为变量引用的对象被修改带来的一些副作用。

程序的副作用体现就是代码的每一次执行给相同的输入,但是输出却并不一定相同,副作用的产生往往跟可变数据以及共享状态相关,比如下面的代码:

var a = 0
fun count(x: Int){
    a = a + 1
    println(x + a)
}

当我们多次执行count(1),每一次print出来的都是不一样的值,这就是因为count函数修改了全局共享状态a的值。

但是val有很严格的限制,编程中可变变量是一个非常重要的需求,在没有var的场景下,程序的实现有时候会很困难,比如对一个list迭代求和,有var的时候我们只需要一个临时可变变量记录迭代中累加的和即可,而如果没有var,我们可能需要通过递归,以函数栈来记录此次递归迭代list的值,然后依次累加,这种限制无论从程序编写还是性能以及内存占用上,显然都不如var,因此Kotlin也需要提供var关键字。

函数

函数参数:

普通参数直接正常写。

可变参数使用varargs声明,类似于Java中的...,最终拿到的入参是一个Array<out T>类型的数组。

fun funcMultiParams(vararg params: Int, single: Int) {}
funcMultiParams(1, 2, 3, single = 4)

Java中可变参数只能是最后一个参数,Kotlin不一定,如上,需要指定第四个参数是给single赋值,这样编译器才知道到参数列表中前三个是给可变参数params赋值,最后一个属于single。

函数变量:

函数是Kotlin中的一等公民,函数可以定义在文件中,类中,抑或是方法中,函数本身也是一种数据类型,也可以向普通变量一样传递给另一个函数,或者在函数中被当做返回值返回。

// 定义一个变量functionVar
// 它的类型是一个入参为Int,返回值为Int?的函数
var functionVar: (Int) -> Int?

既然是一个数据类型可以被当做普通对象一样传递和返回,因此Kotlin显然也就支持了高阶函数的能力。

高阶函数

而所谓的高阶函数,指的就是这个函数的参数或者返回值可以是一个函数,Kotlin对高阶函数的支持,赋予了程序设计一种更高级抽象的能力。

在不用高阶函数或者List提供的filter API的情况下,我们如果需要实现对一个list的过滤操作,过滤条件有很多种,最简单的写法就是封装一个函数,对list进行迭代,然后调用各种策略判断是否需要添加到result中:

fun filterList(list: List<String>): List<String> {
    val result = mutableListOf<String>()
    list.forEach {
        if (filterCondition(it)) {
            result.add(it)
        }
    }
    return result
}

fun filterCondition(value: String): Boolean {
    return if (conditionA) {
        true
    } else if (conditionB) {
        true
    } else {
        false
    }
}

那其实上面设计的filterList并不能成为一个通用的工具,因为过滤的策略依赖了filterCondition,诚然可以通过interface的形式,将filterCondition传入filterList中,但是在Kotlin中不需要,我们去看Kotlin对于Collection的系列filter的实现基本上都如下:

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

声明变量predicate: (T) -> Boolean,filterTo在迭代list的过程中调用predicate判断是否符合过滤条件。

函数类型变量的声明方式前文已经讲了,但是我们不可能将所有的函数都声明成变量,一个普通的函数如何以一个变量的形式传递呢,通过::这个操作符,我们可以获取一个对象的函数变量。

class Case {
    fun test() {}
    fun receiveFuncInner(){
        // 当前语义环境已经在this中,可以省略this
        receiveFunc(::test)
    }
}

fun main() {
    val case = Case()
    // 报错1
    receiveFunc(case.test())
    receiveFunc(case::test)
}

fun receiveFunc(func: () -> Unit) {}

报错1的原因很简单,receiveFunc要的是一个函数入参,而test()调用的返回值是Unit类型,类型对不上自然报错。

但是如果我们改造test函数的返回类型为:

fun test():() -> Unit {
		return {}
}

就不会出现类型错误,这个例子是说明函数类型也可以作为返回值,上面我们直接return了一个闭包,等同于return一个()->Unit类型的函数。

那么其实高阶函数的更简化的写法如下:

var highLevelFunc: (Int) -> ((Int)-> Unit)

匿名函数与Lambda

函数不一定要有名字,直接像下面这样使用匿名函数:

// 直接给receiveFunc传入一个匿名函数
receiveFunc(fun() {
})

都已经到匿名函数了,不如直接上Lambda表达式。

我们将匿名函数再简化简化就行了:

fun receiveFunc(func: (Int) -> Int) {}
receiveFunc(fun(param: Int): Int {
		return param + 1
})

去掉函数定义,去掉return,直接将最后一行作为返回值,函数入参通过一种特殊的格式写入函数体:

receiveFunc { param ->
		print(param)
		param + 1
}

如果我们去看上面这个Lambda写法的Java字节码:

receiveFunc((Function1)null.INSTANCE);

发现传入的是一个Function1类型的实例,FunctionX是Kotlin SDK自带的一些interface。

public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

这些interface的目的是为了在Kotlin中可以兼容Java的Lambda表达式。

回到高阶函数中case::test()这种写法,反编译后的字节码如下:

receiveFunc((Function0)(new Function0(var0) {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            ((Case)this.receiver).test();
         }
      }));

一样的是将函数对象转换为一个Function0类型的匿名对象。

我们看到函数对象的实际Java实现是一个Function的接口的实现,这个接口提供的唯一方法是invoke,表示的就是函数调用,所以对于一个函数变量的调用可以是:

fun receiveFunc(func: () -> Unit) {
    func()
    func.invoke()
}

柯里化Currying

Currying指的是把原本接受多个参数的函数转变成一系列仅接收单一函数的过程,通过函数的依次调用获得最终的返回值。

比如我们要实现一个三个数的累加:

fun nocurrying(a: Int, b: Int, c: Int): Int {
    return a + b + c
}
// 下面的函数并不能通过编译,因为函数体位于a的作用域内,bc是无法找到的
fun currying(a: Int): (b: Int) -> (c: Int) -> Int)){
    return a + b + c
}
// 但是可以通过匿名函数
fun currying2(a: Int) = fun(b: Int) = fun(c: Int) = a + b + c
// 既然匿名函数可以,那么借助Lambda能更简便实现:
fun currying(a: Int) = { b: Int ->
    { c: Int ->
        a + b + c
    }
}
// currying的调用就是:
currying(1)(2)(3)

实际上我们还可以借助扩展函数将一个普通函数转化为currying函数,我们上面知道三个参数的函数对应的类型为Function3:

fun nocurrying(a: Int, operation: String, b: Int): Int {
    print("$a $operation $b")
    return 0
}
// 扩展Function3一个currying转换函数
fun <P1, P2, P3, R> Function3<P1, P2, P3, R>.curried() =
    fun(p1: P1) = fun(p2: P2) = fun(p3: P3) = this(p1, p2, p3)
// 调用
::nocurrying.curried()(1)("+")(3)

Unit类型:

如果一个函数没有返回值,那么它的返回值类型就是Unit。

Kotlin中没有所谓的基本数据类型的概念,因此Unit不能等于void关键字,而应该是和Void封装类型去类比。

表达式:

函数是一个表达式,if-else,try-catch等都是一个表达式,表达式和语句的区别是,表达式是有返回值的,更具象的说,右侧表达式的执行结果可以赋值给左侧。

比如:

1 // 返回字面量1
1 + 1 // 返回两个1的和2
if(x > 1) 2 else 1 // if-else表达式
{x:Int -> 2x } // 类型为(Int) -> Int的Lambda表达式
for(x in list){println(x)}// for循环表达式

Kotlin中表达式的能力很像,很多语句都可以转化为表达式的写法,这部分IDE很多情况下都会有智能提示,注意一下就行。

中缀表达式:

中缀表达式是指操作符位于表达式的中间,中间的操作符称之为中缀函数。

之所以特别提到这个,是因为某些情况下,中缀表达式可以让我们的代码写起来更自然优雅。

中缀表达式在Kotlin中的典型比如:

val map = mapOf<Int, Int>(1 to 2)

这里有两个Kotlin封装的语法糖,第一个是通过中缀函数to生成一个Pair对象,第二个是通过mapOf提供一个将Pair对象转换成Map的Key和Value。

来看Pair.to的实现:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

一个通过infix声明的扩展函数,于是乎你不再需要通过Pair的构造函数,而是以一种更自然的写法创建Pair对象。

字符串

字符串的API很多,见文档:

kotlinlang.org/api/latest/…

原生字符串:Java高版本中支持的"""xxx"""这种写法,直接保留字符串的内容格式,不再像原来的字符串拼接,需要去处理换行,制表符等,Kotlin直接支持。

字符串模板:"a的值是$a",这种写法,有效取代Java的通过+进行字符串拼接。

判等:内容判等用==,等价于equals,对象的引用判等用===。

面向对象:

类和对象的声明:

略。

延迟初始化:

val的延迟初始化,可以使用by lazy:

val any:Any by lazy {
    Any()
}

lazy声明如下:

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

actual指明这是一个平台相关的方法,不同平台有着不同的实现,三种模式分别表示线程安全,PUBLICATION表示初始化可以并行执行,返回第一个初始化的对象。NONE表示线程不安全。

通常而言,我们一些字段的初始化如果可以确保在主线程中,可以使用NONE模式,或者封装一层:

fun <T> lazyNone(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE, initializer)

通常而言,var声明一个变量,必须立即赋值,我们可能会写出这种代码,即便notNull是一个不可能为空的对象:

var notNull: Any? = null

如果希望延迟var声明变量的初始化,可以通过Delegates.notNull实现:

var anyy: Any by Delegates.notNull<Any>()

需要注意,在使用anyy之前一定要先进行真正的初始化,否则会抛异常,

可见性修饰符

Internal关键字修饰的资源只允许在模块内可见,模块指的是比如一个gradle project,一个模块内的文件会被一起编译。而Java的defaut可见性实际上允许定义一个同名的包来调用,即便两个类不再同一个包下。

package com.a;
class A {}
// 在另一个module中,声明一个同名package,便可以访问到A
package com.a;
class B {
    A a = null;
}

private修饰的类只在同一个Kotlin文件中可见。

多继承解决思路

Java和Kotlin都不允许多继承,因为多继承导致的菱形继承问题,很容易让代码维护陷入困局,但是有方式可以达到多继承的效果。

继承本质是为了复用父类的能力描述,或者说直接复用父类的能力,那么多继承就是为了复用多个”父类“的能力

那么可以通过下面这些方式间接达到这个目的:

  1. interface的default方法和属性。
  2. 通过组合多个内部类,每一个内部类继承一个父类,组合起来外部类就有类似多继承的能力了。
  3. 通过委托实现。

委托实现举例如下:

interface CanFly {
    fun fly()
}

open class Flyer : CanFly {
    override fun fly() {
    }
}

interface CanEat {
    fun eat()
}

open class Eater : CanEat {
    override fun eat() {
    }
}

class Bird(fly: CanFly, eat: CanEat) : CanFly by fly, CanEat by eat

注意委托只能委托接口,2和3虽然像是实现了多继承,但是更多的像是组合多个”父类“,间接达到多继承的效果。