分类目录归档:Kotlin

5. Kotlin 函数声明与闭包(Closure)

Published / by sickworm / Leave a Comment

今天介绍闭包。闭包也不是新东西了。其实 Kotlin 就基本没有新东西,不,是新语言都基本没有新东西。新语言都是把近些年好用的特性组装起来,再加点自己的见解,因地制宜
一下。

0. 闭包概念介绍

闭包我第一次接触是在 JavaScript 上,函数当作“一等公民”的编程语言都有这个概念。函数是“一等公民”的意思是,函数和变量一样,它是某种类型的实例,可以被赋值,可以被引用。当然函数还可以被调用。变量类型是某个声明的类,函数类型就是规定了入参个数,类型和返回值类型(不规定名字。函数名就和变量名一样,随便起)。如我要声明 Kotlin 一个函数类型,它的入参是两个整数,出参是一个整数,那应该这样写:val add: (Int, Int) -> Int。箭头左边括号内表示入参,括号不可省略。箭头右边表示返回值。

wiki 上闭包的定义是:引用了自由变量的函数,这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。从定义来说,对闭包的理解,是基于普通函数之上的。一般的函数,能处理的只有入参和全局变量,然后返回一个结果。闭包比普通函数更多一点能力,它还捕获了当前环境的局部变量。当然了,捕获局部变量的前提是可以在局部环境里声明一个函数,这只有把函数当作“一等公民”才可以做到。

1. 闭包与匿名类比较

在函数不是“一等公民”的 Java 老大哥这里,匿名类其实就是代替闭包而存在的。只不过 Java 严格要求所有函数都需要在类里面,所以巧妙的把“声明一个函数”这样的行为变成了“声明一个接口”或“重写一个方法”。匿名类也可以捕获当前环境的 final 局部变量。但和闭包不一样的是,匿名类无法修改捕获的局部变量(final 不可修改)。

而匿名类能引用 final 的局部变量,是因为在编译阶段,会把该局部变量作为匿名类的构造参数传入。因为匿名类修改的变量不是真正的局部变量,而是自己的构造参数,外部局部变量并没有被修改。所以 Java 编译器不允许匿名类引用非 final 变量。

Java8 lambda 是进一步接近闭包的特性,lambda 的 JVM 实现是类似函数指针的东西。但注意: Java7 的 lambda 语法糖兼容不是真正的 lambda,它只是简化了匿名类的书写。同样的 lambda 也只能引用 final 变量。

2. 闭包使用

我们来看一个闭包的例子:

fun returnFun(): () -> Int {
    var count = 0
    return { count++ }
}

fun main() {
    val function = returnFun()
    val function2 = returnFun()
    println(function()) // 0
    println(function()) // 1
    println(function()) // 2

    println(function2()) // 0
    println(function2()) // 1
    println(function2()) // 2
}

分析上面的代码,returnFun返回了一个函数,这个函数没有入参,返回值是Int。我们可以用变量接收它,还可以调用它。functionfunction2分别是我创建的两个函数实例。

可以看到,我每调用一次function()count都会加一,说明countfunction持有了而且可以被修改。而function2functioncount是独立的,不是共享的。

而我们通过 jadx 反编译可以看到:

public final class ClosureKt {
    @NotNull
    public static final Function0<Integer> returnFun() {
        IntRef intRef = new IntRef();
        intRef.element = 0;
        return (Function0) new 1<>(intRef);
    }

    public static final void main() {
        Function0 function = returnFun();
        Function0 function2 = returnFun();
        System.out.println(((Number) function.invoke()).intValue());
        System.out.println(((Number) function.invoke()).intValue());
        System.out.println(((Number) function2.invoke()).intValue());
        System.out.println(((Number) function2.invoke()).intValue());
    }
}

被闭包引用的 int 局部变量,会被封装成 IntRef 这个类。这个 IntRef 里面保存着 int 变量,原函数和闭包都可以通过 intRef 来读写 int 变量。Kotlin 正是通过这种办法使得局部变量可修改。除了 IntRef,还有 LongRef,FloatRef 等,如果是非基础类型,就统一用 ObjectRef 即可。Ref 家族源码:https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/runtime/kotlin/jvm/internal/Ref.java

在 Java 中,我们如果想要匿名类也可以操作外部变量,一般做法是把这个变量放入一个 final 数组中。这和 Kotlin 的做法本质上是一样的,即通过持有该变量的引用来使得两个类可以修改同一个变量。

3. 总结

根据上面分析,我们可以了解到:

  • 闭包不是新东西,是把函数作为“一等公民”的编程语言的特性;
  • 匿名类是 Java 世界里的闭包,但有局限性,即只能读 final 变量,不能写任何变量;
  • Kotlin 的闭包可以捕获上下文的局部变量,并修改它。实现办法是 Kotlin 编译器给引用的局部变量封装了一层引用。

