当前位置: 首页 > 科技观察

Android进阶Kotlin高阶函数原理及Standard.kt源码详解

时间:2023-03-16 12:01:47 科技观察

本文转载自微信公众号《Android开发编程》,作者Android开发编程。转载本文请联系Android开发编程公众号。前言在Kotlin中,高阶函数指的是一个函数,它被用作另一个函数的参数或返回值。如果用f(x)和g(x)来表示两个函数,那么高阶函数可以表示为f(g(x))。Kotlin为开发者提供了丰富的高阶函数,比如Standard.kt中的let、with、apply等,_Collectioins.kt中的forEach。为了能够自如地使用这些高阶函数,我们有必要了解如何使用这些高阶函数。今天我们来讲解一下高阶函数1.高阶函数详解1.什么是高阶函数?如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么这个函数就称为高阶函数。与java不同的是,Kotlin中加入了一个函数类型的概念。如果我们把这个函数加入到一个函数的参数声明或者返回值声明中,那么这就是一个高阶函数。函数类型语法的基本规则:(String,Int)->Unit添加到函数的参数声明中publicfuntest2(test:Int,block:()->Unit){varv=block()DTLog.i("TestTest","Test1")}publicfunT.test22(block:()->T):T{returnblock()}publicfunT.test26(block:T.()->Unit){block()}publicfunT.test23(block:(T)->Unit):T{returnthis}publicfunT.test3(block:(T)->R):R{vart=block(this)returnt}publicfunT.test4(block:(T)->W):W{returnblock(this)}上面是一个高阶函数,接收一个函数类型的参数,调用高阶函数的方法与调用普通函数没有太大区别,只需要在参数名后面加上括号,在括号内传入必要的参数即可;高阶函数类型有一个特殊的符号对应于函数签名,即它们的参数和返回值:所有函数类型都有一个用括号括起来的参数类型列表和一个返回类型:(A,B)->C表示它接受A和B类型的两个参数,并返回函数类型的C类型值。参数类型列表可以为空,如()->A,返回值可以为空,如(A,B)->Unit;函数类型可以有一个额外的接收者类型,它在符号之前指定,例如,类型A.(B)->C表示函数以类型B作为参数并返回类型的值C可以在A的接收者对象上调用。还有一种特殊类型的函数,suspend函数,它的表示法中有一个suspend修饰符,例如suspend()->Unit或suspendA.(B)->C。2、内联函数详解①什么是内联函数inline(注意,不是在线),翻译成“内联”或“嵌入”。意思是:当编译器发现某段代码在调用内联函数时,不调用该函数,而是将该函数的全部代码插入到当前位置。这样做的好处是省去了调用的过程,加快了程序的运行速度。(函数的调用过程总是因为前面提到的参数入栈等操作而花费更多的时间)。这样做的缺点是每当代码调用一个内联函数时,需要在调用处直接插入一段函数代码,这样会增加程序的体积。用生活现象来比喻,就好像电视机坏了,你觉得太慢了,找修理工也来不及了,干脆在家里留了个修理工。这样当然更快,但是修理工住你家会占很多地方。内联函数不是必需的,它只是速度的装饰。将函数修饰为内联,使用如下格式:内联函数的声明或定义是一个简单的语句,在函数声明或定义前加上一个内联修饰符。inlineintmax(inta,intb){return(a>b)?a:b;}内联函数的本质是节省时间但消耗空间。②内联函数的规则内联函数的规则(1)、函数可以调用自身,称为递归调用(后述),包含递归调用的函数不能设置为内联;(2)、使用复杂的流程控制语句:循环语句和switch语句不能设置为inline;(3)、由于inline增加体积的特点,建议inline函数中的代码尽量短。最好不要超过5行。(4)、inline仅作为“请求”使用。在某些情况下,编译器会忽略inline关键字并强制函数成为普通函数。发生这种情况时,编译器会给出警告信息。(5)在你调用一个内联函数之前,这个函数必须先被声明或定义为内联函数。如果在前面声明为普通函数,在调用代码后定义为内联函数,程序可以通过编译,但函数不是内联实现的。例如下面的代码片段://函数一开始没有声明为inline:voidfoo();//然后有代码调用它:foo();//函数定义为inline:inlinevoidfoo()之后调用{...}代码中的foo()函数没有内联实现;(6)、为了调试方便,当程序处于调试阶段时,所有的内联函数都没有实现③内联函数时要注意以下问题(1)在一个文件中定义的内联函数不能在另一个文件中使用。它们通常在头文件中共享。(2)内联函数要简洁,只有几条语句。如果语句很多,不适合定义为内联函数。(3)在内联函数体中,不能有循环语句、if语句或switch语句,否则,即使函数定义中有inline关键字,编译器也会将该函数视为非内联函数。(4)内联函数必须在调用函数之前声明。关键字inline必须和函数定义体放在一起才能使函数内联,只是把inline放在函数声明的前面是没有作用的。3、将高阶函数中inline函数直接使用的lambda表达式,在底层转化为匿名类的实现方法。这意味着我们每次调用Lambda表达式时,都会创建一个新的匿名类实例,这当然会造成额外的内存和性能开销。为了解决这个问题,Kotlin提供了内联函数的功能,可以彻底消除使用Lambda表达式带来的运行时开销。定义高阶函数时只需要加上inline关键字的声明即可。inlinefuntest111(num1:Int,num2:Int,block:(Int,Int)->Int):Int{valresult=block(num1,num2)returnresult}4.闭包函数函数的返回值是一个函数,而函数内部包含另一个函数,可以是带参数和不带参数的匿名函数funmain(args:Array){valmm=aaa()println(mm())println(mm())println(mm())println(mm())println(mm())valkk=bbb()println(kk("shadow"))//shadow---1println(kk("shadow"))//shadow---2println(kk("shadow")"))//shadow---3println(kk("shadow"))//shadow---4println(kk("shadow"))//shadow---5}//闭包函数就是作为返回参数的函数funaaa():()->(Int){varcurrent=10returnfun():Int{returncurrent++}}funbbb():(String)->(String){varcurrent=0;returnfun(str:String):String{current++;return"$str---$current";}}其次,kotin中的标准库Standard.kt源码提供了一些方便的内置高阶函数(let,also,with,run,apply),可以帮助我们写出更简洁的一个nd优雅的Kotlin代码,提高开发效率,学习源码可以帮助我们更快的理解和应用1、apply@kotlin.internal.InlineOnlypublicinlinefunT.apply(block:T.()->Unit):T{contract{callsInPlace(block,InvocationKind.EXACTLY_ONCE)}block()returnthis}作为块函数参数传递(调用时可以省略)),而apply函数的返回值就是调用者本身;执行类型为T的方法、变量等,然后返回自身T;注意参数block:T.(),每当看到block:T.()->这种代码Block,意思是T里面的API可以直接在花括号{}里面调用,不用加T。这个[其实叫this.,this.通常省略]valstr="hello"str.apply{length}//可以省略str.str.apply{this.length}//可以这样做2、let@kotlin.internal.InlineOnlypublicinlinefunT.let(block:(T)->R):R{contract{callsInPlace(block,InvocationKind.EXACTLY_ONCE)}returnblock(this)}let方法是将类型T转为返回另一种类型R的形式;作为block函数参数传递,let函数的返回值由block函数决定;3.also@kotlin.internal.InlineOnly@SinceKotlin("1.1")publicinlinefunT.also(block:(T)->Unit):T{contract{callsInPlace(block,InvocationKind.EXACTLY_ONCE)}block(this)returnthis}执行一个T类型的方法、变量等,然后返回自己T;作为块函数参数传递(调用时不能省略),also函数的返回值是调用者本身;该方法与上面的apply方法类似,但是在花括号中执行T自己的方法时,必须加上T,否则无法调用T中的API。这是什么意思?看下面的代码:valstr="hello"str.also{str.length}//str.必须加上,否则编译器会报错str.also{it.length}//或者用它。4,with@kotlin.internal.InlineOnlypublicinlinefunwith(receiver:T,block:T.()->R):R{contract{callsInPlace(block,InvocationKind.EXACTLY_ONCE)}returnreceiver.block()}with()方法接收一个T类型的参数,处理后的代码块返回R类型的结果valstr="你好"valch=with(str){get(0)}println(ch)//打印h5,run@kotlin.internal.InlineOnlypublicinlinefunrun(block:()->R):R{contract{callsInPlace(block,InvocationKind.EXACTLY_ONCE)}returnblock()}需要传递一个代码块,返回任意类型;每当一个函数接收到一个代码块时,一般建议使用{}来包含代码块中的逻辑,只有在某些特殊情况下才能将参数(::fun)形式简化为@kotlin.internal.InlineOnlypublicinlinefunT.run(block:T.()->R):R{contract{callsInPlace(block,InvocationKind.EXACTLY_ONCE)}returnblock()}这里是执行一个T类型的run方法,还是传递一个代码块,但是内部执行的是T内部的一个变量或者方法,返回一个R类型run{println(888)}valres=run{2+3}funrunDemo(){println("测试运行方法")}//我们可以这样做run(::runDemo)6、takeIfpublicinlinefunT.takeIf(predicate:(T)->Boolean):T?{contract{callsInPlace(predicate,InvocationKind.EXACTLY_ONCE)}returnif(predicate(this))thiselsenull}根据传入的参数T进行内部判断,根据判断结果返回null或者T本身;传入的是[unarypredicate]代码块,非常类似于C++中的一元谓词:方法只包含一个参数,返回类型为Boolean类型;在源码中,通过传入的一元谓词代码块进行判断,为真则返回自身,否则返回null;valstr="helloWorld"str.takeIf{str.contains("hello")}?.run(::println)7,takeUnlesspublicinlinefunT.takeUnless(predicate:(T)->Boolean):T?{contract{callsInPlace(predicate,InvocationKind.EXACTLY_ONCE)}returnif(!predicate(this))thiselsenull}这个方法和takeIf()方法类似,只是在内部判断为false时返回T,为真时返回null,就不多解释了,使用takeIf()方法8.repeat()publicinlinefunrepeat(times:Int,action:(Int)->Unit){contract{callsInPlace(action)}for(indexin0untiltimes){action(index)}}分析:repeat方法包含两个参数:第一个第一个参数是int类型,重复次数,第二个参数表示要重复执行的对象。该方法每次执行时都会将执行次数传递给要执行的模块。至于重复执行模块是否需要这个值,需要根据业务实际需要,例如://打印0到100之间的值,使用内部索引repeat(100){print(it)}//比如简单打印helloworld100次,索引值不用repeat(100){println("helloworld")}3.高阶函数选择如果需要返回给调用者本身(即,returnthis),可以选择applyalso如果需要将this作为参数传递可以选择applyrunwith如果需要将其作为参数传递可以选择letalso如果返回值需要由函数(即returnblock()),可以选择runwithlet总结一下是Kotlin自带的高阶函数还是我们自己自定义的,传入的代码块样式无外乎以下几种:1.block:()->T和block:()->具体类型。使用(::fun)简化形式时,要求传入的方法必须是无参数的,并且return如果值类型为T,可以是任意类型,否则返回类型必须与返回类型一致这段代码block2,block:T.()->Randblock:T.()->specifictypesareused当简化(::fun)的形式时,要求传入的方法必须包含一个T类型的参数,如果返回值类型为R,则可以是任何类型,否则返回类型必须与本代码块的返回类型一致。比如with和apply这两个方法3、block:(T)->Randblock:(T)->concretetype当使用(::fun)简化时,传入的方法必须包含一个T类型的参数,如果返回值类型为R,则可以是任何类型,否则返回类型必须与本代码块的返回类型一致。比如let和takeIf这两个方法