kotlin 函数、匿名函数、内联函数

「函数并不能传递,传递的是对象」和「匿名函数和 Lambda 表达式其实都是对象」

函数类型

  • 简单的函数类型
//无参、无返回值的函数类型(Unit 返回类型不可省略)
() -> Unit
//接收T类型参数、无返回值的函数类型
(T) -> Unit
//接收T类型和A类型参数、无返回值的函数类型(多个参数同理)
(T,A) -> Unit
//接收T类型参数,并且返回R类型值的函数类型
(T) -> R
//接收T类型和A类型参数、并且返回R类型值的函数类型(多个参数同理)
(T,A) -> R
  • 复杂的函数类型
(T,(A,B) -> C) -> R
fun a ( funParam : ( Int ) -> String ): String {
return funParam ( 1 )
}

函数类型不只可以作为函数的参数类型,还可以作为函数的返回值类型

fun c ( param : Int ): ( Int ) -> Unit {
...
}

高阶函数

参数有函数类型或者返回值是函数类型的函数,都叫做高阶函数
另外,除了作为函数的参数和返回值类型,你把它赋值给一个变量也是可以的。
不过对于一个声明好的函数,不管是你要把它作为参数传递给函数,还是要把它赋值给变量,都得在函数名的左边加上双冒号才行

a (:: b )
val d = :: b

Kotlin 里「函数可以作为参数」这件事的本质,是函数在 Kotlin 里可以作为对象存在——因为只有对象才能被作为参数传递。赋值也是一样道理,只有对象才能被赋值给变量。但 Kotlin 的函数本身的性质又决定了它没办法被当做一个对象。那怎么办呢?Kotlin 的选择是,那就创建一个和函数具有相同功能的对象。怎么创建?使用双冒号。

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

怎么个相同法呢?你可以怎么用函数,就能怎么用这个加了双冒号的对象:

b ( 1 ) // 调用函数
d ( 1 ) // 用对象 a 后面加上括号来实现 b 的等价操作
(:: b )( 1 ) // 用对象 :b 后面加上括号来实现 b 的等价操作

对象是不能加个括号来调用的,对吧?但是函数类型的对象可以。为什么?因为这其实是个假的调用,它是 Kotlin 的语法糖,实际上你对一个函数类型的对象加括号、加参数,它真正调用的是这个对象的 invoke 函数

d ( 1 ) // 实际上会调用 d.invoke(1)
(:: b )( 1 ) // 实际上会调用 (::b).invoke(1)

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

b . invoke ( 1 ) // 报错

为什么?因为只有函数类型的对象有这个自带的 invoke 可以用,而函数,不是函数类型的对象。那它是什么类型的?它什么类型也不是。函数不是对象,它也没有类型,函数就是函数,它和对象是两个维度的东西。

包括双冒号加上函数名的这个写法,它是一个指向对象的引用,但并不是指向函数本身,而是指向一个我们在代码里看不见的对象。这个对象复制了原函数的功能,但它并不是原函数。

匿名函数

要传一个函数类型的参数,或者把一个函数类型的对象赋值给变量,除了用双冒号来拿现成的函数使用,你还可以直接把这个函数挪过来写:

a ( fun b ( param : Int ): String {
return param . toString
});
val d = fun b ( param : Int ): String {
return param . toString
}
另外,这种写法的话,函数的名字其实就没用了,所以你可以把它省掉:

a ( fun ( param : Int ): String {
return param . toString
});
val d = fun ( param : Int ): String {
return param . toString
}

等号左边的不是函数的名字啊,它是变量的名字。这个变量的类型是一种函数类型,具体到我们的示例代码来说是一种只有一个参数、参数类型是 Int、并且返回值类型为 String 的函数类型

另外呢,其实刚才那种左边右边都有名字的写法,Kotlin 是不允许的。右边的函数既然要名字也没有用,Kotlin 干脆就不许它有名字了。