版权所有,转载请注明出处:
https://sickworm.com/?p=1786

4. Kotlin 变量声明和变量状态设计

Published / by sickworm / Leave a Comment

本篇文章将会介绍如何通过正确的变量状态设计来达到简化代码逻辑的效果。

本篇并不是针对 Kotlin 的语言特性介绍,但它比语言特性更为重要。上一篇文章讲的是空安全特性,它允许你方便的处理对象可能为空的情况。但他价值更大的另一面在于,Kotlin 可以声明不可能为空的对象

1. 非空类型

对象不可能为空意味着程序复杂度的降低。而且这不是一般的降低,因为我们开发过程很多时候都是在处理“这个变量可能为空”的情况。在 Java 的环境里,我们出于对调用的 SDK 的不信任,总是要去判断以下是否为空,以保平安,这样处理的代价就是,增加了大量的异常分支代码。如果一个变量他永远都不可能为空,那其实是一件很快乐的事!一个对象可能的状态减少了,程序逻辑会变得更简单清晰,代码的可维护性会大大的提高。我们应该尽量将一个变量声明为非空类型。

Java 提供了 @NonNull 和 @Nullable 注解来满足对象状态的空设计。但由于默认只会产生警告级别的提示(相信我,很多程序员不看 warning),以及使用的繁琐,它最终落得和 final 一样的使用频率。

你很可能会担心非空类型会带来内存泄漏。因为在 Java 很多释放操作都会将引用的变量设置为空,这是个很常见的防止内存泄漏的办法。但代价是将程序状态复杂化。我们确实应该慎重考虑一个变量是否可以一直被持有,但大部分情况我们是可以不用担心的。如 Android 开发基本只要考虑 Activity 是否间接被单例这样生命周期过长的对象持有即可。我还依稀记得刚学 Android 的时候,有些网上教程还会教你在 onDestroy 的时候将 onClickListner 设置为 null 防止内存泄漏。。

2. lateinit

说到尽量声明为非空类型,有人就会提出质疑了:非空类型说来简单,但部分依赖外部调用完成初始化的变量,无法声明为非空类型啊?Activity 的初始化,就是通过 onCreateView 回调初始化的,各种 UI 对象只能在 onCreate 回调的时候被赋值。

针对这种情况,可以使用 Kotlin 的 lateinit 关键字。lateinit 人如其名,它表示这个对象会在稍后被初始化。它还有两条限制:

  1. 无法用 val 修饰,只能用 var 修饰;
  2. 必须为非空类型。

1 很好理解,val 意义是声明后无法再被重新赋值,就和 final 一样。而 lateinit 变量要在稍后才被赋值,所以必须是 var。var 也意味着 lateinit 变量可以被多次赋值,可被多次赋值可能是你想要的,也有可能是你不想要的。

2 的话,设想一下,如果是可空类型,也没必要用 lateinit 了,直接初始化为 null 即可。所以 2 也是合理的。

如果一个变量被声明为 lateinit,你可以不用在声明时初始化它,在任意地方把它当作非空类型直接使用。注意了,此时如果你在初始化这个变量前就使用了该变量,则会丢出一个 RuntimeException:

UninitializedPropertyAccessException: lateinit property has not been initialized

意思就是你还没初始化这个变量就使用它了。所以使用 lateinit 关键字,就需要你自己保证调用顺序,保证调用时变量已经被初始化,Kotlin 不再帮你把关了。这看起来像是一个把 Kotlin 空安全废掉,退化为原来 Java 的无空检查的行为。这样就很没意思了,但其实不是这样,lateinit 有他特有的表意,即:这个变量在稍后会被初始化,且以后都不再为空。以后不再为空即是他和可空变量的区别,从状态复杂度来看,lateinit 变量是介于非空变量和可空变量之间的。

使用 lateinit 是一个有风险的事情,因为非空的条件变复杂了(初始化后才是非空)。如果你不能保证所有调用都在赋值后发生,则不应使用它。但对于 Activity 的 onCreate 这种简单的场景,还是建议使用 lateinit 的。但需要注意一点:

如果 Activity 在 onCreate 的时候初始化失败了,你需要弹窗或直接 finish 的时候,此时你的 lateinit 变量可能没有被赋值,而 Activity 仍会执行 onStart onResume onDestroy 这些回调。这种情况就是“没法保证调用前变量已经初始化”的情况了。

这个时候你可以选择将变量声明为可空类型。也可以用 lateinit 变量专有的判断方法::xxx.isInitialized在关键路径进行判断,比如 Activity onCreate finish 掉的话,关键路径就只剩 onDestroy了(Fragmet 还有 onCreateView 和 onViewCreated)。但相比这两种办法,我更建议你思考,这样复杂的情景是不是我想要的,设计是否能够简化?因为正确设计的程序的状态应该是简单清晰的。

