沉思录| 开篇:Kotlin的「丑与美」,以及「最佳实践」

4,616 阅读9分钟

往期文章:《Kotlin Jetpack 实战:开篇》

你好,我是朱涛。

博客两年没怎么更新了,过去的两年里发生了很多事情:

  • 第一,我成为了一名父亲;
  • 第二,郭霖大佬鼓励了我,让我去申请了GDE,很荣幸,我通过了Google的筛选和面试,成为了Kotlin、Android的GDE。(希望疫情赶紧过去,我还欠大佬一顿饭。)
  • 第三,我从上家公司离职,花了半年写了一个技术专栏《Kotlin编程第一课》,体验了一次自由职业者的感觉。

在撰写技术专栏的过程中,我成长了很多,既有技术方面的成长,也有技术之外的成长。在过去的半年里,我做的最多的事情就是:深度思考。我要想尽一切办法,将自己脑子里的知识掏出来,然后,以最简单、最直观的方式呈现给我的读者。比如说,为了解释清楚Kotlin协程的“挂起和恢复”,我掉了很多头发才找到下面这个灵感:

Kotlin13-3-2.gif

说实话,我写专栏,根本不是为了钱,单纯就是因为喜欢做这样的事情。这半年里,我写作的态度比从前的工作还拼命;半年里挣的钱,还不如我一个月工资。但我乐在其中啊,哈哈!

对我来说,读者对课程的赞扬,就是我快乐的源泉。其实,很多互联网的大佬写博客、写书,也是同理。

关于「沉思录」

其实,在这半年里,我还积累了许多创作灵感,这些灵感,大都非常的琐碎,并不足以成为专栏系统课程的一部分。但这些灵感如果不写出来,就太可惜了。所以,我决定将其取名为:「沉思录」,以博客的形式发出来。

「沉思录」,大约分为三个板块:

  • Kotlin沉思录,记录我在Kotlin领域的思考,包括Kotlin JVM,协程,也包括KMM;
  • Android沉思录,记录我在Android、Jetpack领域的思考,两年前我写的《Kotlin Jetpack实战》还是太浅了;
  • Compose沉思录,自然就是Jetpack Compose领域的思考,它的编程理念,实现原理都非常有趣。

今天,我们主要看看Kotlin沉思录。

Kotlin 沉思录

初学Kotlin的时候,我更多的只是看到了Kotlin的「美」,但经过5年多的沉淀,我也渐渐发现了Kotlin「丑」的一面。

世界上不存在完美的语言,Kotlin也不例外。

其实,一直以来,总有读者问我这样问题:

Kotlin的最佳实践是什么?

对于这样的问题,我的回答总是非常的谨慎,说实话,我不敢仅凭我个人的经验就妄下定论。然而,最近,问我这个问题的人越来越多了,有专栏的读者,也有公司的同事。

接下来,我们就来聊聊具体的思路吧,关于Kotlin的「丑」与「美」。

「丑」与「美」

人们常说,穿衣服,要尽量遮住自己「丑」的部位,尽量凸显自己「美」的部分。我的思路也是类似的,简单来说,就是:扬长避短。

Kotlin的「弱点」

考虑到我GDE的身份,不能大肆宣扬Kotlin的缺点。因此,关于Kotlin「丑」的部分,我会以「弱点」、「坑」为标题。毕竟「丑」与「美」都是非常主观的一种判断,「弱点」则是一种更客观的描述。(没错,我就是比较怂。)

以下是部分主题:

  • 02 | 枚举类的弱点在哪?
  • 03 | Data Class的弱点在哪?
  • 06 | Lambda的弱点在哪?
  • 09 | 扩展函数的弱点在哪?
  • 15 | Channel的弱点在哪?
  • 16 | Flow的弱点在哪?

Kotlin之美

聊完Kotlin的「丑」之后,我们就知道Kotlin的「弱点」还有「坑」在哪了,在平时使用的时候,我们就可以很好的避开它的弱点了。

接下来,我们就可以来看看Kotlin的「美」了,欲扬先抑嘛。

以下是部分主题:

  • 17 | 如何理解Kotlin语法之美?
  • 18 | 如何理解sealed之美?
  • 21 | 如何理解Delegation之美?
  • 23 | 如何理解协程之美?
  • 26 | 如何理解Flow之美?
  • 30 | 如何理解inline之美?

**Kotlin的「最佳实践」到底是什么?**看完上面的这些系列以后,我相信每个读者都会有一个自己的答案吧!

Kotlin其实是一个非常宽泛的话题,除了语法特性层面的「丑」与「美」,其实还有许多能聊的东西,比如:编程范式、DSL设计、数据结构与算法、设计模式,KMM、Compose Multiplatform。这些内容,我都会尝试在这个系列的博客里去做一些涉猎。

关于深度

沉思录这个系列不是面向0基础读者的,这里主要会记录我平时的一些灵感和思考,它更像是不成体系的Android、Kotlin随笔。如果你是有一定经验的开发者,应该能让你有所启发。(不敢保证哈。)

如果你对Kotlin没有一个全面的认识,那我建议你先去看看Kotlin官方文档。如果你觉得官方文档枯燥乏味,也可以去看看我公众号里的历史文章,我自认为讲的还不错。

OK,闲聊结束,我们进入正题。

一个让人「又爱又恨」的特性

