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
如出一辙。
还是上边的函数,我们继续实验:
这次我们在 action 中直接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
了。
预告
下一篇文章会说一下泛型和一些隐性开销,可以让你在实践中,做到有的放矢:
- 泛型
- 隐性成本
- 填坑
敬请关注
另外原文有任何错误改进之处,欢迎联系我修正改进,任何疑问也可以联系我交流。欢迎订阅点赞哦,不定期更新~
声明:此为原创,转载请联系作者
声明
本文章首次发布于 我的专栏