3. 空对象模式(Null Object Pattern)

其实相对于 lateinit,我更喜欢空对象这个设计模式。它没有 lateinit 引入的风险,是一种更简单的状态。空对象就是拥有这个类默认实现的对象。对于数据类来说,它的空对象可能所有成员变量都是0,false,长度为0的字符串;对于带方法的类来说,它的空对象可能是所有方法都是空的,可以调用但没有任何效果。这样一个空对象,它可以帮你代替 null,临时顶替正常实现,直到被重新赋值。同样是初始化异常,lateinit 可能会崩溃,而空对象最多是表现异常。

可参考:https://en.wikipedia.org/wiki/Null_object_pattern

4. final

除了 Kotlin 的非空类型/可空类型,val/var(即 Java 的 final 关键字)也是减少变量状态的利器。而且它比非空类型更彻底,非空类型只是不允许这个变量变为 null,val 直接不允许变量重新被赋值!声明为 val 的变量状态可能性更少,并发竞争的问题都没有了。

变量状态设计原则

经过上面的变量状态介绍,我们按照变量状态从简单到复杂的顺序,可以得到一个变量状态声明的优先级:

  1. 声明为 val 变量,无法满足再考虑 var
  2. 声明为非空变量
  3. 无法满足声明时赋值,优先考虑赋值为空对象
  4. 无法满足空对象,看看是否可以用 lateinit
  5. 声明为可空变量

版权所有,转载请注明出处:
https://sickworm.com/?p=1784

3. Kotlin 类型声明与空安全(Void Safety)

Published / by sickworm / Leave a Comment

上一篇文章介绍了 Koltin 的声明类型语法,但我有意避开了 Kotlin 类型系统里最重要的部分:空安全(Void Safety/Null Safety)。在 Kotlin 中,不可能为空的变量和可能为空的变量被强行分开了(Java 有 @Nullable 和 @NonNull 注释,但只会提供警告)。那 Kotlin 为什么要这样设计呢?我们来看一下今天的代码场景:(只想看使用办法的可以跳过这一节)

0. 场景分析

某一天你正在优雅的编写新业务代码,老板突然告诉你,有一个线上的空指针 crash,赶紧处理一下。你赶紧 git stash 了自己的代码,切换到出问题的那个类。

这是一个管理音频播发的类,叫 PlayerController,用来播放用户上传的 ugc 音频内容。播放是一个很基础通用的功能,所以这个类依赖了一个播放库 AudioPlayer,PlayerController 主要是实现业务功能

这个类之前的维护者刚离职,你临时接任,对里面的结构是不够熟悉的。这个类年代久远,在某个初期版本就上线了,承载了无数的业务变更。里面代码逻辑混乱,业务和通用代码耦合在了一起。你想过重构,但功能实在太多了,需要很长的时间,且现在功能也比较稳定了,重构的收益对业务增长没有明显帮助。那还是先打个补丁呗。

我们来看看代码:

PlayerController.java:

/**
  * 用户音频 ugc 播放器。
  * 如果看到奇怪的逻辑,请不要随便删除,那都是为了规避
  * AudioPlayer 库一些奇怪的 bug,或者是为了兼容业务做的处理。
  */
public class PlayerController {
    private AudioPlayer mAudioPlayer;

    public PlayerController() {

    }

    /** 初始化,只会初始化一次 */
    public void init () {
        // 构造播放组件
        if (mAudioPlayer != null) {
            mAudioPlayer = AudioPlayer();
        }
    }

    /** 播放前需要先初始化数据 **/
    public void prepare(String audioPath) {
        // 设置音频文件路径
        if (mAudioPlayer != null) {
            mAudioPlayer.prepare(audioPath);
        }
    }

    /** 开始播放 **/
    public void play() {
        if (mAudioPlayer != null) {
            // 前置条件判断
            // ...
            mAudioPlayer.play();
        }
    }

    /** 暂停 **/
    public void pause() {
        if (mAudioPlayer != null) {
            mAudioPlayer.pause();
        }
    }

    /** 跳转到指定时间 **/
    public void seekTo(long time) {
        if (mAudioPlayer != null) {
            mAudioPlayer.seekTo(time);
        }
    }

    public void stop() {
        if (mAudioPlayer != null) {
            // 数据处理
            // ...
            mAudioPlayer.stop(); // 该行空指针错误了
        }
    }

    public void release() {
        if (mAudioPlayer != null) {
            mAudioPlayer.release();
            mAudioPlayer = null;
        }
    }
}