Kotlin 的匿名函数不——是——函——数。它是个对象。匿名函数虽然名字里有「函数」两个字,包括英文的原名也是 Anonymous Function,但它其实不是函数,而是一个对象,一个函数类型的对象。它和双冒号加函数名是一类东西,和函数不是。

同理,Lambda 其实也是一个函数类型的对象而已。你能怎么使用双冒号加函数名,就能怎么使用匿名函数,以及怎么使用 Lambda 表达式。

这,就是 Kotlin 的匿名函数和 Lambda 表达式的本质,它们都是函数类型的对象。Kotlin 的 Lambda 跟 Java 8 的 Lambda 是不一样的,Java 8 的 Lambda 只是一种便捷写法,本质上并没有功能上的突破,而 Kotlin 的 Lambda 是实实在在的对象

总结

  • 在 Kotlin 里,有一类 Java 中不存在的类型,叫做「函数类型」,这一类类型的对象在可以当函数来用的同时,还能作为函数的参数、函数的返回值以及赋值给变量;
  • 创建一个函数类型的对象有三种方式:双冒号加函数名、匿名函数和 Lambda;
  • 一定要记住:双冒号加函数名、匿名函数和 Lambda 本质上都是函数类型的对象。在 Kotlin 里,匿名函数不是函数,Lambda 也不是什么玄学的所谓「它只是个代码块,没法归类」,Kotlin 的 Lambda 可以归类,它属于函数类型的对象。

内联函数

当一个函数被内联 inline 标注后,在调用它的地方,会把这个函数方法体中的所以代码移动到调用的地方,而不是通过方法间压栈进栈的方式。

  • 代码示例:
  1. 使用 inline 的代码
// 在 main() 中调用 makeTest()
fun main() {
    Log.i("zc_test", "main() start")
    makeTest()
    Log.i("zc_test", "main() end")
}
// 内联函数 makeTest()
private inline fun makeTest() {
    Log.i("zc_test", "makeTest")
}
  1. 使用 inline 编译成 java 的代码
public final void main() {
    Log.i("zc_test", "main() start");
    int $i$f$makeTest = false;
    Log.i("zc_test", "makeTest");
    Log.i("zc_test", "main() end");
}

3.当 makeTest() 不在被 inline 修饰时, 被编辑成 java 的代码为:

public final void main() {
    Log.i("zc_test", "main() start");
    this.makeTest();
    Log.i("zc_test", "main() end");
}

可以看到,当 makeTest() 被inline 修饰时, 在 main() 中原来调用 makeTest() 的地方被替换成了 makeTest() 里面的代码。
换句话说:在编译时期,把调用这个函数的地方用这个函数的方法体进行替换。
这就是 inline 的本质

Kotlin 内联函数的使用

  • 不带参数,或是带有普通参数的函数,不建议使用 inline
  • 带有 lambda 函数参数的函数,建议使用 inline

不应该使用 inline 的情况

当使用 inline 标注时,如果是下面这样,无参数的函数时:

//makeTest() 没有任何的参数
private inline fun makeTest() {
     Log.i("zc_test", "makeTest")
}
//或者带有基本变量参数的函数,编译器也会报错。
private inline fun makeTest2(test: String) {
     Log.i("zc_test", "makeTest")
}

这个时候 AndroidStudio 编译器会在 inline 位置有黄色警告,
Expected performance impact of inlining '...' can be insignificant. Inlining works best for functions with lambda parameters,
翻译过来就是,在这个位置使用 inline 并不会有很大的提高,inline 适合在包含 lambda 参数的函数上。
也就是说 inline 在一般的方法是标注,是不会起到很大作用的,inline 能带来的性能提升,往往是在参数是 lambda 的函数上。

应该使用 inline 的地方: 带有 lambda 参数的函数

当我们写一个会被经常调用的带 lambda参数的函数时, 可使用该方式。

// body 是本身一个函数
fun foo(body:() -> Unit) {
    println("foo() hahaha")
    ordinaryFunction(body)
}

