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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据