这是个很典型的依赖了底层组件的封装类。初始化,释放,播放,暂停这些是外部接口。里面还充斥着很多空判断和 proxy 的代码。这样写代码迅速膨胀了起来。

这个类在后面讲解很多 Kotlin 特性的时候都会引用它,可以多看两眼

开始 crash 分析。通过错误上报,我发现是 mAudioPlayer.stop()这行空指针错误了。mAudioPlayer 在init()时被赋值,release()时被释放,且为了防止内存泄漏被设置为 null。再考虑到并发操作,即mAudioPlayer这个变量在任何使用的时候都可能为 null。

但外部已经有空条件判断了,且这是最新的版本才暴露的问题,为什么会这样呢?

我通过 git 提交记录排查后了解到,是mAudioPlayer.stop()之前新增了一些业务代码,而新增代码有耗时操作。这导致了在空判断时非空,但进入 if 代码块之后,线程被切换了,上层调用了release(),等线程再切回来的时候 mAudioPlayer 已经变成 null 了,再执行就出现了空指针错误。

那最简单的解决办法就是**给mAudioPlayer.stop()单独再包一层***。虽然很丑,但很管用,大伙也很喜欢用,特别是灰度不允许大幅改动的时候。

又或者是给所有 mAudioPlayer 操作都加上锁 synchronized。不过考虑到里面 API 有耗时操作,这样写有可能会造成 UI 卡顿。

不加锁的话也有多次调用,即破坏幂等性的风险。

总之事情就这样暂时解决了。代码随着时间的迁移,越来越多变量可能为空的地方加上了if (xxx != null)的保护代码,甚至可能一个类 10% 的行都是空指针保护!涉及到逻辑冗长的地方,空保护的嵌套甚至到达了 5 层以上!那画面太美。。

这确实是我们 Java Boy 的最通用解决办法。那么 Kotlin Boy 可以如何优雅的解决这个问题呢?

1. Kotlin 非空类型/可空类型(NonNull/Nullable)声明

最开始时我们提到:在 Kotlin 中,不可能为空的变量和可能为空的变量被强行分开了。具体是怎么分开的呢?很简单,默认的类型声明不能为空,类型后面跟问号”?”则可以为空。我们来看下面这段代码:

Nullable.kt:

fun main() {
    var string1: String = "123" // ok
    string1 = "456" // ok

    var string2: String = null // 编译器报错了

    var string3: String? = null // ok
    string3 = "456" // ok
    string3 = null // ok

    var string4 = "123" // ok,类型推断为 String
    string4 = null // 编译器报错了

    var string5 = null // ok,类型推断为 Nothing?
    string5 = "123" // 编译器报错了
}

观察 string1,string2 我们可以得出:
当你像 Java 那样声明一个 String 对象的时候,他在之后的赋值也是不能被赋值为空的。这意味着如果一个变量的类型为 String,则他在任何时候都不可能为空。

观察 string3 我们可以得出:
声明对象为 String? 类型,可以将其设置为空。典型场景是,在你初始化这个变量的时候,还暂时无法得到其值,就必须用可空类型的声明方法了。

观察 string4,string5 我们可以得出:
类型推断是完全根据初始化时的赋值来确定的。他不会根据后面的赋值作为依据来推断这个变量的类型。所以我们需要像 string3 那样显式声明为 String?。至于 Nothing 类型我们暂且不管,实际也很少用到,后面再分析。

2. Kotlin 可空(Nullable)类型的调用

声明一个非空变量,意味着你可以随意的调用他的方法而不用担心空指针错误,相对应的,可空变量则无法保证了。Kotlin 通过不允许可空变量直接调用方法来保证不会出现空指针错误。那么可空变量应该怎么调用呢?

Kotlin 可空变量的调用方法是:调用的”.”号前加”?”或”!!”。前者的行为是,如果非空则调用,否则不调用;后者行为是,如果非空则调用,否则抛出 Illegalstateexception。来看看例子:

Nullable2.kt:

/** 很普通的一个类,有一个“成员变量”,一个返回该变量的方法 **/
class A {
    var code = 0

    fun getMyCode(): Int { // 返回 Int 类型,就像是 Java 的 Integer 那样
        return code
    }
}

fun main() {
    var a1 = A()
    a1.code = 3
    a1.getMyCode() // ok

    var a2: A? = A()
    a2.code = 3 // 编译错误
    a2.getMyCode() // 编译错误

    var a3: A? = A()
    a3?.getMyCode() // ok
    a3!!.getMyCode() // ok
}

生产环境不建议使用双叹号!!,一般只用于测试环境。使用双叹号可以理解为放弃 Kotlin 的空安全特性。

3. Kotlin 可空(Nullable)的传递性

如果一个可空对象调用了方法,因为这个方法有可能不被执行,那么如果我们接收它的返回值,那么返回值的类型应该是什么呢?我们继续使用上面A这个类,来看看这个例子:

/** 很普通的一个类,有一个“成员变量”,一个返回该变量的方法 **/
class A {
    var code = 0

    fun getMyCode(): Int { // 返回 Int 类型,就像是 Java 的 Integer 那样
        return code
    }
}

var a4: A? = null

fun main() {
    var myCode: Int = a4?.getMyCode() // 编译错误
    var myCode2: Int? = a4?.getMyCode() // ok

    myCode2.toFloat() // 编译错误
    myCode2?.toFloat() // ok

    var myCode3: Int? = a4!!.getMyCode() // ok
    myCode3.toFloat() // ok
}

我们可以看到,本来getMyCode()方法返回的是 Int 类型,但由于调用时 a4 为可空类型,所以 myCode 被编译器认为是 Int? 类型。所以,可空是具有传递性的。

双叹号由于在变量为空时会抛出异常,所以它的返回值就还是为 Int,因为抛了异常的话,后面的代码已经不会被执行了。

这个 a4 要写在外面的原因是,如果声明为局部变量,即使 a4 被声明为 A?,但由于局部变量的关系,编译器会把 myCode 纠正为 Int,而不是 Int?。

如果链式调用的话,就会变成这个样子:

myCode2?.toFloat()?.toLong()?.toByte()
myCode2!!.toFloat().toLong().toByte()

看起来比较 ugly。。但不用担心,Kotlin 有其他的特性来协助你处理可空变量,不用写出像这样的嘲讽代码(疯狂打问号 ???)。请继续期待后面的文章吧!

4. 回到场景

如果用 Kotlin 来实现场景中的代码,只需要将 mAudioPlayer 声明为可空类型就可以了:

PlayerController.kt:

/**
  * 用户音频 ugc 播放器。
  * 如果看到奇怪的逻辑,请不要随便删除,那都是为了规避
  * AudioPlayer 库一些奇怪的 bug,或者是为了兼容业务做的处理。
  */

class PlayerController {
    private var mAudioPlayer: AudioPlayer?  = null

    /** 初始化,只会初始化一次  */
    fun init() {
        // 构造播放组件
        if (mAudioPlayer != null) {
            mAudioPlayer = AudioPlayer()
        }
    }

    /** 播放前需要先初始化数据  */
    fun prepare(audioPath: String) {
        // 设置音频文件路径
        mAudioPlayer?.prepare(audioPath)
    }

    /** 开始播放  */
    fun play() {
        // 前置条件判断
        // ...
        mAudioPlayer?.play()
    }

    /** 暂停  */
    fun pause() {
        mAudioPlayer?.pause()
    }

    /** 跳转到指定时间  */
    fun seekTo(time: Long) {
        mAudioPlayer?.seekTo(time)
    }

    fun stop() {
        // 数据处理
        // ...
        mAudioPlayer?.stop() // 不会空指针错误了
    }

    fun release() {
        mAudioPlayer?.release()
        mAudioPlayer = null
    }
}

写起来方便很多,而且少了一层嵌套,人也舒服了。

空安全特性首次出现在 F#(2005) 上,此外 Swift 和 TypeScript 等也是空安全语言。

空指针首次出现在 Algol W(1965) 上,用作者的原话说,就是:后悔,非常的后悔。。(I call it my billion-dollar mistake)

参考:https://en.wikipedia.org/wiki/Void_safety

版权所有,转载请注明出处:
https://sickworm.com/?p=1779

2. Kotlin 变量声明与类型推断

Published / by sickworm / Leave a Comment

本文介绍 Kotlin 变量声明涉及的相关知识点。首先我们来回顾一下 Java 局部变量声明的几个例子(成员变量的修饰符先不讨论):

// 播放器的一些变量
boolean isPlaying = false;
final String songName = "Dingdingdong";
final ReadyForPlayingData readyForPlayingData = new ReadyForPlayingData();
WeakReference onProgressListener = new WeakReference<>(this);
ResultData result = getPlayingResult();

以上基本涵盖了所有情况:基础类型,字符串,对象,范型,函数返回值接收。接下来我们看 Kotlin 是怎么声明的:

// 播放器的一些变量
var isPlaying = false
val songName = "Dingdingdong"
val readyForPlayingData = ReadyForPlayingData();
var onProgressListener = WeakReference(this);
var result = getPlayingResult()

微微统计一下,Java 需要打 263 个字符,Kotlin 需要打 198 个字符。不考虑自动补全帮助的话,Kotlin 变量声明的效率比 Java 高 (263 – 198) / 198 = 33%。一般程序员打字速度在理想(思维行云流水)情况下可以去到 150~200 CPM (字符/分钟),这意味者在声明这段变量 Kotlin boy 比 Java boy 能节省 20~26 秒

