Kotlin入门(11)江湖绝技之特殊函数

上一篇文章介绍了Kotlin对函数的输入参数所做的增强之处,其实函数这块Kotlin还有好些重大改进,集中体现在几类特殊函数,比如泛型函数、内联函数、扩展函数、尾递归函数、高阶函数等等,因此本篇文章就对这几种特殊函数进行详细的说明。


泛型函数

函数的输入参数类型必须在定义函数时就要指定,可是有时候参数类型是不确定的,只有在函数调用时方能知晓具体类型,如此一来要怎样声明函数呢?其实在之前的文章《 Kotlin入门(4)声明与操作数组》里面,就遇到了类似的情况,当时为了采取统一的格式声明基本类型的数组对象,使用“Array<变量类型>”来声明数组对象,并通过arrayOf函数获得数组对象的初始值,具体代码如下所示:
    var int_array:Array = arrayOf(1, 2, 3)
    var long_array:Array = arrayOf(1, 2, 3)
    var float_array:Array = arrayOf(1.0f, 2.0f, 3.0f)
注意到尖括号内部指定了数组元素的类型,这正是泛型对象的写法,“Array<变量类型>”可称作泛型变量,至于arrayOf便是本文要说的泛型函数了。
定义泛型函数时,得在函数名称前面添加“”,表示以T声明的参数(包括输入参数和输出参数),其参数类型必须在函数调用时指定。下面举个泛型函数的定义例子,目的是把输入的可变参数逐个拼接起来,并返回拼接后的字符串,示例代码如下:
//Kotlin允许定义全局函数,即函数可在类外面单独定义,然后其他类也能直接调用
fun  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("古代的四大发明","造纸术","印刷术","火药","指南针")
            1 -> appendString("小于10的素数",2,3,5,7)
            else -> appendString("烧钱的日子",5.20,6.18,11.11,12.12)
        }
        count++
    }


内联函数

注意到前面定义泛型函数appendString,是把它作为一个全局函数,也就是在类外面定义,不在类内部定义。因为类的成员函数依赖于类,只有泛型类(又称模板类)才能拥有成员泛型函数,普通类是不允许定义泛型函数的,否则编译器会直接报错。不过有个例外情况,如果参数类型都是继承自某种类型,那么允许在定义函数时指定从这个基类泛化开,凡是继承自该基类的子类,都可以作为输入参数进行函数调用,反之则无法调用函数。
举个例子,Int、Float和Double都继承自Number,但是定义一个setArrayNumber(array:Array)函数,它并不接受Array或者Array的入参,如果要让该方法同时接受源自Number的数组入参,就得定义泛化自Number的泛型函数,即将改为,同时在fun前面添加关键字inline,表示该函数也为内联函数。内联函数在编译之时,会在调用处把该函数的内部代码直接复制一份,调用十次就会复制十份,而非普通函数那样仅仅提供一个函数的访问地址。该例子的函数定义代码如下所示:
    //该函数不接受Array,也不接受Array,只好沦为孤家寡人
    fun setArrayNumber(array:Array) {
        var str:String = "数组元素依次排列:"
        for (item in array) {
            str = str + item.toString() + ", "
        }
        tv_function_result.text = str
    }

    //只有内联函数才可以被具体化
    inline fun  setArrayStr(array:Array) {
        var str:String = "数组元素依次排列:"
        for (item in array) {
            str = str + item.toString() + ", "
        }
        tv_function_result.text = str
    }
上面的泛型函数兼内联函数setArrayStr,定义的时候稍显麻烦,不过调用的方式没有变化,依旧在函数名称后面补充“<变量类型>”。该函数的调用代码示例如下:
    var int_array:Array = arrayOf(1, 2, 3)
    var float_array:Array = arrayOf(1.0f, 2.0f, 3.0f)
    var double_array:Array = 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_array)
            1 -> setArrayStr(float_array)
            else -> setArrayStr(double_array)
        }
        count++
    }


扩展函数

系统自带的类已经提供了许多方法,然而经常还是无法完全满足业务需求,此时开发者往往要写个工具类,比如StringUtil、DateUtil之类,来补充相关的处理功能,长此以往,工具类越来越多也越来越难以管理。
基于以上情况,Kotlin推出了扩展函数的概念,允许开发者给系统类补写新的方法,而无需另外编写额外的工具类。比如系统自带的数组Array提供了求最大值的max方法,提供了进行排序的sort方法,可是并未提供交换数组元素的方法。于是我们打算给Array增加新的交换方法,也就是添加一个扩展函数swap,与众不同的是要在函数名称前面加上“Array.”,表示该函数扩展自Array。swap函数的定义代码如下所示:
fun Array.swap(pos1: Int, pos2: Int) {
    val tmp = this[pos1] //this表示数组对象自身
    this[pos1] = this[pos2]
    this[pos2] = tmp
}
不过该函数的缺点是显而易见的,它声明了扩展自Array,也就意味着只能用于整型数组,不能用于包括浮点数组、双精度数组在内的其它数组对象。因此,为了增强交换函数的通用性,必须把swap改写为泛型函数,即尖括号内部使用T代替Int。改写为泛型函数的代码见下:
//扩展函数结合泛型函数,能够更好地扩展函数功能
fun  Array.swap(pos1: Int, pos2: Int) {
    val tmp = this[pos1] //this表示数组对象自身
    this[pos1] = this[pos2]
    this[pos2] = tmp
}
有了扩展函数之后,数组对象可以直接调用新增的swap方法,仿佛该函数是系统自带的方法,用起来毫不费劲,真是开发者的福音。以下是swap函数的调用代码例子:
    //val array:Array = arrayOf(1, 2, 3, 4, 5)
    val array:Array = arrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
    btn_function_extend.setOnClickListener {
        //下标为0和3的两个数组元素进行交换
        //array可以是整型数组,也可以是双精度数组
        array.swap(0, 3)
        setArrayStr(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,倘使调用该数组对象的max方法,返回的并非最长的字符串,而是按首字母排序在字母表最靠后的那个字符串。比如字符串数组为arrayOf("How", "do", "you", "do", "I'm   ", "Fine"),调用max方法获得的字符串为“you”,而不是长度最长的的那个字符串。
当然你也可以写个单独的函数专门判断字符串长度,然而要是哪天需要其它比较大小的算法,难道又得再写一个全新的比较函数?显然这么做的代价不菲,所以Kotlin引入了高阶函数这个秘密武器,直接把这个算法作为参数传进来,由开发者在调用高阶函数时再指定具体的算法函数。就获取数组对象的最大值而言,实现该功能框架的高阶函数代码如下所示:
//允许将函数表达式作为输入参数传进来,就形成了高阶函数,这里的greater函数就像是个变量
fun  maxCustom(array: Array, 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 = 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同时也是泛型函数,所以要在函数名称后面加上
            1 -> "字符串数组按长度比较的最大值为${maxCustom(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类型,故而函数名称后面的也可以省略掉
            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中去,扩展后的高阶函数代码示例如下:
fun  Array.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对函数的大部分用法。


点此查看Kotlin入门教程的完整目录


__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
Kotlin入门(11)江湖绝技之特殊函数_第1张图片

你可能感兴趣的:(kotlin开发,Kotlin开发App快速入门)