Kotlin高级函数–顶层函数、扩展函数、中缀调用

顶层函数

在我们的日常的开发过程中我们或多或少的会创建一些Util类,一般我们的写法是这样的:

package com.example;

import android.content.Context;
import android.widget.Toast;

public class ToastUtil {

private static Toast mToast;

private static void showToast(Context context, String toastContent) {
if (mToast != null) {
mToast.cancel();
}

mToast = Toast.makeText(context, toastContent, Toast.LENGTH_SHORT);
mToast.show();
}

private static void showToast(Context context, int toastContentResId) {
showToast(context, context.getResources().getString(toastContentResId));
}

}

在使用这个类的时候,我们会通过类名.方法名的方式去调用,例如

ToastUtil.showToast(applicationContext, "demo")

在Kotlin中则认为一个函数或者方法有的时候可以并不属于任何一个类,它可以独立存在。所以在Kotlin中类似静态函数和静态属性可以去掉外层类的容器,一个函数或者属性可以直接定义在一个Kotlin的文件顶层中,在需要使用到这个方法或者属性的地方直接import即可

基本使用

参照上面的ToastUtil,我们可以将其修改为如下的样子

package com.example

import android.content.Context
import android.widget.Toast

private var mToast: Toast? = null

fun showToastWithKotlin(context: Context, toastContent: String) {
mToast.apply {
Toast.makeText(
context,
toastContent,
Toast.LENGTH_SHORT).also {
mToast = it.apply {
show()
}
}
}
}

fun showToastWithKotlin(context: Context, toastContentResID: Int) {
showToastWithKotlin(context, context.resources.getString(toastContentResID))
}

在其他的Kotlin的类中引用这个函数的时候,我们可以这样引用

//Android Studio自动引入包
import com.example.showToastWithKotlin

showToastWithKotlin(context, "demo")

而在其他的Java的类中引用这个函数的时候,我们可以这样引用

ToastUtilKotlinKt.showToastWithKotlin(context, "demo");

反编译看看

上述的Java引用代码中,有细心的小伙伴可能看到我们的这个类不是叫ToastUtilKotlin吗?为什么具体使用的时候用的是ToastUtilKotlinKt,关于这个问题我们可以反编译ToastUtilKotlin这个类看下

@Metadata(
mv = {1, 1, 18},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000 \n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u000e\n\u0000\u001a\u0016\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u00052\u0006\u0010\u0006\u001a\u00020\u0007\u001a\u0016\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u00052\u0006\u0010\b\u001a\u00020\t\"\u0010\u0010\u0000\u001a\u0004\u0018\u00010\u0001X\u0082\u000e¢\u0006\u0002\n\u0000¨\u0006\n"},
d2 = {"mToast", "Landroid/widget/Toast;", "showToastWithKotlin", "", "context", "Landroid/content/Context;", "toastContentResID", "", "toastContent", "", "app"}
)
public final class ToastUtilKotlinKt {
private static Toast mToast;

public static final void showToastWithKotlin(@NotNull Context context, @NotNull String toastContent) {
Intrinsics.checkParameterIsNotNull(context, "context");
Intrinsics.checkParameterIsNotNull(toastContent, "toastContent");
Toast var2 = mToast;
boolean var3 = false;
boolean var4 = false;
int var6 = false;
Toast var7 = Toast.makeText(context, (CharSequence)toastContent, 0);
boolean var8 = false;
boolean var9 = false;
int var11 = false;
boolean var13 = false;
boolean var14 = false;
int var16 = false;
var7.show();
mToast = var7;
}

public static final void showToastWithKotlin(@NotNull Context context, int toastContentResID) {
Intrinsics.checkParameterIsNotNull(context, "context");
String var10001 = context.getResources().getString(toastContentResID);
Intrinsics.checkExpressionValueIsNotNull(var10001, "context.resources.getString(toastContentResID)");
showToastWithKotlin(context, var10001);
}
}

  • 顶层文件会被反编译为一个容器类,类名默认是顶层文件名+”Kt“后缀,注意容器类名可以自定义

    通过Kotlin中的@file:JvmName(“自定义生成的类名”)注解就可以自动生成对应的Java调用类名,注意需要放在文件的顶部,在package声明的前面

    @file:JvmName("TestClassName")
    package com.example
  • 顶层函数被反编译之后会编译为一个static静态函数,如代码中的两个showToastWithKotlin函数

