月度归档:2020年03月

9. Kotlin 函数声明和扩展(extension)

Published / by sickworm / Leave a Comment

1. Java 的老朋友 Utils 工具类

Utils 工具类是无构造参数的 static 方法集合,用于扩展某个对象的功能,如 MathUtils,ToastUtils,FIleUtils,StringUtils, LogUtils。Utils 类在一定程度上减少了重复代码的问题,它是成本最低的 DRY(Don’t repeat yourself)实践。

Utils 工具类实在太常见了,以至于很多开发者都不曾质疑他的合理性。但 Utils 实际上是反 OOP (面向对象模式)的妥协产物。我们从代码设计的角度看,Utils 方法是 static 的,没有 OOP 的继承,重写,抽象的特性(static 本身就是反 OOP 的)。且 Utils 违反了单一职责,一个类应该包含其属性和所有操作方法。而 Utils 实现的方法并不在这个类内。

而从使用者的角度,使用者必须预先知道这个 Utils 工具类的存在,他能使用为这个类添加的扩展方法。 在实际项目实践中,这个条件往往是缺失的,因为在团队开发中,个人无法掌握所有代码,因为不知道这个代码已经有人实现过了,导致大家都实现了自己的 Utils。一个工程里同一个类的 Utils 往往会有好几个。

但存在必然是合理的。我自己就是一个写 Utils 的老司机。从个人角度来看,让我使用 Utils 而不是对象继承的原因,主要是因为:

1. 无法继承/重写这些类及其方法,只能通过 Utils 扩展;
2. 继承一个类比抽取代码块封装为函数的实现成本+替换成本高;
3. Utils 绝大情况下只是用来存储代码块,需求非常稳定,无需面向对象。

依赖的类是 SDK 提供的时候 Utils 往往是不可避免的。且使用 Utils 的场景里很少会用到面向对象的特性,那么没有面向对象的缺点也并没有那么严重了。那么抛开 Utils 的设计缺点,我们是否可以避免使用上的缺点?Kotlin 提供的解决方法就是扩展(extension)。

2. Kotlin 扩展的使用和实现分析

声明一个 Kotlin 扩展如下:

// StringUtils.kt
fun String.appendHaha(): String {
    return this + "haha"
}

它与普通的方法声明很接近,只是方法名前多了一个类名,来表示其归属的类。扩展声明为顶层声明的时候可以被外部调用(是的,因为函数是一等公民,在方法内部也可以声明扩展方法)。

在函数体内用 this 来引用调用的实例,属性和方法的访问权限与普通调用一致。一致的意思是和你正常在其他方法内部调用的权限一致,并不会因为是扩展声明就可以访问 private/propect 权限的属性和方法。这是因为扩展声明在字节码层面上其实是 static 方法。下面是appendHaha 对应 jvm 字节码的反编译结果:

public class StringUtilsKt {
    @NotNull
    public static final String haha(@NotNull String $this$haha) {
        Intrinsics.checkParameterIsNotNull($this$haha, "$this$haha");
        return $this$haha + "haha";
    }
}

所以从 Java 的角度来看,Kotlin 的扩展方法和 Utils 的调用没有差别,都是调用类的 static 方法然后传入操作的参数。实际上 Java 想要调用 Kotlin 的扩展方法也确实是这样调用的。

扩展方法的调用和实例方法调用一致,在调用者角度没有区别。 Android Studio 会自动提示对应类所有的扩展方法,且扩展方法的颜色(黄色)会和普通实例方法(白色)区分开来。

Kotlin 的扩展特性和 objective-C 的 category 特性功能非常相似,都是为一个现有的类添加方法(且只能添加方法),只是代码组织结构上有些许差异。但 objective-C 的 category 特性是 runtime 特性,Kotlin 扩展的实现更接近语法糖。

3. 总结

Kotlin 扩展依然没有解决 Utils 类的设计缺点。就像 Kotlin companion object 对 Java static,Kotlin Int 对 Java int,Kotlin property 对 Java field 一样,Kotlin 扩展是 Kotlin 对 Java 不完全面向对象的“清理”,使 Kotlin 更接近完全面向对象。相比 Utils 工具类,Kotlin 扩展特性能有效减少开发者心智负担和沟通成本,从而提高开发效率。

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

8. Kotlin 函数声明与默认参数(Default argument)

Published / by sickworm / Leave a Comment

1. Java 的函数重载和烦恼

在 Java 中,当我们要实现同一种功能,但函数入参出参不一样的函数的时候,我们可以用到 Java 的函数重载功能。在 Android framework 中同样也存在大量的重载函数,以方便开发者调用。重载函数深入人心,得到大家的认可。

