美文网首页Android开发Android开发经验谈Android开发
Kotlin 高阶函数从未如此清晰(上)

Kotlin 高阶函数从未如此清晰(上)

作者: 小鱼人爱编程 | 来源:发表于2022-05-18 10:34 被阅读0次

    前言

    高阶函数系列文章:

    Kotlin 高阶函数从未如此清晰(上)
    Kotlin 高阶函数从未如此清晰(中)
    Kotlin 高阶函数从未如此清晰(下) let/also/with/run/apply/repeat 一看就会

    上一篇罗列过Kotlin的属性与函数的基本知识,算是入门篇本。本篇将继续对函数的一些高级用法进行深入分析。
    通过本篇,你将了解到:

    1、什么是函数类型?
    2、Kotlin 函数类型形参声明/实参定义
    3、Kotlin 函数类型参数调用
    4、匿名函数与Lambda
    5、Kotlin 函数作为返回值
    6、Java 如何调用Kotlin 函数?

    1、什么是函数类型?

    Java 如何传递方法?

    有个场景:

    输入学生的姓名、年龄,返回该学生的考试分数。

    通常我们会将它封装为一个方法,而方法需要放在类或者接口里,最终调用时是通过类/接口的实例化对象调用该方法,如下:

        private void testStudent(HandleStudent handleStudent) {
            float score = 0;
            if (handleStudent != null) {
                score = handleStudent.getScore("fish", 18);
            }
            System.out.println("score:" + score);
        }
        //接口
        interface HandleStudent {
            //传入学生的姓名、年龄,返回学生的分数
            float getScore(String name, int age);
        }
    

    实际上我们只需要调用getScore(xx)方法,为了实现这个目的,需要将它放到类/接口封装,最后生成实例对象调用,多了好几个步骤。
    有没有更简单的方式呢?比如直接传递方法本身?
    答案是:没有。

    因为在Java 的世界里,类/接口 是一等公民,方法必须依赖于它们存在。

    Kotlin 函数类型

    Java 不支持方法作为方法的参数,而Kotlin 却支持函数作为函数的参数/返回值。

    因为在Kotlin 的世界里,函数是一等公民,可以脱离类/接口而存在。

    如果你接触过C++等语言,相信你对函数参数不会太陌生,C++里有函数指针,指向的是一个函数的指针,通过该指针就可以调用其指向的函数。

    还是以获取学生分数为例:

    fun upFun1(name: String, age: Int): Float {
        return 88f
    }
    

    我们只关注该函数的输入参数与返回值,并不关心该函数的名字,而函数的输入参数与返回值就决定了该函数的类型。
    upFun1 的函数类型为:

    (String, Int)->Float
    输入参数类型为:String 和 Int,多个参数之间用","隔开,所有参数使用()括起来
    返回值类型为:Float,返回值与输入参数之间使用"->"连接。

    如此一来就可以表示一个函数的类型。

    2、Kotlin 函数类型形参声明/实参定义

    形参声明

    在上一篇文章里,我们有提到过:Kotlin里的引用类型包括函数这种引用类型,既然是引用,那么当然可以作为参数传递了,来看看如何声明一个使用了函数作为形参的函数。

    //testUpFun1 接收的参数为函数类型:(String, Int)->Float
    fun testUpFun1(getScore : (String, Int)->Float) {
    }
    

    传入的形参不使用的话没啥意义,对于函数类型,通常是调用该函数,如下:

    //testUpFun1 接收的参数为函数类型:(String, Int)->String
    fun testUpFun1(getScore : (String, Int)->Float) {
        var score = getScore("fish", 18)
        println("student score:$score")
    }
    

    实参定义

    形参有了,当调用testUpFun1(xx)时需要传入实参,也就是传入函数的定义:

    fun upFun1(name: String, age: Int): Float {
        return 88f
    }
    

    定义了upFun1函数,该函数类型为:(String, Int)->Float,符合作为testUpFun1 形参的条件。

    3、Kotlin 函数类型参数调用

    形参和实参都有了,接着来看如何将两者结合起来,总结来说有如下几种方式:


    image.png

    接下来一一看看三者的实现方式。

    函数引用

    当我们定义了一个函数后,想将这个函数作为实参传递给另一个函数,可以通过:: + 函数名 的方式传递,官方说法叫做:函数引用。

    fun upFun1(name: String, age: Int): Float {
        return 88f
    }
    
    //testUpFun1 接收的参数为函数类型:(String, Int)->String
    fun testUpFun1(getScore : (String, Int)->Float) {
        var score = getScore("fish", 18)
        println("student score:$score")
    }
    
    fun main(args: Array<String>) {
        //通过函数引用调用
        testUpFun1(::upFun1)
    }
    

    如上,testUpFun1 函数需要传入一个函数类型的参数,通过"::"引用函数名即可。

    变量(函数类型)

    普通函数定义

    既然函数引用可以当做参数传递,那么它当然可以赋值给变量,如下:

    fun upFun1(name: String, age: Int): Float {
        return 88f
    }
    //赋值
    //var varFun:(String, Int)->Float = ::upFun1
    //类型推断,可以不用写变量类型
    var varFun = ::upFun1
    
    fun main(args: Array<String>) {
        testUpFun1(varFun)
    }
    

    此时,我们只需要把varFun 作为实参传递即可。
    需要注意的是:Kotlin 会对变量进行类型推断,因此我们可以省略变量类型

    匿名函数

    当然了,若是想要在声明变量的同时将函数定义了,这也是可以的:

    //匿名函数
    //var varFun1:(String, Int)->Float = fun (name: String, age: Int):Float {
    //    return 88f
    //}
    //类型推断
    var varFun1 = fun (name: String, age: Int):Float {
        return 88f
    }
    fun main(args: Array<String>) {
        testUpFun1(varFun1)
    }
    

    可以看出,我们声明的函数没有函数名,只有一个"fun"声明。
    此时,varFun1 表示的是一个匿名函数。
    同样的因为类型自动推导,可以不用写变量类型。

    Lambda 表达式

    在Java 里,有时候我们会将匿名内部类转为Lambda形式,而Kotlin 对于Lambda的使用更广泛了,上面的匿名函数我们可以用Lambda表示。

    //Lambda 表达式
    //var lambda1:(String, Int)->Float = {
    //    name:String,age:Int->
    //    88f
    //}
    //类型推导
    var lambda1 = { name: String, age: Int ->
        88f
    }
    fun main(args: Array<String>) {
        testUpFun1(lambda1)
    }
    

    可以看出,变量作为实参传递,对比普通函数定义、匿名函数、Lambda 表达式三者写法,发现Lambda最简洁,简洁有时候也意味着难以理解。

    Lambda 格式以及一些风骚写法,我们放在下节分析

    直接传入函数体

    不管是函数引用还是变量作为实参,都需要先将函数定义好,有时候函数只在一个地方使用,无需再单独定义出来,此时可以选择直接将函数体当做实参传递。

    匿名函数

    在调用函数的时候,直接传入匿名函数作实参:

    //testUpFun1 接收的参数为函数类型:(String, Int)->String
    fun testUpFun1(getScore: (String, Int) -> Float) {
        var score = getScore("fish", 18)
        println("student score:$score")
    }
    
    fun main(args: Array<String>) {
        //传入匿名函数
        testUpFun1(fun(name: String, age: Int): Float {
            return 88f
        })
    }
    

    Lambda

    老样子,一般匿名函数都可以用Lambda 替代:

    fun main(args: Array<String>) {
        //Lambda 表示
        testUpFun1({ name: String, age: Int ->
            88f
        }
        )
    }
    

    此时,编译器会提示你可以再优化一下写法:

    fun main(args: Array<String>) {
        //传入匿名函数
        testUpFun1 { name: String, age: Int ->
            88f
        }
    }
    

    我们知道函数的调用需要用"()"括起来,此时"()"都没了,越来越简洁了。

    4、匿名函数与Lambda

    上面简单展示了匿名函数和Lambda的使用,只是一些基本写法,尤其是Lambda还有一些风骚写法,接着来分析。

    匿名函数

    顾名思义,函数是有函数名的,如果省略了函数名那么就称之为匿名函数。

    //定义匿名函数
    var anoymous1: (String, Int) -> Float = fun(name: String, age: Int): Float {
        println("name:$name age:$age")
        return 88f
    }
    //自动推导,消除变量类型
    var anoymous2 = fun(name: String, age: Int): Float {
        println("name:$name age:$age")
        return 88f
    }
    
    //调用
    fun main(args: Array<String>) {
        //传入匿名函数
        testUpFun1(fun(name: String, age: Int): Float {
            println("name:$name age:$age")
            return 88f
        })
        testUpFun1(anoymous1)
        testUpFun1(anoymous2)
    }
    

    需要注意的是:

    匿名函数返回值表示的是该匿名函数本身的返回值。

    Lambda

    匿名函数还是不够简洁,此时Lambda出现了,分三步阐述。

    Lambda 基本结构

    var varLambda2 = { name: String, int: Int ->
        println()
        test()
        "jj"
    }
    

    以此为例,Lambda 有如下约定:

    1、大括号"{}" 包裹内容。
    2、使用"->"连接参数与实现体。
    3、"->"左边表示参数列表,参数间使用","分割。
    4、"->"右边表示实现体,多个语句分行表示。
    5、如果没有参数列表,那么"->"可以省略
    6、Lambda 无需"return"关键字,最后一行默认表示返回值(如例子中"jj"表示Lambda返回了String类型

    变量接收Lambda

    //完整写法
    var varLambda1:(String, Int)->Float = { name: String, age: Int ->
        println("student name:$name age:$age")
        88f
    }
    

    因为自动推导类型,因此可以省略变量类型:

    //省略类型
    var varLambda1 = { name: String, age: Int ->
        println("student name:$name age:$age")
        88f
    }
    

    当然,非得要类型的话,还可以这么写:

    //Lambda里省略了参数类型,因为"="之前已经声明了
    var varLambda1: (String, Int) -> Float = { name, age ->
        println("student name:$name age:$age")
        88f
    }
    

    函数调用传入Lambda

    接下来通过不同的case由浅入深演示Lambda各种风骚写法。

    第一种Case:
    将之前的testUpFun1 改造一下,新增一个参数,如下:

    //testUpFun1 接收的参数为函数类型:(String, Int)->String
    fun testUpFun1(getScore: (String, Int) -> Float) {
        var score = getScore("fish", 18)
        println("student score:$score")
    }
    
    //改造后
    fun testUpFun2(getScore: (String, Int) -> Float, needDelay:Boolean) {
        if (needDelay)
            println("delay...")
        var score = getScore("fish", 18)
        println("student score:$score")
    }
    

    testUpFun2 函数有两个参数,一个是函数类型,另一个是Boolean。
    接着来看看如何调用testUpFun2 函数,我们以直接传入函数体为例:

    fun main2(args : Array<String>) {
        testUpFun2({ name: String, age: Int ->
            println("name:$name age:$age")
            88f
        }, true
        )
    }
    

    第二种Case:
    我们再变一下testUpFun2 参数,函数类型和Boolean交换位置:

    fun testUpFun3( needDelay: Boolean, getScore: (String, Int) -> Float) {
        if (needDelay)
            println("delay...")
        var score = getScore("fish", 18)
        println("student score:$score")
    }
    

    调用如下:

    fun main3(args: Array<String>) {
        testUpFun3(true, { name: String, age: Int ->
            println("name:$name age:$age")
            88f
        }
        )
    }
    

    此时,编译器会提示你可以将"{}"整体提取出来放在"()"括号后,如下:

    fun main3(args: Array<String>) {
        testUpFun3(true
        ) { name: String, age: Int ->
            println("name:$name age:$age")
            88f
        }
    }
    

    这是Lambda的一个约定:

    如果Lambda 作为函数的最后一个参数,那么Lambda可以提取到"()"外展示。

    第三种Case:
    再对testUpFun3 参数做调整,只保留一个函数类型的参数:

    //定义
    fun testUpFun4(getScore: (String, Int) -> Float) {
        var score = getScore("fish", 18)
        println("student score:$score")
    }
    //调用
    fun main4(args: Array<String>) {
        testUpFun4(
        ) { name: String, age: Int ->
            println("name:$name age:$age")
            88f
        }
    }
    

    同样的编译器会提示可以将"()"省略,如下:

    fun main4(args: Array<String>) {
        //省略"()"
        testUpFun4 { name: String, age: Int ->
            println("name:$name age:$age")
            88f
        }
    }
    

    这是Lambda的一个约定:

    如果Lambda 作为函数的唯一参数,那么调用函数"()"可以省略。

    第四种Case:
    这次不修改testUpFunX,我们修改Lambda表达式的入参,改为:

    //单参数
    fun testUpFun5(getScore: (String) -> Float) {
        var score = getScore("fish")
        println("student score:$score")
    }
    调用如下:
    fun main5(args: Array<String>) {
        ////省略"()"
        testUpFun5  { name: String ->
            println("name:$name")
            88f
        }
    }
    

    此时,可以写成如下方式:

    fun main5(args: Array<String>) {
        ////省略"()"
        testUpFun5  {
            //用it 替代了Lambda 的name
            println("name:$it")
            88f
        }
    }
    

    这是Lambda的一个约定:

    如果Lambda 入参只有一个,那么可以省略"->"以及入参列表,并在实现体里用it 指代这个唯一的参数。

    第五种Case:
    Lambda 有1个入参可以用"it"指代,Lambda 没有入参呢?

    //无参数
    fun testUpFun6(getScore: () -> Float) {
        var score = getScore()
        println("student score:$score")
    }
    fun main6(args: Array<String>) {
        ////省略"()"
        testUpFun6 {
            println("name")
            88f
        }
    }
    

    可以看出,此时只需要"{}"括起来即可。
    这是Lambda的一个约定:

    如果Lambda 没有入参,那么可以省略"->"以及入参列表。

    注:此时在Lambda里不能使用"it",因为它根本没入参。

    以上就是Kotlin Lambda 常用的一些变换规则。

    5、Kotlin 函数作为返回值

    定义函数:

    fun testUpFun7(getScore: (String) -> Unit): (Boolean, Int) -> String {
        //调用函数
        var score = getScore("fish")
        println("student score:$score")
    
        //返回函数,Lambda表示
        return { need: Boolean, age: Int ->
            println("need:$need  age:$age")
            "fish"
        }
    }
    

    调用:

    fun main7(args: Array<String>) {
        ////省略"()"
        var testReturn = testUpFun7 {
            println("name:$it")
        }
        //调用
        testReturn(true, 5)
    }
    

    只要掌握了高阶函数的传参,返回值也不在话下,此处就不展开细说了。

    6、Java 如何调用Kotlin 函数?

    以上都是Kotlin 调用 Kotlin,来看Java 如何调用Kotlin的高阶函数。
    还是以如下函数为例:

    fun testUpFun3(needDelay: Boolean, getScore: (String, Int) -> Float) {
        if (needDelay)
            println("delay...")
        var score = getScore("fish", 18)
        println("student score:$score")
    }
    

    在Java里调用:

        private void testKotlin() {
            UpFunKt.testUpFun3(true, new Function2<String, Integer, Float>() {
                @Override
                public Float invoke(String s, Integer integer) {
                    return null;
                }
            });
        }
    

    可以看出testUpFun3里的函数类型参数转化为了Function2 的实例,Function2 为何方神圣?


    image.png

    实际上就是Kotlin 里为了兼容Java 调用定义了一堆接口,这些接口标明了入参和返回值,Java 调用时需要重写invoke()方法即可,当在Kotlin里调用对应的函数参数时,将会调用到invoke()回到Java 代码。
    在Functions.kt里定义了23个接口:


    image.png

    基本上可以满足大部分的参数需求。

    小结
    理解了以上内容,我相信大家对Lambda各种写法都不会再陌生,如果你还是有疑惑,可能是我没阐述明白,欢迎留言讨论。
    下篇将会继续分析泛型函数、扩展函数、内联函数、常用的高阶函数如let/run/apply 等,进而自然过渡到协程的分析,那时再看协程就事半功倍了。

    本文基于Kotlin 1.5.3,文中Demo请点击

    您若喜欢,请点赞、关注,您的鼓励是我前进的动力

    持续更新中,和我一起步步为营系统、深入学习Android/Kotlin

    1、Android各种Context的前世今生
    2、Android DecorView 必知必会
    3、Window/WindowManager 不可不知之事
    4、View Measure/Layout/Draw 真明白了
    5、Android事件分发全套服务
    6、Android invalidate/postInvalidate/requestLayout 彻底厘清
    7、Android Window 如何确定大小/onMeasure()多次执行原因
    8、Android事件驱动Handler-Message-Looper解析
    9、Android 键盘一招搞定
    10、Android 各种坐标彻底明了
    11、Android Activity/Window/View 的background
    12、Android Activity创建到View的显示过
    13、Android IPC 系列
    14、Android 存储系列
    15、Java 并发系列不再疑惑
    16、Java 线程池系列
    17、Android Jetpack 前置基础系列
    18、Android Jetpack 易懂易学系列

    相关文章

      网友评论

        本文标题:Kotlin 高阶函数从未如此清晰(上)

        本文链接:https://www.haomeiwen.com/subject/ikzwurtx.html