inline fun ordinaryFunction(block: () -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

在上述代码中,我们把 foo() 的函数参数 body 作为一个参数传递给 ordinaryFunction() ,
这是我们可以通过在 ordinaryFunction() 上面标注 inline 从而使得方法的调用栈少一层,使得代码变为:

fun foo(body:() -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

inline 提高效率的原因

为什么要使用 inline 呢?必然是因为使用 inline 会带来效率的提升。
我们比较一下使用了 inline 和不使用 inline 编译成 java 代码的差异

当然上述 ordinaryFunction() 也可以不使用 inline 标注,我们看一下编译成 java 的代码样式, 「对比」添加了 inline 的标注的 java 代码,我们发现,当不添加 inline 时,代码中,多出了一个类:

final class TestInline$main$1$1 extends Lambda implements Function0 {
   public static final TestInline$main$1$1 INSTANCE = new TestInline$main$1$1();
   // $FF: synthetic method
   // $FF: bridge method
   public Object invoke() {
      this.invoke();
      return Unit.INSTANCE;
   }

   public final void invoke() {
   }

   TestInline$main$1$1() {
      super(0);
   }
}

它便是在编译过程中,因为 lambda 参数 多出来的类,无疑中会增加内存的分配。

所以我们就知道了,在 kotlin 中,因为出现了大量的 高阶函数 -- 「高阶函数是将函数用作参数或返回值的函数」,使得越来越多的地方出现 函数参数 不断传递的现象,每一个函数参数都会被编译成一个对象, 使得内存分配(对于函数对象和类)和虚拟调用会增加运行时间开销。所以才会出现 inline 内联函数。可以通过 inline 的标注,把原本需要生成一个类的开销节省了, 同时也少了一层方法栈的调用。

inline 的其他作用

支持 return 退出函数

在编码中,我们通常习惯使用 return 返回退出这个函数,但是 lambda 表达式不能使包含它的函数返回。

fun foo(body:()->Unit) {
    ordinaryFunction {
        println("zc_testlabama 表达式退出")
        return
    }
    println("zc_test --->foo() end")
} 
fun ordinaryFunction(block: () -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

如果在 ordinaryFunction 这个方法没有 inline 的标注,编译器会在 return 的位置出错,return is not allowed here.

解决上述错误的方式,可以为 return 添加标签,例如 return@ordinaryFunction, 但是这样的话,方法执行只会退出 lambda 表达式,后面的代码 println("zc_test --->foo() end") 还是会走到的。

当我们添加上 inline 时,正确的代码如下:

fun foo(body:()->Unit) {
    ordinaryFunction {
        // 因为标识为 inline 的函数会被插入到调用出,此时 return 肯定是 return 到该整个方法
        println("zc_testlabama 表达式退出")
        return
    }
    println("zc_test --->foo() end")
}
// 如果不使用 inline, 上面代码会被报错。因为「不允许这么做」
inline fun ordinaryFunction(block: () -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

当我们添加了 inline 标志后,在 ordinaryFunction{} 的 return 时就会退出整个 foo() 函数,因此结尾的 println("zc_test --->foo() end") 是不会被调用的。

kotlin 官方注释:break 和 continue 在内联的 lambda 表达式中还不可用,但我们也计划支持它们。

禁止内联:noinline

官网中这么写着:如果希望只内联一部分传给内联函数的 lambda 表达式参数,那么可以用 noinline 修饰符标记不希望内联的函数参数, 代码如:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }

什么时候我们会需要 noinline 呢?
例如代码:

inline fun foo(testName:String, body:()->Unit) {
    // 这里会报错。。。
    ordinaryFunction(body)
    println("zc_test --->foo() end")
} 
fun ordinaryFunction(block: () -> Unit) {
    println("hahha")
    block.invoke()
    println("hahha233333")
}

如果 ordinaryFunction() 不使用 inline 标注,是一般的函数,这里是不允许把内联函数 foo() 的函数参数 body 传递给 ordinaryFunction()。

即:内联函数的「函数参数」 不允许作为参数传递给非内联的函数,
如果我们想要实现上述的调用,便可以使用 noinline 标注内联函数 foo() 的 body 参数

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

推荐阅读更多精彩内容