Kotlin 旅途之函数

Kotlin 旅途之函数

每篇文章一句“名言”

代码即结构,解决问题,先把问题分解。分解的过程就是实现的过程,这就是函数式编程的最朴素的思想。

函数

作为最最基本的语言要素,先来看看函数的面貌吧!

看起来是什么样的

不同于 JavaKotlin 函数的返回类型放在函数声明最后,用关键字 fun 修饰,函数即“乐趣”。

默认参数和命名参数

函数参数可以有默认值,这样就可以在省略某些参数时,采用其默认值,可以减少重载数量;另外,也支持命名参数调用:

上边的函数定义了一个扩展函数,把一个集合中的元素拼接成字符串,传入三个参数,第一个是分隔符,第二个是前缀,最后一个参数是后缀。举几个例子:

// 定义一个集合 books
val books = listof ("宇宙的琴弦", "复杂", "生命是什么")

// 什么参数也不传,将会采用默认参数
books.joinToString() // 宇宙的琴弦,复杂,生命是什么

// 传入第一个参数(顺数参数)
books.joinToString("、") // 宇宙的琴弦、复杂、生命是什么

// 指定命名参数调用(可以不按照顺序)
books.joinToString(prefix="书籍:") // 书籍:宇宙的琴弦、复杂、生命是什么
复制代码

上边的代码代表了不同的调用方式,注释是其运行之后的结果,已经比较详细,就不多说了。

关于函数,有一些特性我们看看:

表达式函数

Kotlin 支持单表达式函数,即函数返回单个表达式时,可以省略花括号,并在【=】后面指定代码体即可:

如果返回值可以推断的话,也可以省略返回类型的声明:

中缀函数

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

然后我们就可以这样调用:
以上两种方式是等效的,整个表达式返回一个 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 还提供了更加细致的控制手段,比如 noinlinecrossinline 等,具体就不展开说了,可以参考官方文档

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 表示的,那么运行的时候一定会有一个实例类与之对应(因为非内联),也就是我们 returnaction 其实就是 invoke,那么就很清楚了,这么做没有任何意义,还会产生歧义,自然不能这么干。

DSL

DSL(Domain Specified Language)是特定领域语言的缩写,比如 gradle 构建系统就用 groovy 语言来实现 DSL 特性。比如对于一个 Android 项目,我们有可能会有如下配置:

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

相关同学应该知道有一个 anko 的库,可以实现在 Androidhtml 一样用代码而不是 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 了。

预告

下一篇文章会说一下泛型和一些隐性开销,可以让你在实践中,做到有的放矢:

  • 泛型
  • 隐性成本
  • 填坑

敬请关注


另外原文有任何错误改进之处,欢迎联系我修正改进,任何疑问也可以联系我交流。欢迎订阅点赞哦,不定期更新~

声明:此为原创,转载请联系作者

声明

本文章首次发布于 我的专栏

原文链接

转载于:https://juejin.im/post/5b5dd0adf265da0f836495fa

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值