感兴趣的同学试试这个网站:livechatinc.com。点击 global-scores 还可以看到全球人民的速度分布。

我是 232 CPM,53 WPM。考虑到编程需要输入大小写和标点,实际会慢不少。

玩完记得回来。。

更高效率的代码编写可以提高你的开发效率。诚然有很多二指禅的大神,但当你思路确定,需要快速的编写出来的一段小代码并调试的时候,这种效率的优势是实实在在的,特别在变量声明这种低思考密度的代码上。这也是现代语言的威力。

那么理清了 Kotlin 变量声明带给我们的好处后,我们一起来看一下里面的几个知识点:

1. 类型推断与 var

Kotlin 不再需要显式的声明变量的类型,取而代之的是通过赋值的类型来判断。事实证明,绝大部分情况都是 work 的。而且编译器非常聪明,甚至连参杂了多种类型的范型都能推断出来!极小部分情况需要显式声明,如:

  • 被赋值的类型不是期望的类型,如想声明为其父类
  • 某些极限情况会出现无法推断的情形,如循环推断依赖

但确实是极小部分的情况,而且 IDE 都能给出解决办法。

声明变量使用 var / val 来代替原本的声明类型的地方。而需要声明类型的时候,在变量名后以“: Class”的形式声明,如:var abc: CharSequence = "abc"

2000 年后出现的编程语言基本都支持类型推断了。连 Java 8 也开始支持类型推断。

可参考:程式語言歷史

2. final 与 val

val = final var,不过 Kotlin 没有 final 这个关键字,只是代表的意义是这个意思。这个知识点已经讲完了,再见!

开个玩笑,我们还是需要知道为什么不要 final var,要val。在 Java 年代,我们很少用 final 这个关键字,虽然很多变量,类型和函数都符合 final 的设定。大部分变量我们只会设置一次,大部分的函数也不会被继承。那为什么不用上呢?唯一的原因就是因为打 final 太麻烦了!而 val 就是为了解决“final”打起来太麻烦而设计的

final 属性其实是一个很好用的代码约束,他代表这个变量后面不会再被修改。如果是个 Java 成员变量,你甚至不需要他被担心设置为 null。否则你就要在很多地方加上非空判断。或者在首次维护一段别人代码的过程中,需要时刻考虑这个变量是否会被更改。

final 意味这这个变量的可能性变少了,我们在阅读代码的过程中,不需要再去关注这个变量的赋值变化,这对我们快速读懂代码是很有帮助的,毕竟我们脑容量都是有限的,并不能同时关注非常多的变化。更少的变化,意味着更清晰易懂的逻辑。

Swift 是 var 和 let。

版权所有,转载请注明出处:
https://sickworm.com/?p=1774

1. Kotlin hello world!与函数声明

Published / by sickworm / Leave a Comment

首先我们看 Java 的 hello world:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

非常熟悉。可在我漫长的编程生涯中,我大概是第 5 次在 Google 搜索了“Java Hello world”之后,才能独自完整的默写出来。。因为他有些“不太好记住”的点:比如他必须通过该类的一个叫 main 的 public 的 static 的函数,且这个函数入参必须是有且仅有一个 String[] 数组。如果错了其中一个,那你就没法运行。

这是 Kotlin 版本:

fun main() {
    println("Hello world!")
}

写起来还蛮快乐的,是吧?没有太多多余的东西,很简洁。但大家可能也会有很多疑问。那我们一起来看:

  • Kotlin 的 Hello world 没有声明类。这很不 Java!要知道 Kotlin 或者其他基于 JVM 的语言无论怎么设计,他最终也是要在 JVM 上跑的,而 JVM 恰好就是一套基于类来设计的运行机制,和 Java 的思想是一致的。你 Kotlin 现在自己搞一套,连类都没有了,怎么在 JVM 上跑?

这其实是 Kotlin 编译器的功劳。这点很重要,以后你遇到 Kotlin 的新特性,和 Java 对不上的时候,就回想这点:都是 Kotlin 编译器的功劳,他把 Kotlin 代码转换成了符合 Java 思想的 JVM 字节码。小本本记好啦,这句话将贯穿整个 Kotlin 学习。

如果你用过 jadx的话,可以反编译试试。这里的结论就是:HelloWorld.kt 会被编译为一个叫 HelloWorkKt 的类,把这段代码塞进去。当然你再去创建一个 HelloWorldKt 的类就会报错了,你可以试试 🙂

  • 没有分号。古时候,一行完整的代码用分号来分隔。后来大家觉得分号已经没有必要了,也基本没有人会在一行写几行代码了。新的语言一部分是柔和派,分号变为可选;另一部分激进派直接去掉了分号。Kotlin 属于前者,如果你写一个分号,IDE 爸爸会告诉你,没得必要,但不会报错。如果你故意把两行代码写成一行,中间加一个分号,这个分号就是必要的。

  • 函数的声明通过 fun 关键字。欸?Java 不需要关键字来声明函数呀。以 JavaScript 为首的语言认为,函数是一等公民,应当做一个对象看待。这样函数就可以被持有,被传递,提高他的灵活性。所以像 JavaScript 这样的语言,方法声明会有关键字,否则你就不知道自己到底是在调用一个函数,还是在执行一个变量持有的函数了。

