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

前言

高阶函数系列文章:

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

上篇讲到了Kotlin 高阶函数定义以及如何使用Lambda进行简化调用,本篇接着来分析未尽事项。
通过本篇文章,你将了解到:

1、Kotlin 泛型初探
2、Kotlin 扩展函数的原理与使用
3、Koltin 内联函数的原理与使用

1、Kotlin 泛型初探

Java 泛型

我们知道Java 泛型是为了在编译时期做类型安全检查,本质上就是参数化类型。
以熟知的List为例,List<T> 是泛型接口,ArrayList<T> 是泛型类,若是没有使用泛型时:

    private void test1() {
        List nameList = new ArrayList();
        //添加字符串
        nameList.add("fish");
        //添加数字
        nameList.add(3);
    }

本意是构建了一个存储名字的List,也就是说该List里的元素是字符串,而上述添加Int 类型的元素却是没报错,因此编译器认为里面的元素都是Object类型,当我们需要取出元素时,就需要强转Object为对应的类型:

        String ss = (String)nameList.get(0);
        int age = (int)nameList.get(1);

强转在类型错误在编译时期是不会被发现的,只能在运行期间才会暴露。
再看看引入了泛型的List:

    private void test2() {
        List<String> nameList = new ArrayList();
        //添加字符串
        nameList.add("fish");
        //添加数字
        nameList.add("forest");
        //编译器不允许
//        nameList.add(3);
        
        //无需强转
        String name1 = nameList.get(0);
        String name2 = nameList.get(1);
    }

可以看出,在编译时期就进行了类型检测,提取元素时无需强转,同时也避免了一些自动拆装箱操作。

Kotlin 泛型

Kotlin 里的泛型和Java 里的泛型功能类似:

//泛型类
class A<T> {}

//泛型接口
interface B<T>{}

//泛型方法
fun <T> pick(a : T) {}

来看个实例:

class Fruit<T> {
    var quality:T? = null

    get() {
        println("$field")
        return field
    }

    fun setValue(t:T) {
        this.quality = t
    }
}

fun main(args: Array<String>) {
    var fruit:Fruit<String> = Fruit()
    fruit.setValue("jj")
    //编译不通过
//    fruit.setValue(33)
    fruit.quality
}

Fruit里的quality 可以是任何类型。
此处仅仅只是简单阐述Kotlin 泛型的写法及使用(为方便下一个小结理解常用的高阶函数),协变、逆变、星号等以及与Java 上下界通配符比对后续会单独开一篇分析。

2、Kotlin 扩展函数的原理与使用

扩展函数原理

先看简单例子:

class Student {
    //来自省份
    var province:String?= null
    //学生名字
    var name:String? = null
    init {
        name = "fish"
        province = "beijing"
    }
    fun printStudent() {
        println("$name")
    }
}

fun main(args: Array<String>) {
    var student = Student()
    student.printStudent()
}

Student类里有个printStudent()函数,打印学生的信息。现在有个需求想要打印名字的同时还打印省份。
你可能会说:直接在printStudent()加入打印省份信息不就得了?
如果是第三方的文件呢?咱们没权限修改源文件,在Java 里我们一般通过包装Student类,再提供打印学生姓名和省份的方法。
而Koltin里更简洁,可以直接对这个类进行函数扩展。

fun main(args: Array<String>) {
    var student = Student()
    student.printStudent1()
}
//扩展函数
fun Student.printStudent1() {
    println("name:$name province:$province")
}

以后在任何一个地方,只要想要打印姓名和省份都可以使用printStudent1()方法。
通过反编译结果,来看看扩展函数的原理:

public static final void printStudent1(@NotNull Student $this$printStudent1) {
    Intrinsics.checkNotNullParameter($this$printStudent1, "$this$printStudent1");
    String var1 = "name:" + $this$printStudent1.getName() + " province:" + $this$printStudent1.getProvince();
    boolean var2 = false;
    System.out.println(var1);
}

当扩展一个类的函数时,实际上传入了该类的对象,通过对象拿到属性/函数并操作。

因此,其本质上还是通过类的对象实例来组合各种操作。

假若现在将"province" 访问权限修改为"private",那么printStudent1 将无法访问到该属性。

扩展函数使用

扩展函数在扩展第三方库时非常有效,从原理上看我们知道它是没有任何副作用的。
假若我们来扩展String类,希望新增一个函数:判断String 首字母是否是大小。

fun String.isFirstUpper():Boolean {
    if (isNotEmpty()) {
        //判断字符范围
        return get(0).code in 65..97
    }
    return false
}

在Kotlin里调用:

fun main(args: Array<String>) {
    var student = Student()
    student.printStudent1()

    var b1 = "Fish".isFirstUpper()
    var b2 = "1Fish".isFirstUpper();
    println("$b1 $b2")
}

在Java里调用:

    private void testExpand() {
        //需要传入扩展类的对象实例
        boolean b1 = ExpandFunKt.isFirstUpper("Fish");
        boolean b2 = ExpandFunKt.isFirstUpper("1fish");
    }

扩展函数与成员函数异同

1、扩展函数不能访问"private" 修饰的函数和属性。
2、扩展函数不会影响原有类的构成(不属于类本身,不能被子类继承)。
3、扩展函数调用方式与成员函数调用方式类似,都可以通过对象调用。

3、Koltin 内联函数的原理与使用

内联函数原理

