前置知识
- 有
Java
编程基础 - 已学习 Kotlin 变量知识
前言
上一篇文章 我们讲述了 kotlin
中变量与 Java 中的变量的不同,同时也简单说明 kotlin
中的类型推导机制、val
修饰的变量是 引用不可变而非对象不可变 以及我们需要优先使用 val
这些知识。
本文给讲述 kotlin
中的 头等公民 ,函数。
函数
kotlin 中的函数与 Java 中的不同
在本系列开篇文章中有提到,kotlin 在兼顾 Java 规范的同时,又做了很多拓展;其中,最重要的拓展就是:kotlin 是支持部分函数式特性的。
函数式编程具有五个鲜明的特点。
1、函数是"第一等公民" 所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
2、只用"表达式",不用"语句" "表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
3、没有"副作用" 所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
4、不修改状态 上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。
5、引用透明性 函数程序通常还加强引用透明性,即如果提供同样的输入,那么函数总是返回同样的结果。就是说,表达式的值不依赖于可以改变值的全局状态。
kotlin 中函数式的特性在接下来的学习中,我们会逐步学习到。下面我们先来看一下,在 kotlin 中使用函数与 Java 有何不同。
//Java public int numsAdd(int x ,int y){ return x+y; } 复制代码
上面 Java 中的函数形式大家都很熟悉了,那么在 kotlin 中,这段函数该如何写呢?
//kotlin public fun numsAdd(x: Int ,y: Int): Int{ return x+y } 复制代码
上述的 kotlin 代码,是将 java 代码按照原来的逻辑,一点都不变的翻译而来的。但事实上,这却并不符合 kotlin 的代码特性。我们先对这段代码进行解释,后面再将其修改为符合 kotlin 特性的代码。
kotlin 中的函数有以下几个基本特性
fun
是 function 的意思,代表这是一个功能,意思是这是一个方法,一个函数。在 kotlin 中,每一个 方法 都要使用fun
来修饰。- 方法中返回值的声明,和前文类型声明一样,采用
<name>:<object>
形式,在参数括号后用:接上类型即可。若无函数类型声明,则返回 Unit 类型,效果与 Java 中的 void 函数一样。 - 参数声明也是使用
<name>:<object>
的形式;若无参数,使用空括号即可。
事实上,若按照逻辑从 Java 中翻译过来,上一段代码中的 public
应该删除。这是为什么呢?
因为 Java 中的可见性修饰符 public ,在 Kotlin 中是不必声明的,因为 kotlin 中默认修饰符就是 public,所以我们可以将上述代码更改为如下。
//kotlin fun numsAdd(x: Int ,y: Int): Int{ return x+y } 复制代码
同时,这里附上修饰符对比表格供大家参考。
修饰符 | Java | Kotlin |
public | 所有类可见 | 所有类可见**(默认)** |
private | 当前类可见 | 当前类可见 |
protected | 当前类、子类、同一包路径下的类可见 | 当前类、子类可见 |
default | 同一包路径下的类可见**(默认)** | 无 |
internal | 无 | 同一模块中的类可见 |
前面我们还说到,kotlin 是支持函数式的,那如何把这段代码 函数式 化呢?我们可以使用一个 = 来代替代码块函数体以及返回值,这种单行的表达称之为 表达式函数体
//函数化 fun numsAdd(x: Int ,y: Int): Int = x+y 复制代码
同时,基于 Kotlin 的类型推导机制,我们可以把函数返回值的类型声明去掉,更改为如下形式
//函数化+类型推导 fun numsAdd(x: Int ,y: Int) = x+y 复制代码
上述代码该到这里,就是妥妥的 Kotlin 风格的代码了。我们前后对照,是不是代码量比Java要少得多呢,但是其魅力远不止于此,我们继续探索学习吧!
Kotlin 中函数的类型推导不是全局的
上述的 Kotlin 代码中,我们最后一步更改是利用了 Kotlin 的类型推导功能,但是我在这里要告诉你的是,Kotlin 的类型推导不是全局的。
这是什么意思呢?何为不是全局的?我们用上述代码举一个例子,相信你很快就能明白。假设 Kotlin 的类型推导是全局的,那么当我们没将代码函数法,且我们想利用它的 类型推导特性 的时候,我们自然而然会写出如下的代码。
//假设支持全局类型推导 fun numsAdd(x: Int ,y: Int){ return x+y } 复制代码
但事与愿违,上述代码报错了。原因是类型缺失,意思是我们需要加上函数返回值的类型。可见,Kotlin 的类型推导并非那么智能,它的类型推导不是全局的。
那么,是不是只要我们使用到 函数表达式 ,Kotlin 就一定能进行类型推导了呢?非也,我们可以看下面的这段代码。
//未作声明,会报错 fun recursion(n: Int) = if (n == 0) 1 else recursion(n-1) 复制代码
这段代码依旧报错,因为递归的存在,它还是无法推导出函数的返回值类型。这是由于 Kotlin 继承和支持子类型的特性,导致类型推导不够聪明。
所以,遇到这些情况,我们做以下显式的类型声明就好了。
//做显式声明,代码成功 fun recursion(n: Int): Int = if (n == 0) 1 else recursion(n-1) 复制代码
那么,关于类型声明,我们该如何选择是否显式声明呢?
- 如果它是一个函数的参数?
必须使用- 如果它是一个非表达式定义的函数?
除了返回 Unit,其他情况必须使用- 如果它是一个递归函数?
必须使用- 如果它是一个公有方法,且具有返回值?
建议使用,可提高代码可阅读性以及稳定性
Lambda语法
Lambda语法,事实上在 Java 中已经出现了,当我们使用 JDK 1.8 以上,就可以在 Java 中使用了。
由于Kotlin 中的 lambda 语法使用起来是有很多技巧,且具有很多高级用法以及知识,所以在这里就只做和 Java 中 lambda 的一些简单区别展示。
首先我们看一下 ,lambda 是什么?
接下来谈众所周知的 Lambda 表达式。它是推动 Java 8 发布的最重要新特性。是继泛型(
Generics
)和注解(Annotation
)以来最大的变化。使用 Lambda 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程。
Lambda 表达式是一个匿名函数(初步这么理解就可,实际上是不一样的),java 8 允许把函数作为参数传递进方法中。
在 Java 中,能够使用 函数式API/lambda 的条件是,对应要表达的匿名内部类,这一个类是 单抽象方法接口 。意思是,这个类是接口,且接口里面只有一个抽象方法。
我们以常用的 button
设置点击的监听方法为例,看一下 Java 中的 lambda 实现。其中,把参数简写为 v ,并使用 -> 指向函数体
//包含匿名内部类 getBinding().homeTextViewFollower.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ... } }); //匿名内部类变为lambda形式 getBinding().homeTextViewFollower.setOnClickListener(v -> { ... }); 复制代码
同时,我们可以看到,该内部类就是 单抽象方法接口。
以上面代码为例,Kotlin 中使用 lambda
即为如下形式。由于参数只有一个,我们可以把其直接简写删除,且外层直接用 {} 表示,表明此处为 lambda 表达式。
getBinding().homeTextViewFollower.setOnClickListener{ ... } 复制代码
关于 lambda 表达式,先讲到这,后续更多的内容在后续文章中会逐步讲明。
如何传入函数作为参数
Kotlin 函数式的一大特点,就是支持在函数内写函数,或者是给函数传递函数,让函数成为第一等公民。
由于时间和篇幅原因,此处暂时不讲如何设置函数的参数可传入函数,只讲如何传入函数。关于如何设置函数参数可传入参数,我们后续讲到函数的高级应用时再说。下面的代码,可以实现向函数的参数中传入bind函数。
val binding = holder.getBinding(RecyclerviewItemRankBinding::bind) 复制代码
我们传入函数,用的是 :: 这个符号,其表示将 RecyclerviewItemRankBinding
这个类中的 bind
函数传入,可以很好的实现复用和传递。
同时,我们可以看一下 getBinding 这个函数的参数设计,但这里暂不做对应更多的解释。
@Suppress("UNCHECKED_CAST") fun <VB : ViewBinding> BaseViewHolder.getBinding(bind: (View) -> VB): VB = itemView.getTag(Int.MIN_VALUE) as? VB ?: bind(itemView).also { itemView.setTag(Int.MIN_VALUE, it) } 复制代码
上述参考代码来自 CymChad/BaseRecyclerViewAdapterHelper: BRVAH:Powerful and flexible RecyclerAdapter (github.com)