其实函数我们在一开始就在使用了:
fun main() {
println("Hello World")
}
我们程序的入口点就是 main
函数,我们只需要将我们的程序代码编写到主函数中就可以运行了,不过这个函数只是由我们来定义,而不是我们自己来调用。当然,除了主函数之外,我们一直在使用的 println
也是一个函数,不过这个函数是标准库中已经实现好了的,现在是我们在调用这个函数:
println("Hello World!"); //直接通过 函数名称(参数...) 的形式调用函数
那么,函数的具体定义是什么呢?
函数是完成特定任务的独立程序代码单元。
其实简单来说,函数是为了完成某件任务而生的,可能我们要完成某个任务并不是一行代码就可以搞定的,但是现在可能会遇到这种情况:
fun main() {
var a = 10
println("H") //比如下面这三行代码就是我们要做的任务
println("A")
a += 10
if (a > 20) {
println("H") //这里我们还需要执行这个任务
println("A")
a += 10
}
when (a) {
30 -> {
println("H") //这里又要执行这个任务
println("A")
a += 10
}
}
}
我们每次要做这个任务时,都要完完整整地将任务的每一行代码都写下来,如果我们的程序中多处都需要执行这个任务,每个地方都完整地写一遍,实在是太臃肿了,有没有一种更好的办法能优化我们的代码呢?
这时我们就可以考虑使用函数了,我们可以将我们的程序逻辑代码全部编写到函数中,当我们执行函数时,实际上执行的就是函数中的全部内容,也就是按照我们制定的规则执行对应的任务,每次需要做这个任务时,只需要调用函数即可。
我们来看看,如何创建和使用函数。
创建和使用函数
Kotlin函数使用 fun
关键字声明:
fun 函数名称([函数参数...]): 返回值类型 {
//函数体
}
其中函数名称也是有要求的,并不是所有的字符都可以用作函数名称,它的命名规则与变量的命名规则基本一致,所以这里就不一一列出了。函数不仅仅需要完成我们的任务,可能某些函数还需要告诉我们结果,我们同样可以将函数返回的结果赋值给变量或是参与运算等等,当然如果我们的函数只需要完成任务,不需要告诉我们结果,返回值类型可以不填,我们先从最简单的开始:
//这个函数用于打印一段文本
fun hello(): Unit { //本质上应该是返回Unit类型,这个类型表示空,类似于Java中的void,默认情况下可以省略
println("PHP是世界上最好的语言.kt")
}
我们要调用这个函数也很简单,只需要像下面这样就可以了:
fun main() {
hello() //调用函数只需使用 函数名() 即可
}
不过,有些时候,我们可能需要外部传入一些参数来使用,比如:
fun say(message: String){ //在定义函数时,可以将参数写到
println("我说:$message")
}
这里我们在函数的小括号中填入的就是形式参数,这代表调用函数时需要传入的数据,比如这里就是我们要打印的字符串,而实际在调用函数时,填入的内容就是实际参数:
fun main() {
//在调用带参数的函数时,必须填写实参,否则无法编译通过
//这里填入的内容就是实际参数
say("你干嘛")
//也可以将变量作为实际参数传入
val str: String = "哎哟"
say(str)
}
还有一些时候,我们的函数可能需要返回一个计算的结果给调用者,我们也可以设定函数的返回值:
//这个函数用于计算两个Int数之和
fun sum(a: Int, b: Int) : Int {
return a + b //使用return语句将结果返回
}
带返回值的函数,调用之后得到的返回值,可以由变量接收,或是直接作为其他函数的参数:
fun main() {
var result = sum(1, 2) //获取函数返回值
println(result)
println(sum(2, 4)) //直接打印函数返回值
}
注意这个 return
关键字在执行之后,是不会继续执行之后的内容的:
fun main() {
println(test(-2))
println(test(10))
}
fun test(i: Int): String{
if(i > 0) return "Hello"
println("继续")
return "World" //如果满足上面条件,在执行return之后,后续无论有没有执行完,都不会再往下了
}
有些时候,我们也可以设计一些参数带有默认值的函数,如果在调用函数时不填入参数,那么就使用我们一开始设置好的默认值作为实际传入的参数:
fun main() {
test() //调用函数时,如果对应参数有默认值,可以不填
}
fun test(text: String = "我是默认值"){
println(text)
}
在调用函数时,我们可以手动指定传入的参数对应的是哪一个形式参数:
fun main() {
test(b = 3) //这里如果只想填写第二个参数b,我们可以直接指定吧实参给到哪一个形参
test(3) //这种情况就是只填入第一个实参
}
fun test(a: Int = 6, b: Int = 10): Int {
return a + b
}
对于一些内容比较简单的函数,比如上面仅仅是计算两个参数的和,我们可以直接省略掉花括号,像这样编写:
fun test(a: Int = 6, b: Int = 10): Int = a + b //函数的结果直接缩减为 = a + b 效果跟之前是一样的
fun test(a: Int = 6, b: Int = 10) = a + b //返回类型可以自动推断,这里可以吧返回类型省掉
这里还需要注意一下,函数的形式参数默认情况下为常量,无法进行修改,只能使用:
比较奇葩的是,函数内部也可以定义函数:
fun outer(){
fun inner(){
//函数内部定义的函数,无限套娃
}
}
函数内的函数作用域是受限的,我们只能在函数内部使用:
fun outer(){
fun inner(){
}
inner()
}
内部函数可以访问外部函数中的变量:
fun outer(){
val a = 10;
fun inner(){
println(a)
}
}
最后,我们不能同时编写多个同名函数,这会导致冲突:
但是,如果多个同名函数的参数不一致,是允许的:
fun test() = println("A")
fun test(str: String) = println("B") //参数列表不一致
我们在调用这个函数时,编译器会根据我们传入的实参自动匹配使用的函数是哪一个:
...
fun main() {
test("") //结果为B
}
以上适用于形参列表不同的情况,如果仅仅是返回值类型不同的情况,同样是不允许的:
像这种编写同名但不同参数的函数,我们称为函数的重载。
再谈变量
前面我们学习了如何使用变量,只不过当时我们仅仅是在main函数中使用的局部变量,我们也可以将变量的作用域进行提升,将其直接变成一个顶级定义:
var str: String = "尊嘟假嘟" //跟定义函数一样,直接写在Kt文件中
fun main() {
...
}
此时,这个变量可以被所有的函数使用:
var str: String = "尊嘟假嘟"
fun main() = println(str) //作用域的提升,使得变量可以被随意使用
fun test() = println(str)
以上也只是对变量的一些简单使用,现在变量的作用域被提升到顶层,它可以具有更多的一些特性,那么,我们就再来重新认识一下变量,声明一个变量的完整语法如下:
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]
前面的我们知道,但是这个getter和setter是个什么鬼?对于这种顶层定义的变量(包括后面类中会用到的成员属性变量)可以具这两个可选的函数,它们本质上是一个get和set函数:
- getter:用于获取这个变量的值,默认情况下直接返回当前这个变量的值
- setter:用于修改这个变量的值,默认情况下直接对这个变量的值进行修改
我们在使用这种全局变量时,对于变量的获取和设定,本质上都是通过其getter和setter函数来完成的,只不过默认情况下不需要我们去编写,程序编译之后,有点像这样的结果:
var name: String = "小明"
fun getName() : String { //编译时自动生成了对应变量的get函数
return this.name
}
fun setName(name: String) { //编译时自动生成了set函数
this.name = name;
}
而对于其使用,在编译之后,会变成这样:
fun main() {
println(getName()) //获取name时本质上是调用getName函数
}
是不是感觉好神奇,一个变量都能搞这么多花样,这其实是为了后续多态的一些性质而设计的(下一章讲解)
可以看到,在默认情况下,变量的获取就是直接返回,设置就是直接修改,不过有些时候我们可能希望修改这些变量获取或修改时执行的操作,我们可以手动编写:
var str: String = "尊嘟假嘟"
get() = field + field //使用filed代表当前这个变量(字段)的值,这里返回值拼接的结果
这里使用的field准确的说应该是Kotlin提供的"后备字段",因为我们使用getter和setter本质上替代了原有的获取和修改方式,使其变得更像是函数的调用,因此,为了能够继续像之前使用一个变量那样去操作它本身,就有了这个后备字段。
最后得到的就是:
甚至还可以写成这样,在获取的时候执行一些操作:
var str: String = "尊嘟假嘟"
get() {
println("获取变量的值:") //获取的时候打印一段文本
return field + "666"
}
fun main() = println(str)
同样的,设置的时候也可以自定义:
var str: String = "尊嘟假嘟"
get() = field + field
set(value) { //这里的value就是给过来的值
println("设置变量的值")
field = value //注意,对于val类型的变量,没有set函数,因为不可变
}
因此,一个变量有些时候可能会写成这样:
val str get() = "你干嘛"
当然,默认情况下其实没有必要去重写get和set除非特殊需求。
递归函数
我们前面学习了如何调用函数,实际上函数自己也可以调用自己。
fun test(){
test() //我自己调用自己
}
肯定会有小伙伴疑问,函数自己调用自己有什么意义?反而还会导致函数无限的调用下去,无穷无尽,确实,如果不加限制地让函数自己调用自己:
就会出现这种 爆栈
的情况,这是因为程序的内存是有限的,不可能无限制的继续调用下去,因此,在自我调用到一定的深度时,会被强制终止。所以说这玩意有啥用呢?如果我们对递归函数加以一些限制,或许会有意想不到的发现:
fun main() {
test(5) //计算0-5的和
}
//这个函数实现了计算0-n的和的功能
fun test(n: Int): Int{
if(n <= 0) return 0 //当n等于0的时候就不再向下,而是直接返回0
return n + test(n - 1) //n不为0就返回当前的n加上test参数n-1的和
}
这个函数最终调用起来就像这样:
test(5) = 5 + test(4) = 5 + 4 + test(3) = ... = 5 + 4 + 3 + 2 + 1 + 0
可以看到,只要合理使用递归函数,加以一定的结束条件,反而能够让我们以非常简洁的形式实现一个需要循环来完成的操作。
我们可以再来看一个案例:
斐波那契数列是一个非常经典的数列,它的定义是:前两个数是1和1,之后的每个数都是前两个数的和。
斐波那契数列的前几个数字依次是:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...
对于求解斐波那契数列第N个数这类问题,我们也可以使用递归来实现:
fun main() {
println(fib(5))
}
fun fib(n: Int): Int{
if(n <= 2) return 1 //我们知道前两个一定是1,所以直接返回
return fib(n - 1) + fib(n - 2) //当前fib(n)的结果就是前两个结果之和,直接递归继续找
}
是不是感觉递归函数非常神奇?它甚至可以解决一些动态规划问题、一些分治算法等。
不过,这种函数的效率就非常低了,相比循环来说,使用递归解决斐波那契问题,时间复杂度会呈指数倍增长,且n大于20时基本可以说很卡了(可以想象一下,每一个fib(n)都会分两个出去,实际上这个中间存在大量重复的计算)
那么,有没有办法可以将这种尾部作为返回值进行递归的操作优化一下呢?我们可以使用 tailrec
关键字来实现:
tailrec fun test(n: Int, sum: Int = 0): Int {
if(n <= 0) return sum //到底时返回累加的结果
return test(n - 1, sum + n) //不断累加
}
实际上在编译之后,会变成这样:
可以看到它变成了一个普通的循环操作,这也是编译器的功劳,同样的,对于斐波那契数列:
tailrec fun fib(n: Int, prev: Int = 0, next: Int = 1): Int {
return if (n == 0) prev else fib(n - 1, next, prev + next) //从0和1开始不断向后,直到n为0就返回
}
实用库函数介绍
Kotlin为我们内置了大量实用的库函数,我们可以使用这些库函数来快速完成某些操作。
比如我们前面使用的 println
就是Kotlin提供的库函数,我们可以使用这个函数快速进行数据打印:
fun main() {
println("Hello World") //这里其实就是在调用函数,传入了一个String类型的参数
}
那既然现在有输出,能不能让用户输入,然后我们来读取呢?
fun main() {
val text = readln()
println("读取到用户输入:$text")
}
我们可以在控制台输入一段文本,然后回车结束:
Kotlin提供的运算符实际上只能进行一些在小学数学中出现的运算,但是如果我们想要进行乘方、三角函数之类的高级运算,就没有对应的运算符能够做到,而此时我们就可以使用数学工具类来完成。
import kotlin.math.* //我们需要使用import来引入某些库,这样才能使用库函数
fun main() {
1.0.pow(4.0) //我们可以使用pow方法直接计算a的b次方
abs(-1); //abs方法可以求绝对值
max(19, 20); //快速取两个数的最大值
min(2, 4); //快速取最小值
sqrt(9.0); //求一个数的算术平方根
}
当然,三角函数肯定也是安排上了的:
fun main() {
//这里我们可以直接使用库中预设好的PI
sin(PI / 2); //求π/2的正弦值,这里我们可以使用预置的PI进行计算
cos(PI); //求π的余弦值
tan(PI / 4); //求π/4的正切值
asin(1.0); //三角函数的反函数也是有的,这里是求arcsin1的值
acos(1.0);
atan(0.0);
}
可能在某些情况下,计算出来的浮点数会得到一个很奇怪的结果:
fun main() {
println(sin(Math.PI));
}
正常来说,sinπ的结果应该是0才对,为什么这里得到的是一个很奇怪的数?这个E是干嘛的,这其实是科学计数法的10,后面的数就是指数,上面的结果其实就是:
- 1.2246467991473532×10−161.2246467991473532×10−16
其实这个数是非常接近于0,这是因为精度问题导致的,所以说实际上结果就是0。
我们也可以计算对数函数:
fun main() {
ln(E) //e为底的对数函数,其实就是ln,我们可以直接使用Math中定义好的e
log10(100.0) //10为底的对数函数
log2(8.0) //2为底的对数函数
//利用换底公式,我们可以弄出来任何我们想求的对数函数
val a = ln(4.0) / ln(2.0) //这里是求以2为底4的对数,log(2)4 = ln4 / ln2
println(a)
}
还有一些比较特殊的计算:
fun main() {
ceil(4.5) //通过使用ceil来向上取整
floor(5.6) //通过使用floor来向下取整
}
向上取整就是找一个大于当前数字的最小整数,向下取整就是砍掉小数部分。注意,如果是负数的话,向上取整就是去掉小数部分,向下取整就是找一个小于当前数字的最大整数。
高阶函数与lambda表达式
注意: 这一部分比较难理解,如果看不懂可以后面回来看。
Kotlin中的函数属于一等公民,它支持很多高级特性,甚至可以被存储在变量中,可以作为参数传递给其他高阶函数并从中返回,就想使用普通变量一样。 为了实现这一特性,Kotlin作为一种静态类型的编程语言,使用了一系列函数类型来表示函数,并提供了一套特殊的语言结构,例如lambda表达式。
那么这里说的高阶函数是什么,lambda表达式又是什么呢?
正是得益于函数可以作为变量的值进行存储,因此,如果一个函数接收另一个函数作为参数,或者返回值的类型就是一个函数,那么该函数称为高阶函数。
要声明函数类型,需要按照以下规则:
- 所有函数类型都有一个括号,并在括号中填写参数类型列表和一个返回类型,比如:
(A, B) -> C
表示一个函数类型,该类型表示接受类型A
和B
的两个参数并返回类型C
的值的函数。参数类型列表可为空的,比如() -> A
,注意,即使是Unit
返回类型也不能省略。
我们可以像下面这样编写:
//典型的函数类型 (参数...) -> 类型 小括号中间是一个剪头一样的符号,然后最后是返回类型
var func0: (Int) -> Unit //这里的 (Int) -> Unit 表示这个变量存储的是一个有一个int参数并且没有返回值的函数
var func1: (Double, Double) -> String //同理,代表两个Double参数返回String类型的函数
同样的,作为函数的参数也可以像这样表示:
fun test(other: (Int) -> String){
}
函数类型的变量,我们可以将其当做一个普通的函数进行调用:
fun test(other: (Int) -> String){
println(other(1)) //这里提供的函数接受一个Int参数返回string,那么我们可以像普通函数一样传入参数调用它
}
由于函数可以接受函数作为参数,所以说你看到这样的套娃场景也不奇怪:
var func: (Int) -> ((String) -> Double)
不过这样写可能有些时候不太优雅,我们可以为类型起别名来缩短名称:
typealias HelloWorld = (String) -> Double
fun main() {
var func: HelloWorld
}
那么,函数类型我们知道如何表示了,如何具体表示一个函数呢?我们前面都是通过 fun
来声明函数:
fun test(str: String): Int {
return 666
}
而现在我们的变量也可以直接表示这个函数:
fun main() {
//这个变量表示的也是(String) -> Int这种类型的函数
var func: (String) -> Int = ::test //使用双冒号来引用一个现成的函数(包括我们后续会学习的成员函数、构造函数等)
}
//这个函数正好与上面的变量表示的函数类型一致
fun test(str: String): Int {
return 666
}
除了引用现成的函数之外,我们也可以使用匿名函数,这是一种没有名称的函数:
fun main() {
val func: (String) -> Int = fun(str: String): Int { //这里写了fun关键字后,并没有编写函数名称,这种函数就是匿名函数,因为在这里也不需要什么名字,只需要参数列表函数体
println("这是传入的内容$str")
return 666
}
}
匿名函数除了没名字之外,其他的用法跟函数是一样的。
最后,我们来看看今天的重量级嘉宾,不要小看了Kotlin的语法,我们也可以使用Lambda表达式来表示一个函数实例:
fun main() {
var func: (String) -> Int = { //一个Lambda表达式只需要直接在花括号中编写函数体即可
println("这是传入的参数$it") //默认情况下,如果函数只有一个参数,我们可以使用it代表传入的参数
666 //跟之前的if表达式一样,默认最后一行为返回值
}
func("HelloWorld!")
}
是不是感觉特别简便?
对于参数有多个的情况,我们也可以这样进行编写:
fun main() {
val func: (String, String) -> Unit = { a, b -> //我们需要手动添加两个参数这里的形参名称,不然没法用他两
println("这是传入的参数$a, 第二个参数$b") //直接使用上面的形参即可
}
val func2: (String, String) -> Unit = { _, b ->
println("这是传入的第二个参数$b") //假如这里不使用第一个参数,也可以使用_下划线来表示不使用
}
func("Hello", "World")
}
是不是感觉玩的非常高级?还有更高级的在后面呢!
我们接着来看,如果我们现在想要调用一个高阶函数,最直接的方式就是下面这样:
fun main() {
val func: (Int) -> String = { "收到的参数为$it" }
test(func)
}
fun test(func: (Int) -> String) {
println(func(66))
}
当然我们也可以直接把一个Lambda作为参数传入作为实际参数使用:
fun main() {
test({ "收到的参数为$it" })
}
不过这样还不够简洁,在Kotlin中,如果函数的最后一个形式参数是一个函数类型,可以直接写在括号后面,就像下面这样:
test() { "收到的参数为$it" }
由于小括号里面此时没有其他参数了,还能继续省,直接把小括号也给干掉:
test { "收到的参数为$it" } //干脆连小括号都省了,这语法真的绝
当然,如果在这之前有其他的参数,只能写成这样了:
fun main() {
test(1) { "收到的参数为$it" }
}
//这里两个参数,前面还有一个int类型参数,但是同样的最后一个参数是函数类型
fun test(i: Int, func: (Int) -> String) {
println(func(66))
}
这种语法也被称为 尾随lambda表达式,能省的东西都省了,不过只有在最后一个参数是函数类型的情况下才可以,如果不是最后一位,就没办法做到尾随了。
最后需要特别注意的是,在Lambda中没有办法直接使用 return
语句返回结果,而是需要用到之前我们学习流程控制时用到的标签:
fun main() {
val func: (Int) -> String = test@{
//比如这里判断到it大于10就提前返回结果
if(it > 10) return@test "我是提前返回的结果"
println("我是正常情况")
"收到的参数为$it"
}
test(func)
}
fun test(func: (Int) -> String) {
println(func(66))
}
如果是函数调用的尾随lambda表达式,默认的标签名字就是函数的名字:
fun main() {
testName { //默认使用函数名称
if(it > 10) return@testName "我是提前返回的结果"
println("我是正常情况")
"收到的参数为$it"
}
}
fun testName(func: (Int) -> String) {
println(func(66))
}
不过,为什么要这么麻烦呢,还要打标签才能返回,这不多此一举么?这个问题我们会在下一节内联函数中进行讲解。
内联函数
使用高阶函数会可能会影响运行时的性能:每个函数都是一个对象,而且函数内可以访问一些局部变量,但是这可能会在内存分配(用于函数对象和类)和虚拟调用时造成额外开销。
为了优化性能,开销可以通过内联Lambda表达式来消除。使用 inline
关键字会影响函数本身和传递给它的lambdas,它能够让方法的调用在编译时,直接替换为方法的执行代码,什么意思呢?比如下面这段代码:
fun main() {
test()
}
//添加inline表示内联函数
inline fun test(){
println("这是一个内联函数")
println("这是一个内联函数")
println("这是一个内联函数")
}
由于test函数是内联函数,在编译之后,会原封不动地把代码搬过去:
fun main() {
println("这是一个内联函数") //这里是test函数第一行,直接搬过来
println("这是一个内联函数")
println("这是一个内联函数")
}
同样的,如果是一个高阶函数,效果那就更好了:
fun main() {
test { println("打印:$it") }
}
//添加inline表示内联函数
inline fun test(func: (String) -> Unit){
println("这是一个内联函数")
func("HelloWorld")
}
由于test函数是内联的高阶函数,在编译之后,不仅会原封不动地把代码搬过去,还会自动将传入的函数参数贴到调用的位置:
fun main() {
println("这是一个内联函数") //这里是test函数第一行
val it = "HelloWorld" //这里是函数内传入的参数
println("打印:$it") //第二行是调用传入的函数,自动贴过来
}
内联会导致编译出来的代码变多,但是同样的换来了性能上的提升,不过这种操作仅对于高阶函数有显著效果,普通函数实际上完全没有内联的必要,也提升不了多少性能。
注意,内联函数中的函数形参,无法作为值给到变量,只能调用:
同样的,由于内联,导致代码被直接搬运,所以Lambda中的return语句可以不带标签,这种情况会导致直接返回:
fun main() {
test { return } //内联高阶函数的Lambda参数可以直接写return不指定标签
println("调用上面方法之后")
}
inline fun test(func: (String) -> Unit){
func("HelloWorld")
println("调用内联函数之后")
}
上述代码的运行结果就是,直接结束,两句println都不会打印,这种情况被称为非局部返回。
回到上一节最后我们提出的问题,实际上,在Kotlin中Lambda表达式支持一个叫做"标签返回"(labeled return)的特性,这使得你能够从一个Lambda表达式中返回一个值给外围函数,而不是简单地返回给Lambda表达式所在的最近的封闭函数,就像下面这样:
fun main() {
test { return@main } //标签可以直接指定为外层函数名称main来提前终止整个外部函数
println("调用上面方法之后")
}
inline fun test(func: (String) -> Unit){
func("HelloWorld")
println("调用内联函数之后")
}
效果跟上面是完全一样的,为了避免这种情况,我们也可以像之前一样将标签写为@test来防止非局部返回。
fun main() {
test { return@test } //这样就只会使test返回,而不会影响到外部函数了
println("调用上面方法之后")
}
有些时候,可能一个内联的高阶函数中存在好几个函数参数,但是我们希望其中的某一个函数参数不使用内联,能够跟之前一样随意当做变量使用:
fun main() {
test({ println("我是一号:$it") }, { println("我是二号:$it") })
}
//在不需要内联的函数形参上添加noinline关键字,来防止此函数的调用内联
inline fun test(func: (String) -> Unit, noinline func2: (Int) -> Unit){
println("这是一个内联函数")
func("HelloWorld")
var a = func2 //这样就不会报错,但是不会内联了
func2(666)
}
最后编译出来的结果,类似于:
fun main() {
println("这是一个内联函数")
val it = "HelloWorld"
println("打印:$it")
//第二个参数由于不是内联,这里依然作为Lambda使用
val func2: (Int) -> Unit = { println("我是二号:$it") }
func2(666)
}
由于目前知识的学习还不太够,函数我们只能先暂时告一段落,在后续的学习中我们会继续认识更多函数的特性。
评论区