东西确实是好东西,但当重载函数过多的时候,代码就显得臃肿了,比如这里有一个 Toast 显示的工具类,在经过不断的功能扩展后,发展成一个拥有海量重载方法的类:

// ...

public static void show(CharSequence msg) {
    show(Toast.LENGTH_SHORT, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void showLong(CharSequence msg) {
    show(Toast.LENGTH_LONG, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void show(Activity activity, int resId) {
    show(activity, resId, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void show(Activity activity, CharSequence msg) {
    show(Toast.LENGTH_SHORT, activity, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

/**
 * 当msg0不为空时展示msg0,忽略msg1;否则显示msg1
 */
public static void show(Activity activity, CharSequence msg0, CharSequence msg1) {
    if (!TextUtils.isEmpty(msg0)) {
        show(Toast.LENGTH_SHORT, activity, msg0, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
    } else if (!TextUtils.isEmpty(msg1)) {
        show(Toast.LENGTH_SHORT, activity, msg1, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
    }
}

public static void show(Activity activity, int resId, int gravity) {
    show(Toast.LENGTH_SHORT, activity, resId == 0 ? null : getString(resId), gravity);
}

public static void show(int duration, Activity activity, int resId) {
    show(duration, activity, resId == 0 ? null : getString(resId), Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void show(int duration, Activity activity, CharSequence msg) {
    show(duration, activity, msg, Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
}

public static void show(final int duration, Activity activity, final CharSequence msg, final int gravity) {
    // 具体实现
}

// ...

和 get/set 方法一样,这是典型的信息密度低的代码。那么有什么办法能够更精简的表达同样的功能,不同的入参的特性呢?有的,就是默认参数特性。

2. 重载函数的替代者,默认参数

Kotlin 拥有默认参数的特性,如果用 Kotlin 实现上述 Java 代码,可以简化为:

fun show(msg: CharSequence,
        msg2: CharSequence? = null,
        context: Context = Global.getApplicationContext(),
        gravity: Int = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM,
        duration: Int = Toast.LENGTH_SHORT) {
    // 具体实现
}

我们看到,声明默认参数的方法很简单,只需要把可以使用默认参数的入参用“=“号给他赋值一个默认值即可。一般来说,我们会把必须提供的参数写在前面,有默认参数的入参排在后面。因为你在调用的时候,函数的入参默认是按顺序映射的,按上面的顺序排列的话,你只需要填完必须的参数即可;而如果还想提供可选参数,就继续按顺序填写。

那如果我只想提供部分可选参数,比如上面的show函数我只想提供duration参数,跳过其他可选参数呢?Kotlin 提供了这样的调用办法:

show("this is a toast");
show("this is a toast, duration = Toast.LENGTH_LONG);
show(msg = "this is a toast, duration = Toast.LENGTH_LONG);
show(duration = Toast.LENGTH_LONG, msg = "this is a toast);

我们发现,Kotlin 方法调用时,可以显式的指明对象和入参的映射关系,无需按顺序传递。注意,这个特性不分必须参数和可选参数,所有的参数都可以用这种形式指定映射。

但一般来说,我们只在可选参数时用到。还有一种应用场景是,当你觉得必须参数的值让人迷惑,想显式的告诉阅读者这个值所对应的入参时。

大家可能已经发现,很早以前,Android Studio 对没有提供名字的函数参数,已经默认显示这个参数对应的名字。

Flutter 的 Dart 语言也有默认参数特性,而且 Flutter 组件对默认参数的使用可谓是淋漓尽致。它会把一个控件所有可配置的参数都提供在构造函数中,而且把必须参数和可选参数分开。这样开发者可以很方便的看到它必须配置和可以配置的所有参数,不用再去慢慢找这个控件提供了什么设置方法。

3. 默认参数和函数重载对比

默认参数和重载函数对比,重载函数可以改变入参和出参(返回值),默认参数只可以改变入参。不过改变出参的场景实在很少,一般我们都会用不同的函数名来区分不同的返回值,比如 covertToFloatcovertToInt

其次,每一个重载函数都是一个方法,会记录在方法表,占用 Dex 的最大方法数。默认参数会生成 2 个方法,一个是全参数的方法 A,另一个方法 B 也是全参数,但比全参数方法多出来了 flag 参数,该 flag 参数用来记录哪些参数是可选参数。外部调用的时候调用的是方法 B,没有指定的可选参数将会被赋值 0 或 false 或 null,指定了的可选参数会被赋值,且对应个 flag 位会被标记。到了方法 B 内部,没有被 flag 标记的参数,会被设置为默认值,最后方法 B 调用 方法 A。Kotlin 通过这种方式,减少了重载函数可能带来过多的方法数。

Kotlin 也支持函数重载。

4. 函数声明的特性发展

如果是一开始接触的都是高级语言的同学,可能会觉得函数重载是个比较奇怪的特性:为什么这也算是一种特性?他们除了方法名是一样的,入参不一样,出参不一样,为什么要单独拿出来说呢?这是因为在 C 语言时代,方法的识别只通过函数名,所以无法做到函数重载。后来大家感受到同名函数不同参数存在的必要性,像 C++,就把方法的入参和出参都写到了符号表里。Java 的方法签名,也是包含入参和出参的。这样的语言,就具备识别重载函数的能力,所以函数重载就成为了一种新特性。

但函数重载,是一个个不同的函数,只是名字一致而已。在语义精简和代码规范有一定的缺陷。语义精简就是“更少的代码表达相同的意图”;而代码规范,因为函数重载的功能基本是相同的,更推荐的做法是函数重载只有一份实现代码,其他函数重载都补全参数,然后调用这个完整的实现代码,就像开头的 Toast 工具类代码一样。但实际上由于缺少实际约束,有些开发者会复制多份实现,填入到不同的重载函数中。可以说函数重载容易写出,“smelly”的代码。

而默认参数特性,避免了函数重载的语义精简和代码规范缺陷。

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

7. Kotlin 变量声明和属性(property)

Published / by sickworm / Leave a Comment

1. Java 的成员变量和它们的 get/set 方法

在 Java 中,我们把在类中声明的变量,称为为成员变量(field),函数中声明的变量称为局部变量。在经典的 Java 设计理念中,成员变量是不建议暴露的,而当你想访问修改成员变量时,应声明其对应的 get/set 方法。因为成员变量没有办法继承重写
,无法声明为接口,get/set 权限无法分开控制等。使用 get/set 方法代替直接修改成员变量,更符合面向对象设计。
因此 get/set 方法在 Java 大地上遍地开花,无处不在。所以我们经常能看到这样的代码:

public class StringEntity {

    private String resId;
    private String value;

    public StringEntity() {
    }

    public StringEntity(String resId, String value) {
        this.resId = resId;
        this.value = value;
    }
    public String getResId() {
        return resId;
    }
    public void setResId(String resId) {
        this.resId = resId;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
}

噼里啪啦的写了一大堆代码,但功能却极其简单:StringEntity 包含了 resId 和 value 两个 String 的属性,你可以读取或修改它。虽说现在 IDE 都可以帮你快速的生成这些代码,但无法摆脱代码的信息密度低,可读性差的缺点。那么有没有什么语言能够更精简的表达 get/set 的语义呢?有的。

更详细的 get/set 与 field 的比较,参考:https://stackoverflow.com/questions/1568091/why-use-getters-and-setters-accessors

2. 引入属性 property 的概念

最开始我是在 objective-C 上了解到 property 的概念。属性 property 和成员变量 field 的声明和使用方法都没有什么区别,但property 允许你自定义它的 get/set 方法。如果你不自定义 property 的 get/set 方法,那它就和一个普通的变量没什么区别;而如果你自定义了 get/set 方法,在你读取/修改 property 时,实际上是调用了 property 的 get/set 方法。简单来说,属性 property=成员变量 field + get/set 方法,且 get/set 方法拥有默认实现。

property 的优点也很明显:

  1. 你可以用更简洁的方式实现 get/set 方法;
  2. field 和 get/set 方法统一后,代码的内聚性更高了,不会出现 field 在文件头,get/set 方法在文件尾的情况;
  3. 在 Java 类内部调用中,你既可以调用 field,也可以调用 get/set 方法,这种情况下内部调用是不统一的,当 get/set 方法添加了更多的行为时,原本直接调用 field 的内部代码可能会出错。property 统一了入口,避免了这种问题。

3. Kotlin 的 property 使用

在你不知道 property 的概念时,你就像声明一个局部变量一样声明 property 即可。

当你希望自定义 property 的 get/set 方法时,就为它增加 get/set 方法,按特定的语法结构来声明。举个例子:

class DataList {
    var dataList: List = listOf()
        private set // 1
    var size: Int // 2
        get() = dataList.size // 3
        set(value) { // 4
            dataList = ArrayList(value)
        }
    var name: String = "DataList"
        set(value) {
            field = value // 5
            println("new name: $field")
        }
}

这段代码列举了几种常用的 property 用法,简要说明:

  1. 当你只需要修改 property 的 get/set 访问权限时,不需要定义方法体;
  2. 如果定义了 get 方法且类型可以推断时,类型是可以省略的
  3. get 方法的声明方式,是一个无参的函数(= 号用法适用于代码只有一行的情况,用大括号也可以)
  4. set 方法的声明方式,是一个只有一个参数的函数,入参名字可随意发挥。
  5. 调用 property 的 field 的方法。前面说到 property = field + get/set 就是这个意思。

Kotlin properties 介绍:https://kotlinlang.org/docs/reference/properties.html

4. Kotlin property 的 JVM 字节码体现

Kotlin property 被编译成字节码后,通过反编译我们可以看到,一个 property 会生成一个同名的 field,以及驼峰命名的 get/set 方法。所以 Java 想要调用 Kotlin 的 property 时,直接调用 get/set 方法即可。而 Kotlin 调用 Kotlin 的property,则没有 Java 那么麻烦,就像局部变量一样调用即可。

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

6.Kotlin 变量声明与基本类型(Primitive Type)

Published / by sickworm / Leave a Comment

本文将会介绍 Java 的基本类型和 Kotlin 的区别。我们知道,Java 的基本类型是 boolean, char, short, int, long, float, double。这些基本类型不是对象,只可以进行基本的数学逻辑运算。Java 虽然打着“一切皆对象”的口号,但在基本类型还是留了一手。他们是特别的存在。

至于为什么要保留基本类型,真相只有一个:性能。大部分基本类型操作是一条指令就可以完成的,而对象方法调用则需要很多条指令才能完成;另外占用内存相比对象,也小很多。可以说 Java 诞生初期,在概念统一和性能的权衡下,把天平偏向了性能。这也很合理:90 年代时候的硬件速度还非常慢。且 Java 最初是为嵌入式设备而设计的,后面才把目标改为互联网。

现在市面上大部分的银行卡,里面装的是 Java 虚拟机,开发者通过编写受限的 Java 代码来实现一个叫 Applet 的应用单元,并装载到银行卡中。银行卡被插入后,机器会通过针脚或 NFC 和银行卡进行通讯。这种技术叫 Java Card 技术。
所谓受限的 Java 代码,没有 String,没有 JDK,甚至大部分连 int 都不支持。只能用 byte 和 short。因为芯片是 16 位的。
我上一份工作,在银行卡上实现了三种数字货币的交易协议。。

我们不妨把 Java 的面向对象称为不完全面向对象。那么是否有“真·面向对象”语言?有的。如 Smalltalk,Python,Kotlin 就是。在他们的编程环境里,没有基本类型,是真正的“一切皆对象”。这样带来的好处是概念的统一。“基本类型”这样的概念不再被需要,不再需要特别的处理它,所有声明出来的变量都具有同样的行为,不再需要区分引用类型和值类型。说到引用类型和值类型,大家在初学 Java 的时候应该都花了不少功夫去理解吧?

当然了,Java 也有基本类型对应的对象封装。如 int 对应 Integer,float 对应 Float,并且 jdk1.5 之后提供了自动装箱拆箱的编译器特性。但因为写起来比基本类型麻烦,且考虑性能问题,导致如果不是限定场景,大家都不会主动用它们。

而 Kotlin 为了提供完全面向对象的特性,摒弃了基本类型。但 Kotlin 没有直接使用 Java 的 java.lang.Integer,java.lang.Float 装箱类,而是另起山头,创造了 kotlin.Int,kotlin.Float 等类,因为别人写的代码都是 shit,因为 Java 的装箱类是集成在 JDK 的,无法随着 Kotlin 版本更新而更新。且在 Kotlin 中,数值类还有拥有额外的编译特性:

前面说到 Java 因为性能问题,保留了基本类型。那么 Kotlin 选择了完全面向对象,那理应要承受一定的性能损失。但其实 Kotlin 在编译成 jvm 字节码的时候,大部分的 Int 都会编译回 int,小部分会被编译成 Integer。这个小部分,典型的情况就是你声明一个变量为可空类型时,即声明为 Int?,这个时候无法使用 jvm 的基本类型结构。

而我们观察 kotlin.Int 时,可以看到除了数学运算的运算符重载方法,和强转的方法(toFloat,toLong 等)外,就没有其他方法了,而这些方法都可以直接对应基本类型运算的操作。kotlin.Int 声明为这样一个简洁的数值封装类,让转换为 jvm 字节码的基本类型铺平道路。

所以使用 kotlin 的数值类型时,绝大部分场景下,不会有额外的性能开销。

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