「码上开学——hencoder」Kotlin笔记(Unit 为啥还能当函数参数?面向实用的 Kotlin Unit 详解)

79 阅读6分钟

很多从Java转到Kotlin的人都会有一个疑惑:为什么Kotlin没有沿用Java的void关键字,而要引用这个叫Unit的新东西?

//Java
public void sayHello() {
    System.out.println("Hello!");
}
// Kotlin
fun sayHello(): Unit {
    println("Hello!")
}

不过这个问题一般也不会维持很久,因为就算你不明白,好像……也不影响写代码。

直到这两年,大家发现Compose的官方示例代码里竟然有把Unit填到函数参数里的情况:

LaunchedEffect(Unit) {
    xxxx
    xxxxxx
    xxx
}

我们才觉得:「啊?还能这么写?」

Unit的本质

今天来讲一讲Unit这个特殊的类型。

我们在刚学Kotlin的时候,就知道Java的void关键字在Kotlin里没有了,取而代之的是一个叫做Unit的东西:

// Java
public void sayHello() {
    System.out.println("Hello!");
}
// Kotlin
fun sayHello(): Unit {
    println("Hello!")
}

而这个Unit,和Java的void其实是不一样的。比如Unit的返回值类型,我们是可以省略掉不写的:

// Kotlin
fun sayHello() {
    println("Hello!")
}

不过省略只是语法上的便利,实际上Kotlin还是会把它理解成Unit

Unit和Java的void真正的区别在于,void是真的表示什么都不返回,而Kotlin的Unit却是一个真实存在的类型:

public object Unit {
    override fun toString() = "kotlin.Unit"
}

它是一个object,也就是Kotlin里的单例类型或者说单例对象。当一个函数的返回值类型是Unit的时候,它是需要返回一个Unit类型的对象的:

// Kotlin
fun sayHello() {
    println("Hello!")
    return Unit
}

只不过因为它是个object,所以唯一能返回的值就是Unit本身。

另外,这一行return我们也可以省略不写。

// Kotlin
fun sayHello() {
    println("Hello!")
}

因为就像返回值类型一样,这一行return,Kotlin也会帮助我们自动加上:

// Kotlin
fun sayHello(): Unit {
    println("Hello!")
    return Unit
}

这两个Unit是不一样的,上面的Unit是个类型,下面的是Unit这个单例对象,它俩长得一样但是是不同的东西。注意了,这个并不是Kotlin给Unit的特权,而是object本来就有的语法特性。你如果有需要,也可以用同样的格式来使用别的单例对象,是不会报错的:

object Rengwuxian

fun getRengwuxian(): Rengwuxian {
    return Rengwuxian
}

包括你也可以这样写

val unit: Unit = Unit

也是一样的道理,等号左边是类型,等号右边是对象——当然这么写没什么实际作用啊,单例你就直接用就行了。

所以在结构上,Unit并没有任何的特别之处,它就只是一个Kotlin的object而已。除了对函数返回值类型和返回值的自动补充之外,Kotin对它没有再施加任何的魔法了。它的特殊之处,更多的是在于语义和用途的角度:它是个由于官方规定出来、用于「什么也不返回」的场景的返回值类型。但这只是它被规定的用法而已,而本质上它真就是个实实在在的类型。也就是在Kotlin里,并不存在真正没有返回值的函数,所以「没有返回值」的函数实质上类型都是Unit,而返回值也都是Unit这个单例对象,这是Unit和Java的void的本质上的不同。

Unit的价值所在

那么接下来的问题就是:这么做的意义在哪?

意义就在于,Unit去掉了无返回值的函数的特殊性,消除了有返回值和无返回值的函数的本质区别,这样很多事做起来就会更简单了。

例:有返回值的函数在重写时没有返回值

比如?

比如在Java里面,由于void并不是一种真正类型,所以任何有返回值的方法在子类里的重写方法也都必须有返回值,而不能写成void,不管你用不用泛型都是一样的:

public abstract class Maker {
    public abstract Object make();
}

public class AppleMaker extends Maker {
    // 合法
    @Override
    public Apple make() {
        return new Apple();
    }
}

public class NewWorldMaker extends Maker {
    //非法
    @Override
    public void make() {
        world.refresh();
    }
}

image.png

public abstract class Maker<T> {
    public abstract T make();
}

public class AppleMaker extends Maker<Apple> {
    // 合法
    @Override
    public Apple make() {
        return new Apple();
    }
}

public class NewWorldMaker extends Maker<void> {
    //非法
    @Override
    public void make() {
        world.refresh();
    }
}

image.png

你只能去写一行return null来手动实现接近于「什么都不返回」的效果:

public class NewWorldMaker extends Maker {
    @Override
    public Object make() {
        world.refresh();
        return null;
    }
}

image.png

而且如果你用的是泛型,可能还需要用一个专门的虚假类型来让效果达到完美:

public class NewWorldMaker extends Maker<Void> {
    @Override
    public Void make() {
        wrold.refresh();
        return null;
    }
}

image.png

而在Kotlin里,Unit是一种真实存在的类型,所以直接写就行了:

abstract class Maker {
    abstract fun make(): Any
}

class AppleMaker: Maker() {
    override fun make(): Apple {
        return Apple()
    }
}

class NewWorldMaker: Maker() {
    override fun make() {
        world.refresh()
    }
}

这就是Unit的去特殊性——或者说通过性——所以我们带来的便利。

例:函数类型的函数参数

同样的,这种去特殊性对于Kotlin的函数式编程也提供了方便。一个函数的函数类型的参数,在函数调用的时候填入的实参,只要符合声明里面的返回值类型,它是可以有返回值,也可以没有返回值的:

fun runTask(task: () -> Any) {
    when (val result = task()) {
        Unit -> println("result is Unit")
        String -> println("result is a String: $result")
        else -> println("result is an unknown type")
    }
}

...
runTask { } // () -> Unit
runTask { println("完成!") } //() -> String
runTask { 1 } // () -> Int

Java不支持把方法当做对象来传递,所以我们没法跟Java做对比;但如果Kotlin不是像现在这样用了Unit,而是照抄了Java的void关键字,我们就肯定没办法这样写。

小结:去特殊化

这就是我刚才所说的,对于无返回值的函数的「去特殊化」,是Unit最核心的价值。它相当于是对Java的void进行了缺陷修复,让本来有的问题现在没有了。而对于实际开发,它的作用是属于润物细无声的,你不需要懂我说的一大堆东西,也不影响你享受Unit的这些好处。

延伸:当做纯粹的单例对象来使用

比如,知道Unit是什么之后,你就能理解为什么他能作为函数的参数去被使用。

Compose里的协程函数LaunchedEffect()要求我们填入至少一个key参数,来让协程在界面状态变化时可以自动重启。

LaunchedEffect(key) {
    xxxx
    xxxxxx
    xxx
}

而如果我们没有自动重启的需求,就可以正在参数里填上一个Unit

LaunchedEffect(Unit) {
    xxxx
    xxxxxx
    xxx
}

因为Unit是不变的,所以把它填进参数里,这个协程就不会自动重启了。这招用的非常方便,Compose的官方示例里也有这样的代码。不过这个和Unit吱声的定位已经无关了,而仅仅是在使用它「单例」的性质。实际上,你再括号里把它换成任何的常量,效果都是完全一样的,比如true、比如false、比如1、比如0、比如你好,都是可以的。所以如果你什么时候想「随便拿个对象过来」,或者「随便拿个单例对象过来」,也可以使用Unit,它和你自己创建一个object然后去使用,效果是一样的。

版权声明

本文首发于:Unit 为啥还能当函数参数?面向实用的 Kotlin Unit 详解

微信公众号:扔物线