kotlin入门潜修之进阶篇—高阶方法和lambda表达式

本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

创作不易,如有转载,还请备注。

高阶方法和lambda表达式

在kotlin中,方法是一等公民。什么是一等公民?翻译成编程语言对应的意思就是:kotlin中的方法同一般的变量一样,可以作为方法参数、可以赋值给其他变量等等。

那么作为静态强类型限制语言的kotlin是怎么做到呢?那就是通过提供方法类型(function types)以及lambda表达式来达到目标的。本篇文章将阐述kotlin中的这些特性。

高阶方法

高阶方法是指,那些入参类型包含方法类型或者返回值是个方法类型的方法。

所以,理解高阶方法之前,必然要理解另外一个定义:方法类型(function types)。

什么是方法类型?其实方法类型和我们常见的数字类型、字符串类型等等是一样的,都是用作定义变量的类型。只不过这个类型看着并不像我们平常定义的其他类型那么容易理解。来看个例子:

fun sayHello(str: String, checkStr: (str: String) -> Boolean) {
    if (checkStr(str)) {
        println("pass...")
    } else {
        println("error...")
    }
}

上面代码定义了一个方法sayHello,这个方法唯一特别的地方就是该方法的第二个参数类型:checkStr: (str: String) -> Boolean 。我们咋一看可能就懵掉了:这个是什么鬼类型,完全看不明白!其实,这个类型就是方法类型!

方法类型的定义如同普通方法的声明一样,可以有入参也可以有返回值,下面针对方法类型做几点阐述:

  1. 方法类型中的参数类型需要使用()括号包裹,如(A, B)-> C中的A、B都需要包裹在小括号中。当然如果没有参数类型可以省略参数类型,如 ()->C就表示该方法类型没有参数类型。
  2. 方法类型中->后面的类型表示该方法类型的返回类型,如(A, B)-> C中的C表示方法的返回类型就是C。如果该方法类型没有返回类型(即默认返回Unit),则必须显示指定其返回类型为Unit,也就是不能省略Unit。
  3. 方法类型可以有额外的接收类型(receive type,如果不明白可以回顾扩展方法那篇文章),如 A.(B) -> C,该方法类型表示可以通过A对象来调用参数为B类型且返回值我C类型的方法。
  4. Suspending方法也是方法类型,只不过比较特殊,这个会在kotlin协程相关的文章中进行阐述。
  5. 方法类型中的入参类型可以指定参数名字,如(x:Int, y:Int)-> Int,这在生成文档时很有用处,增加了文档的可读性。
  6. 方法类型中的->符号是右结合的,所以方法类型(Int) ->((Int) ->Int) 和(Int) -> (Int) -> Int表达的是同一个类型,但是和方法类型((Int) ->(Int)) ->Int是不同的。
  7. 方法类型可以使用typealias关键字定义别名,如:typealias checkStr = (str: String) -> Boolean

好了,总结了一大堆,只不过是在脑子里面留个大概的印象,如果只看一遍而没有实践,过不多久肯定会忘记的。现在先回到前面一个例子中,我们定义了一个方法类型的入参,那么如何使用呢?示例如下:

//main测试方法
fun main(args: Array<String>) {
    val testStr1 = ""
    val testStr2 = "test"
    sayHello(testStr1, { str -> str.isNotEmpty() })//打印'error...'
    sayHello(testStr2, { str -> str.isNotEmpty() })//打印'pass...'
}

上面代码的打印结果已经在语句后面注释出来了,主要关注方法类型是如何调用的。

同其他类型传参一样,调用含有方法类型的方法时,必须要传入一个方法类型的实例,那么该怎么实例化一个方法类型呢?实例化方法类型可以通过以下几种方法:

  1. 使用字面量函数代码块,主要有一下两种:
    (1)lamada表达式。如本例中的{str -> str.isNotEmpt()}就是采用这种方法。
    (2)匿名方法。如上面的例子中我们还可以这么调用:
sayHello(testStr2, fun(str): Boolean { return str.isNotEmpty()})
  1. 传入已定义过的可调用的方法引用,这一类包括成员方法、扩展方法、top-level方法甚至某些构造方法等,如上面例子我们可以直接传入String中的isNotEmpty方法来完成我们的需求(这里以为只判断是否为空,如果有其他复杂逻辑则不能这么用了,除非有已经定义好的完全匹配的相应方法),如下所示:
    sayHello(testStr2, String::isNotEmpty)
  1. 传入实现方法类型接口的自定义类对象。这个是什么意思?看下示例就会明白(注意注释):
//自定义了一个CheckStr类型,实现了 (String) -> Boolean接口
//注意,这里就不能写成 (str:String) -> Boolean,实现方法类型接口时
//不能有命名参数,只能有类型
class CheckStr : (String) -> Boolean {
    override fun invoke(str: String): Boolean = str.isNotEmpty()
}
//调用方法如下
sayHello(testStr2, CheckStr())

至此,上面的例子算是分析完了。

接下来看下如何通过方法类型的实例来调用方法,示例如下:

fun main(args: Array<String>) {
//定义两个测试字符串
    val testStr1 = ""
    val testStr2 = "test"
//方法类型实例1:checkStr,接收两个参数,返回Boolean值。
//实例化的内容是判断str1以及str2是否为空,如果都不为
//空则返回true,否则返回false
    val checkStr: (String, String) -> Boolean = { str1, str2 -> str1.isNotEmpty() && str2.isNotEmpty() }
    println(checkStr.invoke(testStr1, testStr2))//调用方式1
    println(checkStr(testStr1, testStr2))//调用方式2
//方法类型实例2:checkStr2,注意这里将上面两个参数方法类型
//赋值给了包含有一个receiver、但只有一个入参类型的方法类型
    val checkStr2: String.(String) -> Boolean = checkStr
    println(testStr1.checkStr2(testStr2))//可以直接通过receiver对象调用
}

上面代码主要阐述一下几点(原理将会在下面文章中分析):

  1. 方法类型的实例有两种调用方式,一种是像普通方法调用一样传入对应的参数即可,如checkStr(testStr1, testStr2);另一种是通过实例的invoke方法来调用如checkStr.invoke(testStr1, testStr2)。
  2. 对于包含有receiver的方法类型,如果receiver的类型以及该方法类型的剩余参数类型和没有显示定义receiver的方法类的入参类型相匹配,则二者可以进行相互赋值。这个理解起来有点费劲,看下示例说明就会明白:如方法类型String.(String) -> Boolean就是显示包含receiver的方法类型,该receiver的方法类型是String,参数类型也是String,所以它就等同于方法类型(String, String) -> Boolean ,该方法类型同时接收两个String,而第一个String类型刚好和receiver类型相匹配,剩下的参数类型也相互匹配,故可以认为他们是相等的。

Lambda表达式

Lambda表达式一听就很神秘的样子,其实没什么,他和匿名方法都被成为“方法字面量”,他们所表达的场景就是,在没有显示定义方法的时候,我们可以通过这种方式生成一个具有同等形式、功能的方法。

还是回到文章开头提到的sayHello这个例子中:

//sayHello方法,第二个参数类型是方法类型
fun sayHello(str: String, checkStr: (str: String) -> Boolean) {
}
//我们使用中可以这么调用
sayHello("test", { str -> str.isNotEmpty() })
//{ str -> str.isNotEmpty() }就是一个lambda表达式,其作用
//相当于下面显示定义的checkStr方法
fun checkStr(str: String): Boolean {
    return str.isNotEmpty()
}

代码中的注释已经阐述了lambda表达式的含义,lambda表达式可以大大简化代码的写法,也能减少不必要的方法定义,但与此同时带来的副作用就是是代码的可读性大大降低。

lambda表达式定义的语法如下所示:

//直接给sum赋值一个方法类型实例,等于后面就是标识的lambda表达式
 val sum = { x: Int, y: Int -> x + y }
//也可以显示定义sum的类型为(Int,Int)->Int的方法类型
val sum: (Int, Int) -> Int = { x, y -> x + y }

lambda表达式都会被放在{}中,可以指定参数名称也可以省略。当lambda表达式作为方法的最后一个参数时,可以将其提取到方法外部进行实现,如调用上面的sayHello方法可以写成如下形式:

//直接将lambda表达式提取到了方法体的外部
    sayHello("test") { str -> str.isNotEmpty() }

当lambda表达式是唯一的方法入参时,我们可以只保留{},如下所示:

//假如这里我们定义了只包含一个lambda表达式入参的sayHello方法
fun sayHello(checkStr: (String) -> Boolean) {
    if (checkStr("test")) {
        println("is not empty...")
    }
}
//那么我们就可以使用及其简洁的调用方式
sayHello { str -> str.isNotEmpty() }

上面这种就是我们经常见到的lambda方法调用形式,刚接触的时候会一脸懵逼,理解之后就会发现,实际上这种形式只有方法入参有且只有一个lambda表达式类型的时候的一种简写,这也正是lambda表达式的一个吸引人之处,简化代码。

在kotlin中,对于只有一个lambda入参的方法,还有一个优化的地方,那就是可以使用it来代替实际入参,比如上面调用sayHello的方式还可以简写为如下代码:

//it就指代了传入的str对象
 sayHello { it -> it.isNotEmpty() }

上面代码中,我们并没有显示的返回lambda的结果,那么lambda的返回机制是什么呢?照例说话:

//1. 这个是我们常用的调用方法,并没有太多关注返回值
    sayHello { it -> it.isNotEmpty() }
//2. 其实我们可以在{}中做更多工作,然后再{}的最后返回我们想要的值
//即lambda会将最后一条语句作为其返回值
    sayHello {
        val result = it.isNotEmpty()
        result
    }
//3. 这里我们显示指定了返回值,其效果和1、2中一样
//这里使用了精确返回,即return@sayHello,为什么要这样?
//这是因为如果直接return的话就相当于return当前方法,
//而通过指定label,就表示要return的是当前的方法块。
    sayHello {
        val result = it.isNotEmpty()
        return@sayHello result
    }

上面代码已经注释的很详尽,这里不再阐述。

如果一个高阶方法接收多个lambda表达式,当我们不需要传递时可以传入下划线_,如下所示:

//我们定义了一个sayHello方法,参数是一个方法类型,
//该方法类型需要两个String类型的参数
fun sayHello(checkStr: (String, String) -> Boolean) {
}
//当我们调用sayHello的时候,如果我们不需要传入参数,
//则可以使用_代替,如下代码意思就是不需要用到第一个入参
sayHello { _, str -> str.isNotEmpty() }

匿名方法

阐述过lambda表达式后,继续阐述下匿名方法。相较于lambda表达式,匿名方法容易理解多了。

匿名方法同lambda方法同样都是“方法字面量”,都可以在不显示定义方法的时候提供具有同样形式、功能的实现。只不过匿名方法更加接近普通方法的定义,如下所示:

//匿名方法的形式如下所示,但并不能这么在文件中定义!
 fun(x: Int, y: Int): Int = x + y

当然匿名方法并不能当而皇之的写在class中或者top-level中,而只能作为方法实参传入到方法中,如上面的sayHello可以改用匿名方法的调用形式:

  //通过匿名方法的形式调用sayHello
    sayHello(fun(str: String): Boolean {
        return str.isNotEmpty()
    })
//当方法体只有一条语句时,可以简化为下面语句进行调用
   sayHello(fun(str: String) = str.isNotEmpty())

匿名方法和lambada表达式的不同,主要有两点:

  1. 匿名方法中的入参,必须要放到圆括号里面。而lambda可以简化。
  2. 匿名方法返回时,会返回到该方法的调用处,而lambda则会返回最近的方法调用处。

闭包

什么是闭包?很难用一句话来解释,可以理解为,当一个作用域A位于另一个作用域B中时,A可以访问到其外部作用域B的相关环境,A和B所构成的环境就可以称之为一个闭包。

kotlin中的lambda表达式和匿名方法都可以访问其闭包中的相关环境,这里的相关环境其实就是指一些变量等。如下所示:

//main为测试方法
fun main(args: Array<String>) {
//main方法中有个变量initVal
    var initVal = 1
//示例1,我们可以在lambda表达式中访main作用域中的initVal
    sayHello {
        initVal++
        it.isNotEmpty()
    }
//示例2,我们也可以在匿名方法中访main作用域中的initVal
    sayHello(fun(str: String): Boolean {
        initVal++
        return str.isNotEmpty()
    })
}

至此,高阶方法和lambda表达式的用法已阐述完毕。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269