//普通函数
fun normalFun1() {
    println("normal fun")
}
//内联函数
inline fun inlineFun2() {
    println("inline fun")
}
fun main(args: Array<String>) {
    normalFun1()
    inlineFun2()
}

输出结果都很正常,看不出来啥。从写法上看,fun2比fun1 多了"inline"修饰。
接着看看反编译结果:

    public static final void main(@NotNull String[] args) {
        Intrinsics.checkNotNullParameter(args, "args");
        //函数调用
        normalFun1();
        int $i$f$inlineFun2 = false;
        //函数体替换
        String var2 = "inline fun";
        boolean var3 = false;
        System.out.println(var2);
    }

可以看出,当使用"inline" 修饰时,整个函数体被调用方直接复制过去了,而没有使用"inline"修饰时,则是正常的函数调用,这期间会经过:

函数局部变量、返回值等入栈,函数执行完成后出栈,继续从调用处往下执行。

压栈、出栈过程有一定的开销。

内联函数的使用

虽然使用内联可以减少一定的开销,但是不是每个地方都适合用内联修饰的。试想,若是都是内联函数,那么调用内联函数的时候会将整个函数体(实现)拷贝到调用处,如果是多次调用呢?岂不是重复的代码很多?
因此,在Kotlin 里普通函数是无需使用内联修饰的,我们上面的代码编译器会提示:

image.png

意思是:此种场景下使用内联对性能是没有提升的。
什么场景下使用呢?答案是函数参数是函数类型时使用。
定义高阶函数:

fun inlineFun3(block: (Int) -> String): String {
    println("execute fun3")
    return block(3)
}

其参数block 即为函数类型的变量,此处用Lambda表示。
调用inlineFun3:

fun main(args: Array<String>) {
    var str = inlineFun3 {
        if (it > 3) {
            ">3"
        } else {
            "<=3"
        }
    }
    println("str $str")
}

看反编译结果:

   public static final void main(@NotNull String[] args) {
      String str = inlineFun3((Function1)null.INSTANCE);
      String var2 = "str " + str;
      boolean var3 = false;
      System.out.println(var2);
   }

可以看出上面的block 变为了Function1,当调用一个高阶函数时,其函数类型的参数最终都会编译为FunctionX 接口。
也就是说当调用inlineFun3()时,内部是生成了一个FunctionX的对象。
而当我们用inline 修饰inlineFun3()时,最终的反编译如下:

inline fun inlineFun3(block: (Int) -> String): String {
    println("execute fun3")
    return block(3)
}

public static final void main(@NotNull String[] args) {
        String var3 = "execute fun3";
        System.out.println(var3);
        int it = 3;
        String str = it > 3 ? ">3" : "<=3";
        String var7 = "str " + str;
        System.out.println(var7);
    }

总结来说,使用inline 修饰高阶函数有两个好处:

1、当调用高阶函数时,可以避免生成对象,减少开销。
2、同时减少了函数调用的压栈出栈开销。

内联函数规则

参数传递规则

inline fun inlineFun4(block: (Int) -> String): String {
    println("execute fun4")
    //编译错误
    return inlineFun5(block)
}
fun inlineFun5(block: (Int) -> String): String {
    return block(3)
}

如上写法编译器会报错:内联函数的函数类型参数不能作为实参传递给另一个非内联函数。
inlineFun4 是内联函数,其形参为block,inlineFun5 是非内联函数,要想编译通过有两种方式:

1、inlineFun5 加上inline 修饰。
2、block 加上 noinline(禁止内联)修饰。

第二点对应如下:

inline fun inlineFun4(noinline block: (Int) -> String): String {
    println("execute fun4")
    return inlineFun5(block)
}

Return 规则

在上一篇中有说过:Lambda使用最后一条语句作为返回值,在Lambda里不能显示调用return。

fun inlineFun6(block: (Int) -> String): String {
    println("execute fun6")
    return block(3)
}

fun testReturn(): String {
    var str = inlineFun6 {
        if (it > 3) {
            ">3"
        } else {
            "<=3"
        }
        //编译错误
        return "fish"
    }
    println("execute inlineFun6 str:$str")
    return "fish"
}

此时的return 是不被允许的。
当然,也可以改造为如下:

fun testReturn(): String {
    var str = inlineFun6 {
        if (it > 3) {
            ">3"
        } else {
            "<=3"
        }
        //编译错误
        return@inlineFun6 "fish"
    }
    println("execute inlineFun6 str:$str")
    return "fish"
}

运行后发现,return 退出了inlineFun6函数的执行,但还是执行到了"println("execute inlineFun6 str:$str")",说明该return 函数并没有退出testReturn。此时给inlineFun6函数加上inline 修饰:

inline fun inlineFun6(block: (Int) -> String): String {
    println("execute fun6")
    return block(3)
}
fun testReturn(): String {
    var str = inlineFun6 {
        if (it > 3) {
            ">3"
        } else {
            "<=3"
        }
        //直接return
        return "fish"
    }
    println("execute inlineFun6 str:$str")
    return "fish"
}

Lambda里可以使用return函数,并且return 后退出了testReturn()函数。
由此可见:

当inline 修饰带有函数类型参数的函数时,在Lambda里可以使用return,并且执行到该return 语句时可以退出外层函数。

了解了泛型、扩展函数、内联函数,下篇将会分析常用的一些高阶函数:let/also/with/run/apply/repeat 的原理及其应用场景。

本文基于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 易懂易学系列

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

推荐阅读更多精彩内容