我们都知道,Kotlin会为成员属性自动生成getter、setter。不论是使用var、还是使用val,Kotlin编译器都会自动帮我们处理getter、setter的问题。

// 代码段1

class GetterSetterDeepDive {
    // var
    var name: String = "朱涛的自习室"
    // val
    val desc: String = "Kotlin, Android and more."
}

如果将上面的Kotlin代码反编译成Java的话,它大致会长这样:

// 代码段2

public final class GetterSetterDeepDive {

   private String name = "朱涛的自习室";
   private final String desc = "Kotlin, Android and more.";

   public final String getName() {
      return this.name;
   }

   public final void setName(String var1) {
      this.name = var1;
   }

   public final String getDesc() {
      return this.desc;
   }
}

在我初学Kotlin的时候,我曾对着这个案例感叹:Kotlin 666!一行顶十行啊!用Kotlin开发,果然能大大提升效率啊!

说实话,现在回过头来看,当时的我就跟个“脑残粉”一样,大家不要学我。

好了,言归正传,有一说一,Kotlin自动生成getter、setter,它这样的行为,对比Java确实是一大进步,因为它可以避免外部的类直接访问Kotlin类当中的字段(Field)。毕竟,我们都知道,类似下面这样的Java代码是非常不合理的。

// 代码段3

public class FieldJavaBean {
    public String name = "朱涛的自习室";
    public final String desc = "Kotlin, Android and more.";
}

上面的代码,既不符合开放封闭的原则,也难以维护。对比之下,Kotlin生成的代码段2的代码,则要顺眼很多。然而,Kotlin这样的策略其实也有它丑陋的一面。让我们来看看下面的例子。

Getter、Setter的第一个弱点

Kotlin的Getter、Setter主要有两个弱点,我们先来看它的第一个弱点,请看下面的代码。

// 代码段4

class GetterSetterDeepDive {
    var name: String = "朱涛的自习室"
    val desc: String = "Kotlin, Android and more."

    fun printName() {
        println("name = $name,desc = $desc")
    }
}

请问,这段代码的问题在哪?抛开当前文章的语境,如果你是在面试当中遇到这样一个问题,你会给出怎样的答案?

你可以停下来思考一下,心里有了答案以后再继续往后看。

Kotlin最大的问题就在于,大部分的开发者很难意识到Kotlin编译器背后自动生成的那些getter、setter方法,从而导致这个特性被滥用。

让我们将上面的代码反编译来看看。

// 代码段5

public final class GetterSetterDeepDive {

   private String name = "朱涛的自习室";
   private final String desc = "Kotlin, Android and more.";
   // 1
   public final String getName() {
      return this.name;
   }
   // 2
   public final void setName(@NotNull String var1) {
      this.name = var1;
   }
   // 3
   public final String getDesc() {
      return this.desc;
   }

   public final void printName() {
      // 并不会用到getter、setter
      String var1 = "name = " + this.name + ",desc = " + this.desc;
      System.out.println(var1);
   }
}

可以看到,注释1、2、3对应的这三个自动生成的getter、setter方法,它们根本就没被用到!这样的问题,如果是在一个非常小的规模,其实是无伤大雅的,但对于一个大的工程来说,当这样的问题积少成多,就会极大增加方法数,还有应用的包体积。这一点对Android应用尤为重要。

那么,以上的问题该怎么解决呢?答案其实也很简单:

// 代码段6

class GetterSetterDeepDive {
// 变化在这里
//     ↓
    private var name: String = "朱涛的自习室"
    private val desc: String = "Kotlin, Android and more."

    fun printName() {
        println("name = $name,desc = $desc")
    }
}

我们将其反编译成Java看看:

// 代码段7

public final class GetterSetterDeepDive {
   private String name = "朱涛的自习室";
   private final String desc = "Kotlin, Android and more.";

   public final void printName() {
      String var1 = "name = " + this.name + ",desc = " + this.desc;
      System.out.println(var1);
   }
}

这样的 Java 代码看起来是不是干净了很多?这就对了!

反思

Kotlin编译器自动生成Getter、Setter的操作,它对比Java确实是存在优势的,因为它更符合「开放封闭」的原则。我们Kotlin开发者随手写出的var、val,它背后都会转换成Java当中的「最佳实践」,真的很方便。这个设计最精彩的地方在于:「简洁」,简单的var背后代表了:Field + Getter + Setter,这样的信息密度是Java所不能比拟的。不得不佩服Kotlin设计者的精妙构思。

然而,几乎所有的技术都是一种trade-off,Kotlin这样「方便」、「简洁」的设计,让它丢失了许多底层的信息。许多开发者只看到了var的方便,却很容易忽略它底层自动生成的Field + Getter + Setter。Kotlin官方其实也在想办法解决类似这样的问题,但这远远不够。

当我们写出有问题的代码时,IDE其实是会有警告的。可是,它只告诉了我们:desc可以变成private,并没有告诉我们具体的后果。当我们看到这样的提示时,我们脑子里很难将“private”与“Getter、Setter”建立对等的关系。

  • 对于var属性来说,加上private修饰,就意味着方法数减2
  • 对于val属性来说,加上private修饰,就意味着方法数减1

其实,许多事物发展到一定程度,最后都会开始拼细节。考试是如此、商业是如此、编程也是如此。

好,考虑到篇幅限制,我们之后再聊Getter、Setter的第二个弱点吧!我们下期再见!