关于顶层函数的补充

是否可以通过@file:JvmName注解实现将两个Kotlin中的文件在Java调用的时候”同名“?

答案是可以的,举个例子:

我们定义了两个不同的Kotlin文件,分别叫做ToastUtilKotlinKt和ToastUtilNotKotlinKt

image-20210306162807873.png

两个类的代码为:

@file:JvmName("TestClassName")
package com.example

import android.content.Context
import android.widget.Toast

private var mToast: Toast? = null

fun showToastWithKotlin(context: Context, toastContent: String) {
mToast.apply {
Toast.makeText(
context,
toastContent,
Toast.LENGTH_SHORT).also {
mToast = it.apply {
show()
}
}
}
}

fun showToastWithKotlin(context: Context, toastContentResID: Int) {
showToastWithKotlin(context, context.resources.getString(toastContentResID))
}
@file:JvmName("TestClassName")
package com.example

import android.content.Context
import android.widget.Toast

private var mToast: Toast? = null

fun showToastNotWithKotlin(context: Context, toastContent: String) {
mToast.apply {
Toast.makeText(
context,
toastContent,
Toast.LENGTH_SHORT).also {
mToast = it.apply {
show()
}
}
}
}

fun showToastNotWithKotlin(context: Context, toastContentResID: Int) {
showToastWithKotlin(context, context.resources.getString(toastContentResID))
}

我们在Java调用的时候就可以同名了

image-20210306163048887.png

什么时候使用Object、什么时候使用顶层函数呢?

Android Studio提供了一个强制把代码从Java转为Kotlin的方法,我们在强转Util的时候往往最后转出来的并不是顶层函数,而是一个Object类。例如:

package com.example

import android.content.Context
import android.widget.Toast

object ToastUtil {
private var mToast: Toast? = null
fun showToast(context: Context?, toastContent: String?) {
if (mToast != null) {
mToast!!.cancel()
}
mToast = Toast.makeText(context, toastContent, Toast.LENGTH_SHORT)
mToast?.show()
}

fun showToast(context: Context, toastContentResId: Int) {
showToast(context, context.resources.getString(toastContentResId))
}
}

这个类是我把ToastUtil从Java强转为了Kotlin之后得到的代码。

那问题来了,我们应该如何判断和评估,我们到底是应当使用一个Object类呢,还是说使用一个Top-level的函数?

我查看了下StackOverflow和Kotlin的社区,看到Kotlin的开发者给出的结论是:

The recommended practice is to never use object for creating namespaces, and always use top-level declarations when possible. We haven’t found name conflicts to be an issue, and if you do get a conflict, you can resolve it using an import with alias.这个是讨论的连接,感兴趣的伙伴可以看下下面的讨论https://discuss.kotlinlang.org/t/best-practices-for-top-level-declarations/2198

最后我自己关于这个问题的回答是,工具类方法尽量写成Top-level(可以写成多个kt文件,每个文件中有不同场景的顶层函数),而Object Class当做单例来使用

顶层属性

既然有顶层方法,应该也有顶层属性。和顶层函数一样,属性也可以放在文件的顶层,不附属与任何一个类。这种属性叫顶层属性。

package com.example

import android.content.Context
import android.widget.Toast

var mToast: Toast? = null

顶层属性和其他任意属性一样,都提供对应的访问器(val 变量提供getter,var 变量提供getter和 setter)。也就是说,当Java访问该顶层属性时,通过访问器进行访问的。

扩展函数

当我们缺少某个类的所有权,或者由于某个类不允许继承的时候,我们需要扩展该类的功能。为此,Kotlin创建了名为扩展的特殊声明,Kotlin支持扩展函数和扩展属性

