Kotlin编程指南(函数)
函数声明
kotlin中的函数使用fun
关键字声明:
fun minus(a: Int, b: Int): Int { return a - b } 复制代码
函数调用
对于顶级函数,标准调用形式如下:
val result = minus(10, 5) 复制代码
当需要调用成员函数时,则需要使用点「.」来操作:
File("file://xxx").readText() // 实例化File并调用它的readText方法 复制代码
函数参数
函数参数应该使用驼峰命名法(官网原话是:Pascal notation「帕斯卡表述法」,即:首字母大写及后面每个单词的首字母都大写,但实际使用的却是驼峰命名法,怀疑有误),参数使用逗号「,」分割,并且每个参数都必须明确指定类型:
fun func(arg1: Int, arg2: Int): Int { /*...*/ } 复制代码
在某些场景下,你可以使用一个参数占一行且末尾跟上一个逗号的形式来声明函数参数:
fun func( arg1: Int, arg2: Int, // 这里可以放置一个逗号,方便以后新增参数 ) { /*...*/ } 复制代码
默认参数
函数参数可以指定一个默认值,这种参数叫做“默认参数”在使用的时候你可以跳过对应参数不需要给它们赋值,它们会使用默认参数。这种机制使得我们在仅定义一个带有默认参数的情况下获得一定程度的函数重载能力,因为你不需要给所有的参数传值了。
给一个参数指定一个默认值,需要使用运算符「=」,等号左边是是参数的类型,右边则紧跟默认值:
fun plus( a: Int, b: Int = 2, // b参数分配了一个默认值,在调用该函数的时候若不传递该参数值将使用默认值2 ) { /*..*/ } 复制代码
覆写方法时,不允许指定参数默认值,覆写方法中的参数会继承被覆写方法的参数的默认值:
open class A { open fun foo(i: Int = 10) { /*...*/ } } class B : A() { override fun foo(i: Int /* 无法重新指定i的默认值了,它会继承A#foo方法中i参数的默认值 */) { /*...*/ } } 复制代码
如果默认参数出现在非默认参数的前面,要跳过默认参数,则只能使用“具名参数”:
fun foo( bar: Int = 0, baz: Int, ) { /*...*/ } foo(baz = 1) // 给baz传递参数值,bar使用默认参数值0 复制代码
如果默认参数最后的参数接收一个lambda,可以通过具名参数来传递参数值,或者将lambda体放置到函数的小括号外面(注意这种情况需要lambda参数是最后一个参数):
fun foo( bar: Int = 0, baz: Int = 1, func: () -> Unit, ) { /*...*/ } foo(1) { println("hello") } // 给bar传值,baz使用默认值 foo(func = { println("hello") }) // bar和baz都使用默认值 foo { println("hello") } // bar和baz都使用默认值 复制代码
具名参数
当调用一个函数时,可以给一个或多个参数指定参数名的方式传值。当一个函数有非常多的参数时,我们很难知道传递的值究竟对应哪个参数,尤其是它们的值都是boolean
或者null
类型时。
当你在调用函数时使用具名参数,你可以自由的改变参数的顺序,且当你想使用它们的默认值时,只需要简单的跳过该参数即可。
假设有一个函数,其中有四个默认参数:
fun func( str: String, bool1: Boolean = true, bool2: Boolean = true, bool3: Boolean = false, char: Char = ' ', ) { /*...*/ } 复制代码
要调用该函数,你无需给所有的参数命名:
func( "Hello", false, bool2 = false, bool3 = true, '_' ) 复制代码
你可以跳过所有带默认值的参数:
func("这是一个字符串") 复制代码
也可以跳过带有默认值的特定参数,而不是所有的默认参数。然而,当你跳过一个参数后,要给后续的参数传值,都必须使用具名参数:
func("这是一个字符串", bool3 = false, char = '_') 复制代码
你可以使用扩展运算符「*」给一个具名的可变数量参数(vararg
)传值:
fun foo(vararg strings: String) { /*...*/ } foo(strings = *arrayOf("a", "b", "c")) // 扩展运算符将数组展开成一个个字符串传递给string 复制代码
注意:不能对Java方法使用具名参数语法,因为Java字节码不总是保留函数参数的名称。
返回Unit的函数
如果一个函数不返回一个有效的返回值,那么它的返回值类型就是Unit。
Kotlin函数总是有返回值类型的,即使执行的是一个没有返回值的操作,比如:println("hello")
输出一段信息,此时函数的返回值类型是Unit
,它等同于Java中的void
声明:
fun greeting(name: String?): Unit { if (name != null) println("Hello $name") else println("Hi there!") // 这里可以使用`return Unit`返回,或者直接忽略 } 复制代码
如果一个函数的返回值类型是Unit
,在声明函数时可以忽略返回值类型,上面的代码等同于:
fun greeting(name: String?) /* 这里不需要声明 Unit 了 */ { ... } 复制代码
单表达式函数
当函数返回一个单一表达式(注意:if-else
,when
,try-catch-finally
也是表达式)时,大括号可以被等号「=」代替,要返回的表达式则直接放置在等号右边(无需使用return
关键字):
fun plus(a: Int, b: Int): Int = a + b 复制代码
同时等号右边的表达式的返回值类型可以被推断,所以函数的返回值类型声明可省。如下的函数定义都是合法的:
fun plus(a: Int, b: Int) = a + b fun div(m: Int, d: Int) = try { m / d } catch (e: Exception) { -1 } fun gtZero(n: Int) = n > 0 fun isKtFile(filename: String) = if (filename.endsWith(".kt")) println("Kotlin file") else println("Other file") 复制代码
显式声明返回类型
使用大括号作为函数体时,必须显式声明函数的返回值类型,否则该函数的返回值类型就是Unit
,即无任何有效返回。当你明确知道你的函数不需要返回值时(Unit
),你可以不指定返回值。
注意:当一个函数使用大括号「{}」声明函数体时,必须要显式指定返回值类型,除非你希望它的返回值类型是Unit
。因为这样的函数往往有复杂的控制流,导致编译器无法准确推断返回值的类型。
可变数量参数
你可以使用vararg
标识符来标记一个函数的参数为可变数量的参数(对应Java中的变长参数),这种参数只有一个参数名,但是可以通过逗号「,」分割的形式传递多个参数值(通常是参数列表的最后一个参数):
fun <T> asList(vararg ts: T): List<T> { val result = ArrayList<T>() for (t in ts) // ts 实际上是一个数组 result.add(t) return result } 复制代码
可以使用如下形式给该参数传值:
val list = asList(1, 2, 3) 复制代码
在函数内部,vararg
参数ts
是作为一个T
类型元素的数组对象,它的类型是Array<out T>
。
一个函数中仅能有一个参数被标记为vararg
,如果vararg
参数不是作为函数参数列表的最后一个参数存在,则需要使用具名参数的形式给所有参数传值,因此最好是将vararg
参数放至最后。如果参数类型是函数类型,可以在圆括号外传递lambda。
对于vararg
函数,假如我们已经有一个arrayOf(1, 2, 3)
的数组,要把其中的元素传递给vararg
函数,可以在数组对象前面使用扩展运算符「*」展开数组中的元素(也就是把数组中的元素一个个取出来传递给vararg
):
val a = arrayOf(1, 2, 3) val list = asList(-1, 0, *a, 4) 复制代码
如果你想传递一个原始类型的数组给vararg
,需要使用toTypedArray()
函数转化之后再操作:
val a = intArrayOf(1, 2, 3) // IntArray 是原始类型的数组 val list = asList(-1, 0, *a.toTypedArray(), 4) 复制代码
函数的中缀表述
使用infix
关键字标记的函数可以使用中缀形式调用(忽略点运算符「.」及函数调用时的括号「()」),但是定义中缀函数前必须要满足如下条件:
必须是成员函数或扩展函数。
必须仅有一个参数。
参数不能是
vararg
且不能有默认值。
以下是中缀函数的定义和用法:
infix fun Int.shl(x: Int): Int { ... } // 使用中缀形式调用函数 1 shl 2 // 同样标准形式的函数调用也是支持的 1.shl(2) 复制代码
中缀函数的调用优先级要低于一些算术运算符,类型匹配和rangeTo
操作符,如:
1 shl 2 + 3
等同于1 shl (2 + 3)
0 until n * 2
等同于0 until (n * 2)
xs union ys as Set<*>
等同于xs union (ys as Set<*>)
但另一方面,中缀函数的优先级要高于布尔运算符「&&」和「||」,is
和in
检查,以及其它一些操作符,如:
a && b xor c
等同于a && (b xor c)
a xor b in c
等同于(a xor b) in c
注意中缀函数必须要指定receiver及参数。当你使用中缀形式在当前receiver中调用一个方法时,必须显式使用this
:
class MyStringCollection { infix fun add(s: String) { /*...*/ } fun build() { this add "abc" // 正确 中缀形式 add("abc") // 正确 通用形式 //add "abc" // 错误 中缀形式,但是没有指定receiver } } 复制代码
注:receiver可以理解为Java中的对象,即函数的持有者。
函数可定义范围
Kotlin的函数可以被定义在文件层级(称为顶级函数),即你不需要为此专门创建一个类去持有它,而Java中必须由类去持有方法(注意这里使用方法「method」称谓,Java中没有函数「Function」这一概念,要注意函数≠方法)。除了顶级函数,还可以声明:本地函数,成员函数和扩展函数。
本地函数
Kotlin支持本地函数,即声明在一个函数中的函数:
fun foo(x: Int) { val v = 3 // 本地变量(注意kotlin中val声明的也是变量,还不能称为常量) fun foo(x: Int) { // 这里函数名可以跟外部函数不一样 println("本地变量: $v") // 访问本地变量 foo(v) // 当本地函数名与外部函数一样时,调用的是本地函数本身 } foo(v) // 外部函数调用本地函数 } 复制代码
可以看到,本地函数的方法签名甚至可以和外部函数一样(函数名,参数列表跟函数返回值类型),还可以访问外部函数定义的变量(称为本地变量)。
需要注意的是,外部函数和本地函数之间是可以互调的,前提是函数名或参数列表不同:
fun foo(x: Int) { val v = 3 fun foo(x: String) { println("本地变量: $v") // 访问本地变量 println("本地函数入参:$x") foo(v) // 调用外部函数 } foo("Hello") // 调用本地函数 } 复制代码
但是请特别注意:不要像上面那样滥用本地函数,否则很容易导致栈溢出!
成员函数
成员函数就是定义在类(class
修饰)或单例对象(object
修饰)中的函数:
class Foo { fun bar() { print("bar") } } 复制代码
成员函数使用点运算符「.」调用:
Foo().bar() // 创建一个 Foo 实例并调用其成员函数 bar 复制代码
泛型函数
函数可以有泛型参数,可以在函数名前面使用尖括号「<>」指定:
fun <T> singletonList(item: T): List<T> { /*...*/ } 复制代码
尾调函数
Kotlin支持尾调函数「tail recursive functions」(或者说尾递归函数)。函数的递归调用是一种非常有用的机制,但是受限于栈空间的大小,当函数调用层级过深时很容易导致栈溢出「Stack Overflow」。
那么有没有一种机制让我们安全的使用递归呢?有!Kotlin支持对尾调函数进行优化,在获得更加高效性能的同时,我们也无需担心栈溢出。
要使一个函数获得尾调优化,需要使用tailrec
修饰符来修饰:
tailrec fun tailRecFunc(v: Long): Long = if (v <= 0) v else tailRecFunc(v - 1) 复制代码
我们来对比一下,普通的递归函数与使用tailrec
修饰的(尾调)函数使用时会发生什么:
可以看到,上述定义的两个函数都被IDEA识别到有函数的递归调用,但是结果出乎意料,编译器做了尾调优化(使用tailrec
标识)的函数tailRecFunc
成功执行完毕并返回了预期结果,而另一个函数recFunc
仅仅因为少了tailrec
标识而产生了栈溢出错误。
要知道原因我们需要对Kotlin生成的字节码进行反编译:
原来对于tailrec
函数,编译器已经将它转化为使用while
循环实现了,既然已不存在函数的递归调用,自然不会产生栈溢出的错误。
但是需要注意了,tailrec
标识符不是万金油,你需要保证函数对于自身的调用是最后的执行的操作,如果函数对自身的调用被包裹在try-catch-finally
块中,或者它是一个open
函数,则不执行尾调优化:
上述代码反编译后的字节码依然存在函数的递归调用,即便加了tailrec
修饰:
对于open
标识的函数:
open class Foo { open fun recFunc(v: Long): Long = if (v <= 0) v else recFunc(v - 1) } 复制代码
同样无法得到优化,依然存在递归调用:
结语
这是我的第一篇关于Kotlin的文章,为什么从函数讲起?因为函数(在一些语言中可能叫方法,我们可以不严谨的把二者同等看待就行)是程序执行的入口,小到每一门语言入门时的hello world,大到游戏和APP都是如此,而恰恰在Kotlin中编写一个”Hello world!“你只需要这样:
fun main() { println("Hello World!") } 复制代码
没错!你只需要新建一个kt类型文件,把上述代码C/V就可以了,甚至都不需要class
!
本文基于目前最新的Kotlin 1.6.10版本,覆盖了基本的函数用法,一些关于函数的高级内容,比如:
内联函数「Inline functions」
扩展函数「Extension functions」
高阶函数和lambda等「Higher-order functions and lambdas」
我会在其它文章中进行补充,本文参考了Kotlin官方指引(很遗憾没有中文),若你对自己的英文水平比较自信,亦可参照官网函数教程学习。
因本人水平有限,如行文有错误和纰漏,欢迎指正:)!
作者:麦穗星
链接:https://juejin.cn/post/7045163613681614884
玩站网免费分享SEO网站优化 技术及文章 伪原创工具 https://www.237it.com/