Kotlin 也支持函数是一等公民,所以函数声明需要关键字。Java 其实也有类似的东西,他叫匿名类。只不过匿名类需要声明一个类,再用匿名类的特殊写法去创建一个看起来像函数对象的东西。

  • 函数属性默认是 public。 Java 默认是用的很少的 package private。
  • 函数默认返回值是 Unit。 Java 没有默认返回值,需要显式声明为 void。为啥 Kotlin 不用 void 呢?因为声明返回值为 Unit 可以让一切对象化,在某些场景可以简化代码编写。举一个简单的情况,你可以写 return println("Hello world!"),而不再需要写成两行了。

  • **println 代替了 System.out.println **。学 Java 的时候大家应该都吐槽过,写个打印好废键盘啊!这是因为 Java 严格按照对象调用的规则办事,方法必须是属于类的,除非你在类里面调同一个类里的办法,可以省略this.,其他地方都需要加对象才能调用一个方法(类也是对象嘛)。

所以 Kotlin 是怎么做到不用指定对象也能调用方法呢?是有顶层声明(top-level declaration)的特性。就像第一点提到的那样,Kotlin 编译器会通过各种各样的方法把顶层声明的函数编译成对象方法调用的形式。顶层声明还有更多酷炫的能力,比如给任意一个类“增加方法”,后面我们会展开来讲。

以上就是 Kotlin 的 Hello world 涉及到的几个知识点。是不是觉得要写个 Hello world 也要懂这么多很费劲呢?其实要弄懂 Java 的 Hello world 也很费劲的,只是你已经过去那个初学的阶段了。在 Java 的基础上理解 Kotlin 相对还是简单的,如果你有其他语言的开发经验那就更简单了,因为 Kotlin 的特性,基本都能在某个语言上找到,它本身并不是新特性的创造者,他只是好用特性的搬运工。

版权所有,转载请注明出处:
https://sickworm.com/?p=1768

0. 如何判断一门语言的好坏

Published / by sickworm / Leave a Comment

我在组内推广 Kotlin 遇到不少挑战,虽然我自己觉得它确实是一门优秀的语言,有着丰富的特性,能提高我们开发效率,减少 bug 的出现,但同事们并不觉得。而且他们的观点往往不是“Kotlin 并没有比 Java 好多少”,而是“Kotlin 就没比 Java 好“。Kotlin 语法不习惯;Kotlin 这样强推这么多年,也不温不火,说明 Kotlin 不行。

所以本着“先问是不是,再问为什么”的原则,我们在讨论“Kotlin 比 Java 好在哪里之前”,必须先说清楚“Kotlin 比 Java 好吗”这个问题。

关于判断语言是否优秀,我们听过最多的可能就是“XXX 是最好的语言”了。不过这种旧世界的观点,在大家充分学习了网络上的编程知识之后,现在都成为大家调侃的段子了。现在,我们更喜欢“编程语言没有优劣之分”,“没有最好的编程语言,只有最适合的应用场景”这样的论调。这听起来就很有哲理,很编程正确嘛!如果把这些的观点套在 Java 和 Kotlin 身上,也可以得到“要根据具体场景,有些场景确实 Java 更好”的观点。

不过今天,我们不谈这些和稀泥的观点。这篇文章会告诉大家,客观准确评判一门语言好坏的标准。

无论如何,编程语言是给人使用的,那我们就从人本身特点出来来讲这个逻辑。我们人是靠大脑思考的,而大脑有个致命的缺点:容量有限,且有一定错误率。这就导致我们程序员在编写状态复杂流程冗长的代码的时候,容易出现错误。这是人大脑的特点决定的,无法避免。

但劳动人民的智慧是无穷的,程序员们想出各种办法来降低自己犯错概率。从机器码到汇编,到面向流程,到面向对象,到设计模式,编码规范,高级语法特性,不断有新编程技术的出现,让程序员们可以写更简单的代码。也就是用更抽象的表达来表示同样的意图

阿拉伯人:30 加 42 等于 72。

法国人: 30 加 40又2 等于 60又10又2。

