Kotlin Primer·第五章·函数与闭包

Stella981
• 阅读 724

国内目前已经有几家公司开始大规模使用 Kotlin 开发,沪江就是其中一个。本文来自沪江工程师之手,且看他怎么认识 Kotlin,欢迎大家关注他的博客——http://kymjs.com/ ,也欢迎大家关注 Kotlin 中文博客 http://www.kotliner.cn/


函数与闭包的特性可以算是 Kotlin 语言最大的特性了。

5.1 函数

即使 Kotlin 是一门面向对象的编程语言,它也是有函数的概念的——而不像 Java 那样,仅仅有“方法”。
回顾一下前面第二章讲述的函数声明语法:

fun say(str: String): String {
    return str
}

函数使用关键字fun声明,如下代码创建了一个名为 say() 的函数,它接受一个 String 类型的参数,并返回一个 String 类型的值。

5.1.1 Unit

如果一个函数是空函数,比如 Android 开发中的 TextWatch 接口,通常只会用到一个方法,但必须把所有方法都重写一遍,就可以通过这种方式来简写:

editText.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) = Unit

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
})

Unit 表示的是一个值的类型。 这种类型对应于Java中的void类型。

5.1.2 Nothing

如果一个函数不会返回(也就是说只要调用这个函数,那么在它返回之前程序肯定出错了,比如一定会抛出异常的函数),理论上你可以随便给他一个返回值,通常我们会声明为返回 Nothing 类型。我们看到 Nothing 的注释:

/**
 * Nothing has no instances. You can use Nothing to represent "a value that never exists": for example,
 * if a function has the return type of Nothing, it means that it never returns (always throws an exception).
 */
public class Nothing private constructor()

没有任何实例。 您可以使用 Nothing 来表示“永远不存在的值”.

5.2 复杂的特性

5.2.1 嵌套函数

Kotlin 的函数有一些非常有意思的特性,比如函数中再声明函数。

fun function() {
  val valuesInTheOuterScope = "Kotlin is awesome!"

  fun theFunctionInside(int: Int = 10) {
    println(valuesInTheOuterScope)
    if (int >= 5) theFunctionInside(int - 1)
  }
  theFunctionInside()
}

与内部类有些类似,内部函数可以直接访问外部函数的局部变量、常量,这种写法通常使用在 会在某些条件下触发递归的方法内,在一般情况下是不推荐使用嵌套函数的。

5.2.2 运算符重载

fun main(args: Array<String>) {
  for (i in 1..100 step 20) {
    print("$i ")
  }
}

这段函数将会输出 1 21 41 61 81  

这段神奇的循环是怎么执行的?

in关键字,在编译以后,会被翻译为一个迭代器方法,其源码可以在Progressions类中查看到

override fun iterator(): IntIterator = IntProgressionIterator(first, last, step)

/**
 * An iterator over a progression of values of type `Int`.
 */
internal class IntProgressionIterator(first: Int, last: Int, val step: Int) : IntIterator() {
    private var next = first
    private val finalElement = last
    private var hasNext: Boolean = if (step > 0) first <= last else first >= last

    override fun hasNext(): Boolean = hasNext

    override fun nextInt(): Int {
        val value = next
        if (value == finalElement) {
            hasNext = false
        }
        else {
            next += step
        }
        return value
    }
}

in 关键字之后,还有两个点的..,他表示一个封闭区间,其内部实现原理是通过运算符重载来完成的。首先看到他的函数定义,你可以在 Int 类的源码中找到:

 /** Creates a range from this value to the specified [other] value. */
public operator fun rangeTo(other: Int): IntRange

运算符重载需要使用关键字operator修饰,其余定义与函数相同。
通过源码看到,上面的代码实际..的原理实际上就是对一个 Int 值,调用他的 rangeTo方法,传入一个 Int 参数,并返回一个区间对象。
带入到上面的代码,实际上就是把..看做是方法名,调用 1 的rangeTo方法,传入 100 作为参数,会返回一个区间对象。 然后再用迭代器 in 便利区间中的每一个值。
所以上面那种写法改写为下面这样,依旧是能正常运行的。

for (i in 1.rangeTo(100) step 20) {
    print("$i ")
}

说道运算符给大家讲个笑话,在 C/C++/Java 中,其实有一个大家经常使用但是没有人知道的运算符,叫趋近于写法为 -->,例如下面的代码:
int i = 10;
while(i → 0){
   printf(“%d”, i);
}
这个代码运行完后将会依次打印 10 到 0 数字。不信你试试

5.2.3 中缀表达式

运算符的数量毕竟是有限的,有时并不一定有合适的。例如上面代码中的步长这个意义,就没有合适的运算符可以标识。
这时候我们可以用一个单词或字母来当运算符用(其本质还是函数调用),叫做中缀表达式,所谓中缀表达式就是不需要点和括号的方法调用
你可以在 Reangs 中看到step源码声明:

public infix fun IntProgression.step(step: Int): IntProgression {
    checkStepIsPositive(step > 0, step)
    return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}

中缀表达式需要用infix修饰,从源码看到,在 SDK 中定义了一个叫 step 的方法,最终返回一个IntProgression对象,这个对象最终会被作用到 in,也就是迭代器的第三个参数step上。

5.2 闭包

其实在 Kotlin 中与其说一等公民是函数,不如说一等公民是闭包。

例如在 Kotlin 中,你可以写出这种怪异的代码

fun main(args: Array<String>) {
    test
}
val test = if (5 > 3) {
    print("yes")
} else {
    print("no")
}

当然,我们都知道这段代码永远都只会输出yes。
这里只是为了演示,if 语句仍旧是一个闭包。而事实上,上文包括前文讲到的所有:函数、Lambda、if语句、for、when,都可以称之为闭包,但通常情况下,我们所说的闭包是 Lambda 表达式。

5.2.1 自执行闭包

自执行闭包就是在定义闭包的同时直接执行闭包,一般用于初始化上下文环境。
例如:

{ x: Int, y: Int ->
    println("${x + y}")
}(1, 3)

5.3 Lambda

5.3.1 Lambda 表达式

Lambda 表达式俗称匿名函数,熟悉Java的大家应该也明白这是个什么概念。Kotlin 的 Lambda表达式更“纯粹”一点, 因为它是真正把Lambda抽象为了一种类型,而 Java 8 的 Lambda 只是单方法匿名接口实现的语法糖罢了。

val printMsg = { msg: String -> 
    println(msg) 
}

fun main(args: Array<String>) {
  printMsg.invoke("hello")
}

以上是 Lambda 表达式最简单的实例。
首先声明了一个名为printMsg的 Lambda,它接受一个 String 类型的值作为参数,然后在 main 函数中调用它。如果还想省略,你还可以在调用时直接省略invoke,像函数一样使用。

fun main(args: Array<String>) {
  printMsg("hello")
}

Lambda 表达式还有非常多的语法糖,比如

  • 当参数只有一个的时候,声明中可以不用显示声明参数,在使用参数时可以用 it 来替代那个唯一的参数。

  • 当有多个用不到的参数时,可以用下划线来替代参数名(1.1以后的特性),但是如果已经用下划线来省略参数时,是不能使用 it 来替代当前参数的。

  • Lambda 最后一条语句的执行结果表示这个 Lambda 的返回值。

需要注意的是:闭包是不能有变长参数的
例如前面讲过变长参数的函数,但是闭包的参数数量是必须固定的。

fun printLog(vararg str: String) {
}

5.3.2 高阶函数

Lambda 表达式最大的特点是可以作为参数传递。当定义一个闭包作为参数的函数,称这个函数为高阶函数。

fun main(args: Array<String>) {
    log("world", printMsg)
}

val printMsg = { str: String ->
    println(str)
}

val log = { str: String, printLog: (String) -> Unit ->
    printLog(str)
}

这个例子中,log 是一个接受一个 String 和一个以 String 为参数并返回 Unit 的 Lambda 表达式为参数的 Lambda 表达式。
读起来有点绕口,其实就是 log 有两个参数,一个str:String,一个printLog: (String) -> Unit。

5.3.3 内联函数

在使用高阶函数时,一定要知道内联函数这个东西。它可以大幅提升高阶函数的性能。
官方文档的描述是这样的:使用 高阶函数 在运行时会带来一些不利: 每个函数都是一个对象, 而且它还要捕获一个闭包, 也就是, 在函
数体内部访问的那些外层变量. 内存占用(函数对象和类都会占用内存) 以及虚方法调用都会带来运行时的消耗.

但是也不是说所有的函数都要内联,因为一旦添加了inline修饰,在编译阶段,编译器将会把函数拆分,插入到调用出。如果一个 inline 函数是很大的,那他会大幅增加调用它的那个函数的体积。

5.4 小结

闭包应该算是 Kotlin 最核心特性之一了。
使用好闭包可以让代码量大大减少,例如 Kotlin 最著名的开源库:Anko,使用 Anko 去动态代码布局,比使用 Java 代码配合 xml 要更加简洁。

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        MyActivityUI().setContentView(this)
    }
}

class MyActivityUI : AnkoComponent<MyActivity> {
    override fun createView(ui: AnkoContext<MyActivity>) = ui.apply {
        verticalLayout {
            editText()
            button("Say Hello") {
                onClick { ctx.toast("Hello!") }
            }
        }
    }.view
}

可以看到,充分运用了闭包的灵活性,省略了很多的临时变量和参数声明。
然而,也正是因为闭包的灵活性,造成如果泛滥的话,可能会写出可读性非常差的代码(这里就不举反例了, js 的 lambda 滥用的结果就能想象了)

Kotlin Primer·第五章·函数与闭包

本文分享自微信公众号 - Kotlin(KotlinX)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这