[译]Kotlin珍品 7 -Unit,Nothing,Any,null

4,265 阅读9分钟

翻译说明: 翻译水平有限,文章内可能会出现不准确甚至错误的理解,请多多包涵!欢迎批评和指正.每篇文章会结合自己的理解和例子,希望对大家学习Kotlin起到一点帮助.

原文地址: [Kotlin Pearls 7] Unit, Nothing, Any (and null)

原文作者: Uberto Barbini

如何利用kotlin的特殊类型

前言

如果你正好浏览了这篇文章,应该可以明显看出我是真的很喜欢kotlin这门语言.其中一个原因是它的类型层次结构,它既非常容易使用,又非常强大.它还非常接近Java类型系统,因此互操作性非常好.

想完全掌握kotlin的类型层次结构的话,搞懂下面的几种类型是如何工作的很有必要.

下面我们先详细的看一下Unit,Noting,Any并跟它们相应的Java类做个对比.

最后我们再思考一下,null,和以?结尾的类型是如何适配到这些类型的.

正文

  • Unit

    在kotlin中Unit类型相当于Java中的void.
    或者如果你更喜欢这样理解的话,我们说在Kotlin中Unit是所有没有指定返回值函数(方法)的返回值.(例如println())

    fun whatIsLove(){
      println("Baby don't hurt me!")
    }
    

    在Kotlin中我们有两种定义方法或者函数的方式:如果把上面的语法转成Java,那么就是声明方式;如果不转在Kotlin中那么就是表达式.

    如何区分函数声明和函数表达式是很重要的基础知识,搞清两者的关系和区别有助于我们准确的阅读别人的文章和描述我们的代码!


    我们可以重写所有不带返回值的函数声明为返回`Unit`的函数表达式:

    因为Kotlin中的方法如果不指定返回值的话也会至少有一个Unit返回值,所以所有方法都是表达式.那么我们在Kotlin中指出表达式的时候,大部分都是下面这种单行表达式
    fun whatIsLove(): Unit = println("Baby don't hurt me!")

    通常表达式的可读性是优于声明的,至少保证了方法代码的长度.不论怎么说方法代码太长都不太可取.
    在Kotlin标准库中 Unit被简单的定义为一个被object关键字修饰的Any的子类,带着一个重载的toString()方法.

    在Kotlin中Any是类层次结构的顶级类,是所有类的父类.

    被object修饰符修饰的类会在类的初始化生成器中创建一个静态的全局单例.

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

    在Kotlin和Java混合开发的时候,Kotlin编译器足够聪明,可以把任何返回void的结果值转成Unit,对一些特殊的函数类型也是一样支持.

    例如我们可以传递一个返回类型为Consumer<Integer>的java lambda给接受一个(Int) -> Unit类型参数的函数使用.

    //Java
    public static Consumer<Integer> printNum = x -> System.out.println("Num " + x);
    //Kotlin
    fun callMe(block: (Int) -> Unit): Unit = (1..100).forEach(block)
    fun main(){
        callMe { Lambdas.printNum } //it works fine
    }
    

    注意一个容易让人迷惑的地方,在Java中还有Void类(注意是大写)间接的关联着关键字void并且这个类不能实例化.所以null是这个返回类型的唯一有效的值.

  • Nothing

    这里需要你们打起精神来了,因为Nothing是这篇文章最复杂的一段.

    Nothing是一个其他所有类的子类(包括final类)的class(不是一个object).但是有一个陷阱:它只有一个私有构造函数,所以无法实例化Nothing对象.这个类的注释也说的很清楚了.Nothing没有实例,我们可以用Nothing去表示一个不存在的值.例如,如果一个方法返回Nothing,那么就代表永远不会返回(永远throws一个异常).

    public class Nothing private constructor()
    

    在电脑编程中,不管是何种返回类型,都有可能不返回任何值.因为方法可能出错或者陷入一个无限的计算中.而Kotlin通过Nothing类型,让这个特性更明确易懂.

    现在让我们看看Nothing有用的场景.

    • 第一个有用的地方就是TODO()函数.当我们还不想明确一个实现的时候,这个方法就格外有用了.
      public inline fun TODO(): Nothing = throw NotImplementedError()

      你是否曾经想过它如何替代任何可能的返回类型?

      它之所以起作用,是因为Nothing是任何类的子类型。

      fun determineWinner(): Player = TODO() 
      //It compiles because Nothing is a subclass of Player
      

      注意在Java中我们这样写的话,是无法通过编译的

      static Void todo(){
        throw new RuntimeException("Not Implemented");
      }
      String myMethod(){   
          return todo(); //无法编译因为Void不是一个String
      }
      
    • 第二个有用的地方是代表所有的空集合
      fun findAllPlayers(): List<Player> = emptyList()

      Kotlin标准库中的EmptyList怎么就成了List<Player>有效的返回类型了呢?让我们看一下实现来解答这个疑问.

      public fun <T> emptyList(): List<T> = EmptyList 
      //使用类型推断的泛型函数
      object EmptyList : List<Nothing> ... //最终的协变
      

      你们可能已经猜到了,因为Nothing类型的关系让上面的方法成立.同理,emptyMap(),emptySet()也都是一样的.

    • 第三个地方可能没有上面两个地方有代表性,但是仍然很有用!

      假定你有一个可能会失败的方法,例如从一个数据库中读取用户的操作.

      当一个用户不存在的时候,既简单又合理的一个选择是返回一个null.方法签名差不多会想下面这样:

      fun readUser(id: UserId): User? = …

      但是有些时候我们希望拿到更多的错误信息,比如说到底是哪里出错了(链接失败?表不存在?等等),然后让调用者根据不同的错误去做不同的逻辑处理.
      这个时候Nothing就可以出来完美的处理这个需求了.一个聪明的方法是提供一个可能发生错误的回调,并返回Nothing.

      inline fun readUser(id: UserId, onError: (DbError) -> Nothing): User = …

      它是如何工作的?让我们看下面的完整例子:

      fun createUserPage(id: UserId): HtmlPage {
          val user = readUser(id) { err ->
              when (err) {
                  is InvalidStatement -> //错误一的处理
      return@createUserPage throw Exception(err.parseError)
                  is UserNotFound -> //错误二的处理
      return@createUserPage HtmlPage("No such user!")
              }
          }
          return HtmlPage("User ${user.name}") //没报错的情况
      }
      

      lambda表达式中不允许使用不带标签的return语句(因为lambda中return是退出外围的函数,这叫non-local return(非局部返回),但如果lambda是传入到一个inline函数中,则可以return.这个示例中,readUser是一个内联函数.

      最后让我们通过一个例子来帮助我们区分UnitNothing:

      fun fooOne(): Unit {
          while (true) {
          }
      }
      
      fun fooZero(): Nothing {
          while (true) {
          }
      }
      //两个方法都能通过编译
      
      fun barOne(): Unit {
          println("hi")
      }
      
      fun barZero(): Nothing {
          println("hi")
      }  //error
      //最后的方法编译报错
      

      Nothing是Unit的子类,所以上面两个都能编译通过.而最后一个不行.

  • Any

    在Kotlin中Any是顶层层级.所有其他的类型都继承与它.

    在Any类的注释中写到:"Kotin类层级的顶层类.是所有Kotlin class的父类."

    它跟Java里面的Object很像,但是方法签名要精简一点:

    public open class Any {
        public open operator fun equals(other: Any?): Boolean
        public open fun hashCode(): Int
        public open fun toString(): String
    }
    

    Any一比,java.lang.Object有11个方法,其中还有5个是同步方法.(wait,notify,notifyAll).这是一个后续版本明显可以优化的地方.可能在之前的时候看起来是个好主意,但是现在让同步原语在任何可能的对象上就不是那么必要了.

    看一下Any类的代码,我们发现它是Kotlin标准库中几个不多被open修饰的类.这意味着我们可以直接继承它.open在这也是必要的,因为所有Kotlin class会自动继承Any,也就是说我们不显示的指定父类的时候也会默认继承Any.

    在JVM层,并不存在Any类型,编译器会把它转成java.lang.Object.生成下面的字节码,即使你不太熟悉这种格式,你也能发现参数的实际类型(加粗的).

    fun whatIcanDoWithAny(obj: Any){
        obj.toString()
    }
    
    public final static whatIcanDoWithAny(Ljava/lang/Object;)V
        // annotable parameter count: 1 (visible)
        // annotable parameter count: 1 (invisible)
        @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
       L0
        ALOAD 0
        LDC "obj"
        INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V
       L1
        LINENUMBER 5 L1
        ALOAD 0
        INVOKEVIRTUAL java/lang/Object.toString ()Ljava/lang/String;
        POP
    

    所以即使我们在Kotlin中不能直接调用Any中缺失的方法(比如说notify.wait这些),但是实际上所有Kotlin的对象都是有这些方法的.

    这让传递任何Kotlin的对象,给任何需要Object类型参数的java方法当参数,变为可能.

    最后,注意一个地方.Any类实现了 Java Object 类里面的 equals 操作符.所以,equals == 是Kotlin中唯一需要被重载的操作符并且你不能改变方法签名.例如,返回一个不一样的类型来代替Boolean.

  • null

    null放在最后说是因为它很特殊.因为它并不是一个类型但又可以跟任意类型合用(类型后面加上?).

    String?就是一个组合类型,String+null.所以我们知道这是一个可空的String类型.通过这种方式,在Koltin的智能转换和特殊操作符的帮助下,我们可以轻松的处理空安全问题.

    那么上面的提到的特殊类型呢?Unit?Nothing?Any?这些也都是可以用的:

    • Unit? 允许返回一个声明 或者 null.我不知道它有什么有趣的用途.
    • Nothing? 比较特殊 因为这是让一个方法只能返回 null 的返回类型.据我所知,目前还不知道这个的实际用途.
    • Any? 这个就很重要了.因为它相当于Java的Object,并且在Java是可以返回null的.还有当你声明一个泛型类,假设MyClass<T>,跟Java一样,这个T类型的隐示约束就是Any?.如果你想限制这个泛型类不能为可空类型,你就必须显示的指定:MyClass<T:Any>

    下面是一个用扩展方法重写的map和flatmap方法,让它们可以支持所有kotlin中可空的类型

    fun <A:Any, B:Any> A?.map(f: (A) -> B): B? = when(this) {
        null -> null
        else -> f(this)
    }
    fun <A:Any, B:Any> A?.flatMap(f: (A) -> B?): B? = when(this) {
        null -> null
        else -> f(this)
    }
    

结尾

完整的代码示例和更多的代码都能在作者的GitHub上浏览下载项目仓库

我希望你们能喜欢这篇文章,如果喜欢的话 请关注原作者或者在掘金关注我并给我点个赞吧.