Kotlin 旅途之函数
每篇文章一句“名言”
代码即结构,解决问题,先把问题分解。分解的过程就是实现的过程,这就是函数式编程的最朴素的思想。
函数
作为最最基本的语言要素,先来看看函数的面貌吧!
看起来是什么样的

不同于 Java,Kotlin 函数的返回类型放在函数声明最后,用关键字 fun 修饰,函数即“乐趣”。
默认参数和命名参数
函数参数可以有默认值,这样就可以在省略某些参数时,采用其默认值,可以减少重载数量;另外,也支持命名参数调用:

// 定义一个集合 books
val books = listof ("宇宙的琴弦", "复杂", "生命是什么")
// 什么参数也不传,将会采用默认参数
books.joinToString() // 宇宙的琴弦,复杂,生命是什么
// 传入第一个参数(顺数参数)
books.joinToString("、") // 宇宙的琴弦、复杂、生命是什么
// 指定命名参数调用(可以不按照顺序)
books.joinToString(prefix="书籍:") // 书籍:宇宙的琴弦、复杂、生命是什么
上边的代码代表了不同的调用方式,注释是其运行之后的结果,已经比较详细,就不多说了。
关于函数,有一些特性我们看看:
表达式函数
Kotlin 支持单表达式函数,即函数返回单个表达式时,可以省略花括号,并在【=】后面指定代码体即可:


中缀函数
用中缀表示法调用,需要用 infix 修饰一个函数:
我们先定义一个 Book类,然后定义一个 Desktop 类,包含一个 contains 方法,然后用 infix 修饰一个扩展函数 on,内部实现就是调用了 Desktop 的 contains 函数。


Boolean。好用,但是一般情况下不建议滥用,最好能有团队内部规约,方便代码维护。
局部函数
即一个函数嵌套在另一个函数内部,除非逻辑需要,否则尽量不要这样做,尤其是这个局部函数捕获了闭包的变量,这会为其生成一个实例对象:

a,竟然没有 final, 看看其背后的实现:

final 成员持有 a 的值。
高阶函数
函数类型
在看高阶函数之前先看一下函数类型在 Kotlin 中的表示形式
先定义一个函数,如下,这个函数是一个单表达式函数,返回一个 if 表达式:

(Int, Int) -> Int
这是一个接收两个Int参数返回一个Int的函数类型,那么既然是类型,就符合 Kotlin 中的类型系统,以上的函数类型当然也可以声明为可空的:
((Int, Int) -> Int)?
高阶函数
高阶函数就是把函数用作参数或返回值的函数,比如现在就来实现一个filter函数:

Iterable<T>,元素类型是泛型,注意到它的参数是一个函数类型 (T) -> Boolean,它的意思是说给一个 T 类型的元素,经过一个操作返回一个 Boolean 类型的值。同时整个 filter 函数返回一个 List<T> 类型的列表。函数实现是先创建了一个新的列表,然后用传入的函数挨个判断 Iterable 元素是否符合条件,符合条件的就加入这个创建的新列表,迭代完毕返回这个新列表,就完成了过滤操作。
我们还注意到这个函数最开始用 inline 修饰,它的意思是“内联”,很多语言都有这个概念。想一想,如果我们很多高阶函数用起来短小精悍,频繁而方便,但是导致大量调用会增加中间多一层的函数调用成本,就需要保存和恢复栈以及两次对于的跳转,美中不足。所以针对这种情况增加了这个修饰符,它告诉编译器,这个函数被调用时,就把这段代码拷贝到调用处,这样多出来的开销就被解决了。从哲学上说事物都有两面性,开销解决了,但是如果一个内联函数足够大,调用的地方足够多,就会产生代码膨胀,最终导致体积的增大,所以掌握一个平衡,尽量保证内联函数短小非常重要。同时 Kotlin 还提供了更加细致的控制手段,比如 noinline、crossinline 等,具体就不展开说了,可以参考官方文档。
lambda 表达式
接下来我们用不同的方式调用一下上边定义的 filter 函数:
先来第一种方式,我们前边声明的函数类型拿来用用,声明一个与其参数类型相同的函数:

(Int) -> Boolean,同时赋值给了 predicate 变量,这种方式叫做匿名函数。
相似的我们也可以这样做:

predicate 变量,这种方式叫做 lambda 表达式,对比以上,可以很容易的找到语义的对应。
构造一个集合调用之:

predicate 定义,我们知道这将会把其中的正数过滤出来即 [1, 2]
来看看第二种更加直接的调用 filter 的方式。上边我们还经过了一个变量的转手,那么我们是不是可以直接把上边的 lambda 表达式传过去,就像这样:

Kotlin 提供给我们的约定,我们还能优化一下:

filter 函数最后一个参数是一个 lambda,按照约定我们把它拿出到小括号外边;
第二步,因为没有别的参数了,所以小括号内空空如也,当然也可以去掉;
第三步,这个 lambda 表达式只有一个参数 i ,那么如果不写它,编译器会提供一个默认的 it 来代替,这就是我们的最终调用版本了。
虽然高阶函数版本众多,但是万变不离其宗,其基本原理都是如此。
在 Kotlin 中,其实所有的lambda表达式或者函数都具有函数类型,这些类型根据参数的不同分别对应着不同的 FunctionX,比如上边的 predicate 就是如下形式:

Kotlin 支持最多 22 个参数的函数类型,基本上够用了。看到图上有一个 operator 的修饰符,它表示操作符重载,很多语言像如 C++、Scala 都支持操作符重载,例子中的 invoke 函数就是对 () 的重载,也即以下两种调用完全是等价的:
foo()
foo.invoke()
不是所有的方法都能用 operator 修饰的,必须是 Kotlin 中声明的运算符才可以,具体可参见官方文档。
返回
函数返回分为裸返回(return)和带限定的返回(return@xxx),什么意思呢?还记得之前说过的内联函数吗?既然内联代码会被织入调用处,那么当我们 return 的时候,我们在返回什么?
接下来探究下原因吧。我们先定义一个函数如下:

filter 函数的探讨,这个 forEach 对我们来说简直易如反掌,就不再多说了。我们就用它来做一个实验看看。

return@forEach 日志,遍历完成之后输出 “我运行了”。有个小细节,我们在每一个 action 中都进行了一次判断,若是当前元素大于等于 0 我们就做一个带限定的返回。
右边是这段代码的执行结果,有一点点出乎我们的意外,并不是我们原先所认为的循环内只输出两次字符串,执行的 return@forEach 并没有退出 forEach 函数的循环执行,而只是提前中断了本次执行,行为和 continue 如出一辙。
还是上边的函数,我们继续实验:

return ,这次符合我们预期的是,循环内只有两次输出,但是又有一点很诡异,“我运行了”并没有输出,当然这个代码块一定是包含在否一个 fun 中的,所以我们猜测应该是令包裹它的外部函数返回了。
综合上边的结果,我们可以认为,裸 return 总是使得最近的 fun 函数返回。另外,我们注意到以上所述均针对内联函数,那么非内联的高阶函数又会如何呢?我也做了一个测试:


lambda 表达式是用 FunctionX 表示的,那么运行的时候一定会有一个实例类与之对应(因为非内联),也就是我们 return 的 action 其实就是 invoke,那么就很清楚了,这么做没有任何意义,还会产生歧义,自然不能这么干。
DSL
DSL(Domain Specified Language)是特定领域语言的缩写,比如 gradle 构建系统就用 groovy 语言来实现 DSL 特性。比如对于一个 Android 项目,我们有可能会有如下配置:

Kotlin 也支持 DSL,现在我们多了一个选择:用 Kotlin 实现上边的构建配置。这样做有几个好处,由于静态类型语言的优势,首先各种 IDE 提示又回来了,另外安全性也增强了,另外也不需要专门熟悉 groovy 这门新语言了(这个不知是不是好处 摊手),看看实现效果:

相关同学应该知道有一个 anko 的库,可以实现在 Android 像 html 一样用代码而不是 xml 写各种布局。好处就是性能相比运行时解析 xml 来说提升了,当然预览会麻烦一些。有兴趣可以研究一下。
对于高阶函数,我们还有最后一个需要说的,对于内联函数,这个在实际操作中还是很有用处的,提供了很多便利。 ### 具体化类型参数 众所周知,JVM 上因为历史包袱,我们并不能享受真正泛型在运行时带来的优势,因为泛型擦除,使得泛型仅剩下编译期保证类型安全的职能,和 `C++、C#` 相比相当于“阉割版”泛型实现,而**具体化类型参数**的横空出世,多少给了我们一些安慰。
对于内联函数,编译器知道每一处内联的具体类型,所以可以提供一些便利,让我们能直接拿到泛型的具体类型信息。我们改一下上边的 filter 函数,让它看起来能够识别泛型参数的具体类型:

filterIsInstance 实现好了,看到泛型 R 前边多了一个 reified,它代表这是一个具体化的泛型,也就是一定程度上我们可以获取更多的信息,element is R 这个条件相当于在运行时去判断当前元素是否是 R 的实例。

再看一个 Android 中的例子:
先声明一个扩展函数

Intent 时直接是用了 T::class.java 这样的方式

T::class.java 的值就是 HistoryActivity.class
原理你一定猜到了,我们看下背后实现:

T 已经被替换为了 Activity.class,而对于 startActivity<HistoryActivity>()就是 HistoryActivity.class 了。
预告
下一篇文章会说一下泛型和一些隐性开销,可以让你在实践中,做到有的放矢:
- 泛型
- 隐性成本
- 填坑
敬请关注
另外原文有任何错误改进之处,欢迎联系我修正改进,任何疑问也可以联系我交流。欢迎订阅点赞哦,不定期更新~
声明:此为原创,转载请联系作者
声明
本文章首次发布于 我的专栏