什么是扩展函数以及扩展函数的组成

概念上讲,一个扩展函数是这样一个东西:它是一个可以作为一个类成员进行调用的函数,但是定义在这个类的外部,通过扩展函数你可以给已有的类去添加额外的函数,而且既不需要改动源代码,也不需要写子类。

扩展函数的语法组成是这样的:

img

扩展函数的写法

扩展函数写在哪里都可以,但是写的位置不同,效果也不同

举个例子

我们在我们的日常开发中,经常都会使用到RecyclerView这个控件,那么在自定义Adapter的时候,我们时常会面临需要根据不同的viewType加载不同的ViewHolder

image-20210309235804068.png

我们会发现一个问题,这段代码中有部分的重复代码,也就是这一段

image-20210309235417641.png

这一段中,我们可以看到基本上变化的也就是我们中间的resource ID,这种情况我们完全可以通过扩展函数让这段代码变得更加简洁

image-20210309235848196.png

这就是一个典型的成员扩展函数的例子,它作为成员函数存在于一个类的内部,我们是没办法在这个类之外使用这个扩展函数的。那我们有什么办法能够让更多的类用上这个扩展函数呢?答案就是上面说过的顶层函数,通过顶层函数我们可以在更多的onCreateViewHolder内部使用到这种参数结构更为简洁的函数

扩展函数的其他类型

上面我们看到的扩展都是带有固定类型的扩展,例如我们上面写的扩展函数是针对于ViewGroup的扩展,那么是否可以在一个可空的类上定义扩展函数呢?答案是可以的,这里参考一个Kotlin中文站的例子

fun Any?.toString(): String {
if (this == null) return "null"
//判空检查之后,this会自动转为非空类型,所以下面的toString()解析为Any类的成员函数
return toString()
}

我们在实际的编码中往往也可以看到伴生对象–Companion objects的存在,伴生对象也是可以添加扩展函数的,举个例子:

在实际的编码中,我们时常会有需要获取实例,一般来说我们会这样去写:

companion object {
const val VIEW_TYPE_CLICKABLE = 0
val instance: SupportRecyclerAdapter
get() = SupportRecyclerAdapter()
}

//-------使用-----------
SupportRecyclerAdapter.instance

我们也可以添加一个扩展函数

companion object {
const val VIEW_TYPE_CLICKABLE = 0
val instance: SupportRecyclerAdapter
get() = SupportRecyclerAdapter()
}

fun SupportRecyclerAdapter.Companion.getInstance(): SupportRecyclerAdapter {
return instance
}

//-------使用----------
SupportRecyclerAdapter.getInstance()

看了上面的扩展ViewGroup的例子,我们可以发现其实上面的功能除了用扩展函数来实现,我们也可以直接用一个Object Class或者顶层函数也可以实现,至于具体选择什么样的实现方式由开发者选择,例如:

Object InflateUtil {
......
fun inflate(parent: ViewGroup, layoutRes: Int): View {
return LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
}
}

关于引用

我们都知道,Kotlin中函数是可以通过**::**被指向的,例如:

mHeight = h.Float()

//等价于
(Int::toFloat)(h)

扩展函数也是可以被指向的,但是前提是必须是Top-level(顶层)的,例如,我们上面改造的扩展函数用的时候就可以写成这个样子

TextViewHolder(parent::inflate)(R.layout.item_toolbar_with_btn)

但是这个时候如果我们的函数是一个成员扩展函数,使用**::**指向函数会报错

image-20210309235848196.png

‘inflate’ is a member and an extension at the same time. References to such elements are not allowed

—-报错内容

函数的引用也可以赋值给变量,例如:

val hTemp = Int::toFloat
val hTemp: (Int) -> Float = Int::toFloat

//使用
mHeight = hTemp(h)
mHeight = hTemp.invoke(h)

扩展函数也是一样的

val textViewQuote: (Int) -> View = parent::inflate
val textViewQuote = parent::inflate

