Kotlin入门(11)江湖绝技之特殊函数
上一篇文章介绍了Kotlin对函数的输入参数所做的增强之处,其实函数这块Kotlin还有好些重大改进,集中体现在几类特殊函数,比如泛型函数、内联函数、扩展函数、尾递归函数、高阶函数等等,因此本篇文章就对这几种特殊函数进行详细的说明。
泛型函数
函数的输入参数类型必须在定义函数时就要指定,可是有时候参数类型是不确定的,只有在函数调用时方能知晓具体类型,如此一来要怎样声明函数呢?其实在之前的文章《Kotlin入门(4)声明与操作数组》里面,就遇到了类似的情况,当时为了采取统一的格式声明基本类型的数组对象,使用“Array<变量类型>”来声明数组对象,并通过arrayOf函数获得数组对象的初始值,具体代码如下所示:
var int_array:Array<Int> = arrayOf<Int>(1, 2, 3) var long_array:Array<Long> = arrayOf<Long>(1, 2, 3) var float_array:Array<Float> = arrayOf<Float>(1.0f, 2.0f, 3.0f)
注意到尖括号内部指定了数组元素的类型,这正是泛型对象的写法,“Array<变量类型>”可称作泛型变量,至于arrayOf便是本文要说的泛型函数了。
定义泛型函数时,得在函数名称前面添加“<T>”,表示以T声明的参数(包括输入参数和输出参数),其参数类型必须在函数调用时指定。下面举个泛型函数的定义例子,目的是把输入的可变参数逐个拼接起来,并返回拼接后的字符串,示例代码如下:
//Kotlin允许定义全局函数,即函数可在类外面单独定义,然后其他类也能直接调用 fun <T> appendString(tag:String, vararg otherInfo: T?):String { var str:String = "$tag:" for (item in otherInfo) { str = "$str${item.toString()}," } return str }
调用上面的泛型函数appendString,就跟调用arrayOf方法一样,只需在函数名称后面添加“<变量类型>”即可,然后输入参数照原样填写。以下是appendString函数的调用代码例子:
var count = 0 btn_vararg_generic.setOnClickListener { tv_function_result.text = when (count%3) { 0 -> appendString<String>("古代的四大发明","造纸术","印刷术","火药","指南针") 1 -> appendString<Int>("小于10的素数",2,3,5,7) else -> appendString<Double>("烧钱的日子",5.20,6.18,11.11,12.12) } count++ }
内联函数
注意到前面定义泛型函数appendString,是把它作为一个全局函数,也就是在类外面定义,不在类内部定义。因为类的成员函数依赖于类,只有泛型类(又称模板类)才能拥有成员泛型函数,普通类是不允许定义泛型函数的,否则编译器会直接报错。不过有个例外情况,如果参数类型都是继承自某种类型,那么允许在定义函数时指定从这个基类泛化开,凡是继承自该基类的子类,都可以作为输入参数进行函数调用,反之则无法调用函数。
举个例子,Int、Float和Double都继承自Number,但是定义一个setArrayNumber(array:Array<Number>)函数,它并不接受Array<Int>或者Array<Double>的入参,如果要让该方法同时接受源自Number的数组入参,就得定义泛化自Number的泛型函数,即将<T>改为<reified T : Number>,同时在fun前面添加关键字inline,表示该函数也为内联函数。内联函数在编译之时,会在调用处把该函数的内部代码直接复制一份,调用十次就会复制十份,而非普通函数那样仅仅提供一个函数的访问地址。该例子的函数定义代码如下所示:
//该函数不接受Array<Int>,也不接受Array<Double>,只好沦为孤家寡人 fun setArrayNumber(array:Array<Number>) { var str:String = "数组元素依次排列:" for (item in array) { str = str + item.toString() + ", " } tv_function_result.text = str } //只有内联函数才可以被具体化 inline fun <reified T : Number> setArrayStr(array:Array<T>) { var str:String = "数组元素依次排列:" for (item in array) { str = str + item.toString() + ", " } tv_function_result.text = str }
上面的泛型函数兼内联函数setArrayStr,定义的时候稍显麻烦,不过调用的方式没有变化,依旧在函数名称后面补充“<变量类型>”。该函数的调用代码示例如下:
var int_array:Array<Int> = arrayOf(1, 2, 3) var float_array:Array<Float> = arrayOf(1.0f, 2.0f, 3.0f) var double_array:Array<Double> = arrayOf(11.11, 22.22, 33.33) //Kotlin进行函数调用时,要求参数类型完全匹配。所以即使Int继承自Number类,也不能调用setArrayNumber方法传送Int类型 //btn_generic_number.setOnClickListener { setArrayNumber(int_array) } btn_generic_number.setOnClickListener { when (count%3) { 0 -> setArrayStr<Int>(int_array) 1 -> setArrayStr<Float>(float_array) else -> setArrayStr<Double>(double_array) } count++ }
扩展函数
系统自带的类已经提供了许多方法,然而经常还是无法完全满足业务需求,此时开发者往往要写个工具类,比如StringUtil、DateUtil之类,来补充相关的处理功能,长此以往,工具类越来越多也越来越难以管理。
基于以上情况,Kotlin推出了扩展函数的概念,允许开发者给系统类补写新的方法,而无需另外编写额外的工具类。比如系统自带的数组Array提供了求最大值的max方法,提供了进行排序的sort方法,可是并未提供交换数组元素的方法。于是我们打算给Array增加新的交换方法,也就是添加一个扩展函数swap,与众不同的是要在函数名称前面加上“Array<Int>.”,表示该函数扩展自Array<Int>。swap函数的定义代码如下所示:
fun Array<Int>.swap(pos1: Int, pos2: Int) { val tmp = this[pos1] //this表示数组对象自身 this[pos1] = this[pos2] this[pos2] = tmp }
不过该函数的缺点是显而易见的,它声明了扩展自Array<Int>,也就意味着只能用于整型数组,不能用于包括浮点数组、双精度数组在内的其它数组对象。因此,为了增强交换函数的通用性,必须把swap改写为泛型函数,即尖括号内部使用T代替Int。改写为泛型函数的代码见下:
//扩展函数结合泛型函数,能够更好地扩展函数功能 fun <T> Array<T>.swap(pos1: Int, pos2: Int) { val tmp = this[pos1] //this表示数组对象自身 this[pos1] = this[pos2] this[pos2] = tmp }
有了扩展函数之后,数组对象可以直接调用新增的swap方法,仿佛该函数是系统自带的方法,用起来毫不费劲,真是开发者的福音。以下是swap函数的调用代码例子:
//val array:Array<Int> = arrayOf(1, 2, 3, 4, 5) val array:Array<Double> = arrayOf(1.0, 2.0, 3.0, 4.0, 5.0) btn_function_extend.setOnClickListener { //下标为0和3的两个数组元素进行交换 //array可以是整型数组,也可以是双精度数组 array.swap(0, 3) setArrayStr<Double>(array) }
尾递归函数
Kotlin引入了扩展函数,还能反过来精简函数。具体地说,如果一个函数的表达式比较简单,一两行就可以搞定的话,Kotlin允许使用等号代替大括号。例如数学上计算n!的阶乘函数,5!=5*4*3*2*1,这个阶乘函数使用Kotlin代码的书写格式如下所示:
fun factorial(n:Int):Int { if (n <= 1) n else n*factorial(n-1) }
从上看到阶乘函数类似Java中的“判断条件?取值A:取值B”三元表达式,只不过内部递归调用函数自身而已。前两篇文章提到Kotlin把函数当作一种特殊的变量类型,所以接下来也允许通过等号给函数这个特殊的变量进行赋值。下面便是使用等号改写后的阶乘函数代码:
fun factorial(n:Int):Int = if (n <= 1) n else n*factorial(n-1)
这里的阶乘函数是个普通的递归函数,Kotlin体系还存在一种特殊的递归函数,名叫尾递归函数,它指的是函数末尾的返回值重复调用了自身函数。此时要在fun前面加上关键字tailrec,告诉编译器这是个尾递归函数,则编译器会相应进行优化,从而提高程序性能。以下是个尾递归函数的声明代码例子:
//如果函数尾部递归调用自身,则可加上关键字tailrec表示这是个尾递归函数, //此时编译器会自动优化递归,即用循环方式代替递归,从而避免栈溢出的情况。 //比如下面这个求余弦不动点的函数就是尾递归函数 tailrec fun findFixPoint(x: Double = 1.0): Double = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))
高阶函数
前面多次提到函数被Kotlin当作特殊变量,包括函数声明采取跟变量声明一样的形式“名称:类型”,以及简化函数允许直接用等号连接函数体等等,那么本节最后讲述的则是把A函数作为B函数的输入参数,就像普通变量一样参与B函数的表达式计算。此时因为B函数的入参内嵌了A函数,故而B函数被称作高阶函数,对应的A函数则为低阶函数。
为了解释地更加清楚些,我们来看一个例子。对于一个数组对象,若想求得该数组元素的最大值,可以调用数组对象的max方法。现在有个字符串数组Array<String>,倘使调用该数组对象的max方法,返回的并非最长的字符串,而是按首字母排序在字母表最靠后的那个字符串。比如字符串数组为arrayOf("How", "do", "you", "do", "I'm ", "Fine"),调用max方法获得的字符串为“you”,而不是长度最长的的那个字符串。
当然你也可以写个单独的函数专门判断字符串长度,然而要是哪天需要其它比较大小的算法,难道又得再写一个全新的比较函数?显然这么做的代价不菲,所以Kotlin引入了高阶函数这个秘密武器,直接把这个算法作为参数传进来,由开发者在调用高阶函数时再指定具体的算法函数。就获取数组对象的最大值而言,实现该功能框架的高阶函数代码如下所示:
//允许将函数表达式作为输入参数传进来,就形成了高阶函数,这里的greater函数就像是个变量 fun <T> maxCustom(array: Array<T>, greater: (T, T) -> Boolean): T? { var max: T? = null for (item in array) if (max == null || greater(item, max)) max = item return max }
上面高阶函数的第二个参数就是一个函数变量,其中变量名称为greater,“(T, T)”表示该函数有两个类型为T的参数,然后低阶函数的返回值是Boolean类型。有了高阶函数的定义,再来看看如何调用这个高阶函数,调用的示例代码如下:
var string_array:Array<String> = arrayOf("How", "do", "you", "do", "I'm ", "Fine") btn_function_higher.setOnClickListener { tv_function_result.text = when (count%4) { //string_array.max()返回的是you 0 -> "字符串数组的默认最大值为${string_array.max()}" //因为高阶函数maxCustom同时也是泛型函数,所以要在函数名称后面加上<String> 1 -> "字符串数组按长度比较的最大值为${maxCustom<String>(string_array, { a, b -> a.length > b.length })}" //string_array.max()对应的高阶函数是maxCustom(string_array, { a, b -> a > b }) 2 -> "字符串数组的默认最大值(使用高阶函数)为${maxCustom(string_array, { a, b -> a > b })}" //因为系统可以根据string_array判断泛型函数采用了String类型,故而函数名称后面的<String>也可以省略掉 else -> "字符串数组按去掉空格再比较长度的最大值为${maxCustom(string_array, { a, b -> a.trim().length > b.trim().length })}" } count++ }
以上代码在调用maxCustom函数时,第二个参数被大括号包了起来,这是Lambda表达式的匿名函数写法,中间的“->”把匿名函数分为两部分,前半部分表示函数的输入参数,后半部分表示函数体。“{ a, b -> a.length > b.length }”按照规范的函数写法是下面这样的代码:
fun anonymous(a:String, b:String):Boolean { var result:Boolean = a.length > b.length return result }
前述的高阶函数maxCustom同时结合了泛型函数的写法,其实还可以给它加上扩展函数的功能。因为该函数的目的是求数组对象的最大值,所以不妨将该函数扩展到Array<T>中去,扩展后的高阶函数代码示例如下:
fun <T> Array<T>.maxCustomize(greater: (T, T) -> Boolean): T? { var max: T? = null for (item in this) if (max == null || greater(item, max)) max = item return max }
相对应的,maxCustomize将作为数组对象的成员函数进行调用,而非maxCustom那样把数组对象作为入参。改写后的调用代码如下所示:
btn_function_higher.setOnClickListener { tv_function_result.text = when (count%4) { 0 -> "字符串数组的默认最大值为${string_array.max()}" //下面是结合高阶函数与扩展函数的调用代码 1 -> "字符串数组按长度比较的最大值为${string_array.maxCustomize({ a, b -> a.length > b.length })}" 2 -> "字符串数组的默认最大值(使用高阶函数)为${string_array.maxCustomize({ a, b -> a > b })}" else -> "字符串数组按去掉空格再比较长度的最大值为${string_array.maxCustomize({ a, b -> a.trim().length > b.trim().length })}" } count++ }
总结一下,本文一口气介绍了Kotlin的五个特殊函数,包括泛型函数、内联函数、扩展函数、尾递归函数、高阶函数,同时穿插说明了全局函数、简化函数和匿名函数,并通过实际应用叙述了多种函数结合起来的写法。通过本文与前面两篇文章的描述,读者应能掌握Kotlin对函数的大部分用法。
__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。