深刻理解 Kotlin高阶函数匿名函数以及lambda表达式

Posted 小猪快跑22

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深刻理解 Kotlin高阶函数匿名函数以及lambda表达式相关的知识,希望对你有一定的参考价值。

在说高阶函数之前,我们得知道什么是函数类型,啊,什嘛?不知道函数类型?那好吧,下面就说说什么是函数类型。

一 、函数类型

在 Kotlin 里面定义变量类型很简单,如下:

var a :Int = 1 // Int 型变量
var person :Person = Person()  // 定义 Person 型变量

那么函数类型应该是什么样的表现形式呢?有的,形式如下:

(X, Y)-> Z,其中 XY 表示函数的参数,当然了,参数的个数任意,这里只写了2个,Z 表示函数的返回值

知道这种形式后,我们可以很快的写出函数类型,如下:

var f :(Int , Int)-> Int

(Int , Int)-> Int 表示函数的参数为2个Int 型,且返回值为 Int 型的函数类型。

注意:参数列表的小括号是必须要的,即使没有参数也不能省略,即写成()-> String;即使函数类型的返回值类型为Unit,也不可省略Unit不写,例如:(Int) -> Unit

函数类型如何赋值呢?

假设我们定义了函数类型如下:

var f :(Int , Int)-> Int

然后定义了一个sum函数如下:

fun sum (a :Int, b :Int) : Int {
   return a + b
}

可以看出来,上面定义的函数类型 fsum 函数的签名和返回值类型都是一样的。
那么我们可以直接像这样的赋值吗 ?

f = sum

这样是不行的,那正确的写法是什么样的呢?

f = ::sum
双冒号(::)加方法名是什么鬼呢?

Kotlin 官方的说法 :双冒号的写法叫做函数引用 (Function Reference)。
但是这又表示什么意思?表示它指向上面的函数?那既然都是一个东西,为什么不直接写函数名,而要加两个冒号呢?

因为加了两个冒号,这个函数才变成了一个对象,只有对象才能被赋值给变量。
但 Kotlin 的函数本身没办法被当做一个对象。那怎么办呢?Kotlin 的选择是,那就创建一个和函数具有相同功能的对象。怎么创建?使用双冒号。

在 Kotlin 里,一个**函数名左边加上双冒号,它就不表示这个函数本身了,而表示一个对象,或者说一个指向对象的引用,但是,这个对象可不是函数本身,而是一个和这个函数具有相同功能的对象。

怎么使用呢?
val f = ::sum
val res1 = f(2, 3) // 输出5
(::sum)(2, 3) // 输出5

还有这样:

f.invoke(2, 3) // 输出5

所以你可以对一个函数类型的对象调用 invoke(),但不能对一个函数这么做:

sum.invoke(2, 3) // 报错,因为只有函数类型的对象有这个自带的 invoke() 可以用

下面开始说什么是高阶函数(high-order-function)

二、高阶函数(high-order-function)

什么是高阶函数:参数或者返回值为函数类型的函数。如下就是高阶函数:

// 其中 参数 opt 为函数类型
fun f1(name :String, opt :(Int, Int) ->Int) :String {
    return name + opt.invoke(2, 3)
}

// 函数的返回值为 函数类型
private fun f2 () : (Int, Int) ->String {
    return ::sum
}
为什么要使用高阶函数?

在Java中,我想把方法作为参数传递给另一个方法,可以吗?
答案是不行的,但是Java中可以用接口来代替:

interface OnReporterListener {
   void onReporter(String msg);
}

void report(OnReporterListener listener) {
   listener.onReporter("哈哈,小猪哥");
}

但是在Kotlin中有了高阶函数的存在就可以轻松实现了

private fun report(opt : (String) -> Unit) {
    opt.invoke("哈哈,小猪哥")
}
// 同样定义一个函数前面和返回值一致的函数
private fun doReport(msg :String) {
    Log.e("xxx", "msg = $msg")
}

测试代码可以这样写:

private fun test() {
    report(::doReport)
}

是不是感觉 啊… 这 …也太麻烦了吧,得先定义一个函数,然后还得以 :: 函数名的形式传入。其实有更简单的方式,下面就来说说匿名函数lambda 表达式

三、匿名函数

匿名函数:就是指没有名字的函数,没有名字的函数要怎么调用呢?答案是没法直接调用,可以通过 把匿名函数赋值给变量或者作为函数的参数传递。

匿名函数如下:

fun(a: Int, b: Int) = a + b

上面的匿名函数是没法直接调用的,可以赋值给变量:

val f = fun(a :Int, b :Int) = a + b
f(2, 3)

注意 :匿名函数实质上也是函数类型的对象,所以可以赋值给变量。

所以上面的测试方法 report(::doReport)可以改成如下的方式:

我们直接将匿名函数传入,相当于将匿名函数直接赋值给函数类型的参数:

private fun test() {
    //  report(::doReport)
    // 这里面的 String 可以省略,因为从 report 方法中可知,传入的函数类型的入参为String
    report(fun(msg :String) {
        Log.e("xxx", "msg = $msg")
    })
}

我们把匿名函数赋值给变量 opt,那么 opt 就是一个函数类型

private fun test() {
     // report(::doReport)
     // 这里面的 String 可以省略,因为从 report 方法中可知,传入的函数类型的入参为String
    val opt = fun (msg :String) { 
        Log.e("xxx", "msg = $msg")
    }
    report(opt)
}

注意:如果匿名函数可以从左边的变量类型推断出 函数参数类型,那么匿名函数的参数类型可以省略,如下:

// 你可以以如下的方式来写,因为左边的变量指明参数类型
val noNameFun3: (Int, Int) -> Int = { a, b ->
    a + b
}

如下是不能省略的:

// 不能省略入参的参数类型,因为无法从其他地方推断入参类型
// 返回值类型时可以省略的,因为是可以推断的
val noNameFun2 = { a: Int, b: Int -> 
    a + b
}

从上面可以知道,好像真的传入了一个函数一样。而实际上你传递的是一个对象,一个函数类型的对象。因为匿名函数它本来就不是函数,而是一个函数类型的对象

这样的写法还是不够简洁,有更简洁的方式吗?当然,那就是 lambda 表达式。

四、lambda 表达式

其实Lambda表达式的本质匿名函数,而匿名函数的本质是函数类型的对象。因此,Lambda表达式、匿名函数、双冒号+函数名这三个东西,都是函数类型的对象,他们都能够赋值给变量以及当作函数的参数传递。

lambda 表达式的格式如下:
Lambda 表达式被大括号{}包围着
Lambda 表达式的参数在->的左边,如果没有参数,则只保留函数体
Lambda 表达式的函数体在->的后面
Lambda 表达式的返回值类型 为函数体最后一行代码的返回值类型

  1. 有参数有返回值的表达式 :
val lambda1 = {a :Int, b :Int ->
    a + b
}
  1. 有参数无返回值的
val lambda2 = { name : String ->
    Log.e("xx", "name = $name")
}
  1. 无参数无返回值的
val lambda3 = {
    Log.e("xx", "无参数无返回值的 lambda 表达式")
}

定义一个高阶函数如下:

fun test (name :String , opt : (a :Int , b: Int) -> Int) :String {
    return name + opt.invoke(3, 4)
}

然后我们用lambda 表达式的方式调用,如下:

test("lambda", {a, b -> a*b})

注意:由于lambda 表达式是函数test的最后一个参数,所以可以把 lambda 表达式移到外面,如下:

test("lambda") { a, b -> 
    a * b
}

如果函数只接收一个函数类型的参数,在传入Lambda表达式时,连函数调用的括号都可以去掉:

 test3 { a, b ->
     a + b
 }

fun test3 (opt : (a :Int , b: Int) -> Int) :String {
    return opt.invoke(3, 4).toString()
}

如果函数类型只有一个入参,它的这个参数也省略掉不写:

test4 { 
    val value = it // 用 it表示唯一的参数
    Log.e("xxx", "it = $it")
}

fun test4(opt : (String) -> Unit) {
    opt.invoke("123")
}

五、总结:

本文介绍了函数类型、高阶函数、匿名函数以及Lambda表达式的本质:

  1. 函数不能通过函数名直接传递,可以通过双冒号加函数名传递
  2. 双冒号+函数名、匿名函数、Lambda表达式的本质实际上都是函数类型的对象
  3. 高阶函数就只是参数列表或返回值类型存在函数类型的函数没啥特别的

以上是关于深刻理解 Kotlin高阶函数匿名函数以及lambda表达式的主要内容,如果未能解决你的问题,请参考以下文章

深刻理解 Kotlin高阶函数匿名函数以及lambda表达式

Kotlin高阶函数内联函数以及集合变换序列

Kotlin中匿名函数(又称为Lambda,或者闭包)和高阶函数的详解

Kotlin——高级篇:高阶函数详解与标准的高阶函数使用

11Kotlin项目实操之高阶函数二

09Kotlin项目实操之高阶函数一