//使用
TextViewHolder(textViewQuote(R.layout.item_toolbar_with_btn))
TextViewHolder(textViewQuote.invoke(R.layout.item_toolbar_with_btn))

静态解析

在JVM语言的堕胎中,被重写的方法的调用是依据其调用对象的实际类型进行的,但是扩展函数却是静态分发的,即意味着扩展函数是由其所在表达式中的调用者的类型来决定的,我们可以写一个例子来实验一下

fun View.static() {
Log.d(TAG, "static with view: ")
}

fun Button.static() {
Log.d(TAG, "static with button: ")
}

var view: View = Button(this)
view.static

最后输出发现,调用的是View的static函数。究其原因,即使当前的变量是一个Button对象,但是因为扩展函数所在的表达式中,view是View类型而不是Button类型的

在Kotlin和Java中我们都知道类的成员函数是可以被重写的,子类是可以重写父类的成员函数的,但是子类是不可以重写父类的扩展函数的

这个时候如果我们执行以下的代码

Button(this).static
View(this).static

执行结果是他们分别去执行了对应的类型的扩展函数中的static方法,但是这个情况本质来说是因为扩展函数是一个静态方法而不是类方法,所以是因为参数类型更加匹配选择的子类方法。

扩展属性

一般来说,我们做dp转像素值会通过一个叫做getDimensionPixelSize的函数来做,例如

resources.getDimensionPixelSize(R.dimen.search_result_chip_limited_height)

但有的时候,我们需要在程序运行的时候将获取到的px转为dp值,这个时候我们就可以扩展下一个Float类型的属性,例如

val Float.dp
get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics)
//使用
200f.dp

这个扩展出来的dp就是一个扩展属性

中缀调用

中缀调用给人的感觉更像是一种语法糖,当程序的逻辑和调用链复杂的时候可以让我们的程序以一种更加贴近自然语言的方式来写,更加可读,另外一点就是Kotlin本身也预置了挺多的中缀调用,如果当前与之的中缀调用的操作符不满足我们的需求的时候,我们可以通过这种方式进行扩展

实现中缀调用必须满足三个条件:

  • 函数必须为成员函数或者扩展函数
  • 必须只有一个参数
  • 使用infix关键字修饰

举个例子

我们在上一节的时候看到了一个扩展函数的示例,我们继续以这个扩展函数来做中缀调用的示例。我们可以用infix关键字修饰这个方法

infix fun ViewGroup.inflate(layoutResID: Int): View {
return LayoutInflater.from(context).inflate(layoutResID, this, false)
}

此时我们的程序可以修改为:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == VIEW_TYPE_CLICKABLE) {
ClickableViewHolder(parent inflate R.layout.item_recycler_view_clickable)
} else {
UnClickableViewHolder(parent inflate R.layout.item_recycler_view_unclickable)
}
}

这个就是一个扩展函数的中缀调用的例子,我们可以再举例一个成员变量的示例

在我们的Adapter中,我们会添加ClickListener,例如

fun setItemClickListener(listener: ItemClickListener) {
mListener = listener
}

//具体使用为
val recyclerAdapter = SupportRecyclerAdapter(resources.getStringArray(R.array.android_knowledge_kind)).apply {
setItemClickListener(object: SupportRecyclerAdapter.ItemClickListener {
override fun onItemClick(adapterPosition: Int) {
refreshUI(adapterPosition)
}
})
}

通过中缀调用,我们可以把这个函数改造成这个样子

infix fun setItemClickListener(listener: ItemClickListener) {
mListener = listener
}

//具体使用为
recyclerAdapter setItemClickListener object : SupportRecyclerAdapter.ItemClickListener {
override fun onItemClick(adapterPosition: Int) {
refreshUI(adapterPosition)
}
}

个人看法,中缀调用更多的像是一种语法糖,虽然我们可以通过更加贴近自然语言的计算机语言去写程序,但是限制也是很多的,不是所有的函数都可以写作中缀调用,酌情使用即可