你能想象,用汇编去实现你现在要的一个业务,需要多长时间?写出来会有多少个 bug 吗?当你用 Python 快速实现了一个算法并开始验证的时候,别人可能还在用 C++ 吭哧吭哧地写着 std::vector 的 for 循环;当你用 Swift 快速实现了一个 iOS demo 的时候, 别人可能还在用 objective-C 吭哧吭哧的写着头文件声明;当你用 Go 的协程快速实现了并发处理时,别人可能还在用 Java 吭哧吭哧地实现一个线程池。又或者说,用 C++ 和 Java 实现同一个需求,C++ 你需要花费额外的精力关注内存管理,数组越界,类型安全等问题,这样你留给业务本身的精力就少了,开发变慢了,bug 也更容易出现了。

Java:你知道单例线程安全的四种写法吗?

Kotlin:你是说 by lazy 吗?

编程语言是为了实现业务而存在的,那我们就应该选择一门实现业务编写效率高,维护成本低的编程语言。

编写效率高意味着同样的功能我可以用更少的代码实现;同时 sdk 功能齐全,轮子多,大部分基础组件不需要重复开发。没错就像 Python 那样。Kotlin 相比 Java,他的语法表达更简洁,更容易写出低耦合,高内聚的代码;且和 Java 互操作的特性,可以直接使用 Java 的轮子,大大缩短了建设 Kotlin 生态的过程。

维护成本低分为几个方面,分别是:bug 少,代码简洁易懂,对需求变更友善。

bug 少。国外有对千行 bug 数量进行了研究,研究提出 bug 数量和所使用的语言没有直接关系,和语法表达流畅性有关系^[https://stackoverflow.com/questions/2898571/basis-for-claim-that-the-number-of-bugs-per-line-of-code-is-constant-regardless], ^[https://stackoverflow.com/questions/2898571/basis-for-claim-that-the-number-of-bugs-per-line-of-code-is-constant-regardless]。 我觉得可以理解为,人大脑的犯错频率是比较固定的,使用时长越长,出现的“bug”也就越多。如果你能通过选择一门语言,更快的实现指定功能,那么 bug 数量会相应减少。

代码简洁易懂。Kotlin 需要编写的代码更少,是因为 Kotlin 对语意有更精简的表达,你在习惯之后可以比 Java 更快的阅读完同样的功能。这点在后面我会继续说明。

对需求变更友善。这是代码简洁易懂的自然延伸,所谓 less is more 嘛。

Pythonista:人生苦短,我用 Python。

Androider:以前我没得选,现在我想做个 Kotlin boy。

PS:我也了解到很多同学拒绝 Kotlin 的理由是“不习惯”。比如说语法用着不习惯,看着也不习惯,很难看懂云云。我想说的是,无论是编程语言还是其他工作外的事,千万要忌讳用“习惯”作为理由。那些 30 多岁的外企程序员,失业中年危机,不就是“习惯”习出来的么?习惯会让你避开新的东西,而能让你能力,事业,资产产生“增量”的,往往就是这些新的东西。新的东西最容易产生“增量”。

我们判断一个东西好不好,有没有价值,有没有必要去投入,不要用“习惯”。要把好与不好列出来。就像我这样,我说 Kotlin 好,我把好的理由讲给你听,你觉得没道理,你可以针对这些点进行反驳,或者提出新的观点,然后和我进行讨论。用“不习惯”作为理由来拒绝,只会让自己错失“增量”的机会。

版权所有,转载请注明出处:
https://sickworm.com/?p=1766

Kotlin 从拒绝到真香

Published / by sickworm / Leave a Comment

《Kotlin 从拒绝到真香》是一系列介绍 Kotlin 好用的特性的文章。文章会以某一个经典的 Java 使用场景开始,提供相应的 Kotlin 实现代码,并讲解其中用到的特性。

文章主要面向 Android 开发者。如果你正在考虑是否使用 Kotlin 来代替 Java 作为 Android 开发语言,这些文章会非常适合你。笔者所在团队正积极使用 Kotlin,并有计划的使用 Kotlin 重构项目代码。但对团队成员来说,使用 Kotlin 并不是强制的。笔者团队内推广 Kotlin 的过程中也遇到了部分同事“拒绝”Kotlin,而这些文章正是我在推广和不断尝试说服他们的过程中逐渐积累起来的“真香”部分。

文章不求鞭辟入里,但尽可能保证有趣不枯燥。如果恰好你也喜欢这些文章,请务必 star 它!

作为 Android 开发者的你肯定已经拥有了 Android Studio,但我还是建议你下载一个 JetBains CE 或其他版本,这样你可以方便的创建一个 Kotlin 文件并运行它。大部分例子中,我们只需要 kotlin 的环境就可以了。
https://play.kotlinlang.org 是一个在线 Kotlin playground,使用它可以方便快速的运行一小段代码。

版权所有,转载请注明出处:
https://sickworm.com/?p=1760