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对函数的大部分用法。
__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
Kotlin入门(11)江湖绝技之特殊函数的更多相关文章
- Kotlin入门教程——目录索引
Kotlin是谷歌官方认可的Android开发语言,Android Studio从3.0版本开始就内置了Kotlin,所以未来在App开发中Kotlin取代Java是大势所趋,就像当初Android ...
- Kotlin入门(15)独门秘笈之特殊类
上一篇文章介绍了Kotlin的几种开放性修饰符,以及如何从基类派生出子类,其中提到了被abstract修饰的抽象类.除了与Java共有的抽象类,Kotlin还新增了好几种特殊类,这些特殊类分别适应不同 ...
- Kotlin入门第二课:集合操作
测试项目Github地址: KotlinForJava 前文传送: Kotlin入门第一课:从对比Java开始 初次尝试用Kotlin实现Android项目 1. 介绍 作为Kotlin入门的第二课, ...
- 写给Android开发者的Kotlin入门
写给Android开发者的Kotlin入门 转 https://www.jianshu.com/p/bb53cba6c8f4 Google在今年的IO大会上宣布,将Android开发的官方语言更换为K ...
- Kotlin入门(32)网络接口访问
手机上的资源毕竟有限,为了获取更丰富的信息,就得到辽阔的互联网大海上冲浪.对于App自身,也要经常与服务器交互,以便获取最新的数据显示到界面上.这个客户端与服务端之间的信息交互,基本使用HTTP协议进 ...
- Kotlin入门(28)Application单例化
Application是Android的又一大组件,在App运行过程中,有且仅有一个Application对象贯穿应用的整个生命周期,所以适合在Application中保存应用运行时的全局变量.而开展 ...
- Kotlin入门(5)字符串及其格式化
上一篇文章介绍了数组的声明和操作,包括字符串数组的用法.注意到Kotlin的字符串类也叫String,那么String在Java和Kotlin中的用法有哪些差异呢?这便是本文所要阐述的内容了. 首先要 ...
- Kotlin入门(9)函数的基本用法
上一篇文章介绍了Kotlin新增的空安全机制,控制语句部分可算是讲完了,接下来将连续描述Kotlin如何定义和调用函数,本篇文章先介绍函数的基本用法. 前面几篇文章介绍控制语句之时,在setOnCli ...
- Kotlin入门(13)类成员的众生相
上一篇文章介绍了类的简单定义及其构造方式,当时为了方便观察演示结果,在示例代码的构造函数中直接调用toast提示方法,但实际开发是不能这么干的.合理的做法是外部访问类的成员属性或者成员方法,从而获得处 ...
随机推荐
- B - Red and Black 问题思考
红黑地板问题 There is a rectangular room, covered with square tiles. Each tile is colored either red or bl ...
- 关于@font-face的使用
以前在写网页的时候,总是使用浏览器默认的字体,因此从未使用过@font-face,然而,最近在做官网的时候,UI规定了字体,要在所有浏览器下都展现同一效果.多番查询下,发现@font-face用起来是 ...
- 关于文件命名,你必须要知道的(浏览器报错:net::ERR_BLOCKED_BY_CLIENT)
坑爹的,今天在写完页面,用各个浏览器测试的时候,火狐.谷歌都是正常的,QQ浏览器出幺蛾子了,在使用兼容模式的时候页面正常,使用急速模式的时候部分页面正常,点击跳转到其他页面的时候就出错了,打开控制台一 ...
- C# 使用PrintDocument类打印标签
最近做了一个项目,使用不干胶标签贴在RFID抗金属标签上,那么就会出现标签打印的问题,该如何打印呢?后来经过网上冲浪发现,其实打印标签和打印A4纸的方法一样,只不过就是布局.设置纸张大小的问题. 本文 ...
- python基础学习笔记 - 备忘
基础中的基础 Python标识符 命名规则: Python标识符区分大小写. 可以包括英文.数字以及下划线,但不能以数字开头. 以下划线开头的标识符是有特殊意义的: a) 以单下划线开 ...
- Hive的union和join操作
建表语句: create table tb_in_base ( id bigint, devid bigint, devname string ) partitioned b ...
- javascript的hashCode实现
hashCode = function(str){ var hash = 0; if (str.length == 0) return hash; for (i = 0; i < str.len ...
- MVC5笔记
创建一个MVC网站后,我们可以在/app_strat/routeConfig.cs中来查看集中控制路的方法,RegisterRoutes方法(注册路由),我们改一下,删除默认的RegisterRout ...
- interface21 - web - ContextLoaderListener(Spring Web Application Context加载流程)
前言 最近打算花点时间好好看看spring的源码,然而现在Spring的源码经过迭代的版本太多了,比较庞大,看起来比较累,所以准备从最初的版本(interface21)开始入手,仅用于学习,理解其设计 ...
- Apache Commons Digester 二(规则模块绑定-RulesModule、异步解析-asyncParse、xml变量Substitutor、带参构造方法)
前言 上一篇对Digester做了基本介绍,也已经了解了Digester的基本使用方法,接下来将继续学习其相关特性,本篇主要涉及以下几个内容: 规则模块绑定,通过定义一个RulesModule接口实现 ...