Kotlin 学习笔记 第 5 篇 函数和 Lambda 表达式

背景

函数其实可以理解成就是 Java 中所说的方法。函数是执行特定任务的一段代码,这段代码可以在需要的时候多次调用。因此,函数时代码复用的重要手段。 Kotlin 对 Java 的纯粹面向对象进行了补充,增加了函数式变成的支持,提高了变成的灵活性。Kotlin 融合了面向过程和面向对象语言的特征,因此 Kotlin 完全支持定义、调用函数。 Kotlin 的函数比 C 语言中的函数要更加强大, Kotlin 支持局部函数(局部函数是 Lambda 表达式的基础),Kotlin 中的函数是一个非常重要的知识点,因此 Kotlin 的函数需要或更多的精力去掌握。
与函数紧密关联的一个知识点是 Lambda 表达式,Lambda 表达式可以作为表达式、函数参数或者函数返回值,使用 Lambda 表达式可以让程序更加简洁。Lambda 表达式相当于简化的匿名函数。
函数 和 Lambda 表达式是 Kotlin 变成的两大核心机制之一,Kotlin 既支持面向过程编程,也支持面向对象编程。函数和 Lambda 表达式是 Kotlin 面向过程编程的语法基。

1. 函数

1.1 定义和调用函数

Kotlin 声明函数必须使用 fun 关键字,语法格式如下:

	fun 函数名(形参列表多个形参以逗号隔开)[: 返回值类型] {// 无返回值的 [] 方括号部分可以省略
		//函数体
	}

如果函数没有返回值,可以将上述语法格式中的方括号部分"[: 返回值类型]"省略掉 ; 或者是 将返回值类型写成“: Unit” ,返回 Unit 代表没有返回值,其实 Unit 就等价于 Java 的 void。

//定义一个函数,功能是获取两个数中的较大的数
fun max(x: Int, y: Int) : Int {//返回值为 Int 类型
	return if(x > y) x else y  
}

fun main(args: Array<String>) {
	var a = 10 
	var b = 11
	var maxNum = max(10,11)
	println("maxNum: ${maxNum}")
	sayHello("小明")
	sayHi("小红")
}
fun sayHello(name: String): Unit{// :Unit 方式 无返回值
	println("${name} , Hello")	
}
fun sayHi(name: String) { //省略方式 无返回值
	println("${name}, Hi")
}

单表达式函数

在某些情况下,函数体只有一行代码,此时可以省略花括号 ,需要加上一个 等于号 “=”,这种方式叫做单行表达式函数 。对于单表达式函数而言,编译器可以推断出函数的返回值类型,因此 Kotlin 允许省略声明函数的返回值类型。例如:

 fun area(x: Double, y: Double): Double = x * y
 fun main(args: Array<String>) {
	println(area(5.0, 4.0))
 }
 // 省略返回值类型 的方式
// fun area(x: Double, y: Double) = x * y

1.2 函数的形参

Kotlin 函数功能比 Java 方法更强大,其中一点就体现在函数的形参上,Kotlin 函数形参功能更加丰富,更加灵活。

1.2.1 命名参数

Kotlin 函数的参数名是有意义的,Kotlin 允许调用函数时通过名字来传入参数值。因此,Kotlin 函数的参数名应该是具有更好的语义,程序可以立刻明确传入函数的每个参数的含义。除了第一个参数,其他所有形参都分配隐式的外部形参名,这些外部形参名与内部形参名保持一致。
实例如下:

//计算矩形的周长
fun perimeter(width: Double , height: Double): Double{
	return 2 * (width + height)
}
fun main(args: Array<String>){
	//传统的传参方式 和 Java 一样
	println(perimeter(5.6, 7.8))
	// 根据参数名传参
	println(perimeter(width = 5.6, height = 7.8))
	// 使用参数名称时 可以不按照形参的顺序传参
	println(perimeter(height = 7.8, width = 5.6))
	// 混合使用 位置参数和命名参数:部分使用命名参数, 部分使用位置参数
	println(perimeter(5.6,height = 7.8)) //命名参数必须位于位置参数之后,如果写成 (width = 5.6 , 7.8)则是错误的
}

1.2.2 形参默认值

有些情况下需要在定义函数时为一个或多个形参指定默认值,这样调用函数时就可以省略该形参,直接使用该形参的默认值。给形参指定默认值的语法格式如下:

形参名: 形参类型 = 默认值

实例:

fun pre(x: Int = 23 , y : Int = 2) {

}
fun main(args: Array<String>){
	pre()
	pre(100)// y 使用默认值
	pre(112,100)//都不适用默认值
	pre(y = 2343)// x 使用默认值
}

1.2.3 个数可变的形参

如果在一个形参的前面加了 vararg 修饰,则说明该形参可以接受多个参数值,多个参数值被当成数组传入,实例如下所示:

fun check(a: Int, vararg b: String) {
	for(item in b) {
		println(item)
	}
	println(a)
}
fun main(args: Array<String>) {
	check(3,"Hi","Hello")
}

Kotlin 允许个数可变的形参可以位于形参列表的任意位置,而不是像 Java 中的那样必须要放在最后一个位置,但是,Kotlin 要求一个函数最多只能有一个可变形参。如果可变形参不在最后位置,则位于其后的参数必须要使用命名参数传入。如果想把一个数组给一个可变形参当做可变参数传入,只需要将数组名前面加上一个“*”就可以了,如下所以:

var arr = arrayOf("Hi","Hello","Oh")
check(20, *arr)

1.3 函数重载

和 Java 类似,Kotlin 中也可以有多个同名不同参的函数,这种同名不同参的多个函数就被成为函数的重载。

1.4 局部函数

前面所有提到的函数是全局范围内定义的,都是全局函数,Kotlin 还支持在函数体内部再定义函数,这种在函数体内再定义的函数就叫做局部函数。在默认情况下,局部函数对外部是隐藏的,局部函数只能在其封闭函数内有效,其封闭函数也可以返回局部函数,以便程序在其他作用域中使用局部函数。

1.5 高阶函数

Kotlin 不是纯粹的面向对象语言,Kotlin 的函数也是一等公民,因此函数本身也具有自己的类型,也就是说函数类型就像数据类型一样既可用于定义变量,也可用作函数的形参类型,还可作为函数的返回值类型。

1.5.1 函数类型

Kotlin 的每个函数其实都有特定的类型,函数类型是由 参数类型列表 加上 箭头“->” 再加上 返回值组成的,具体的格式如下:

(Int, String) -> String   // 这就是函数类型

举例:

fun(age: Int, name: String) -> String {
	...
}

该函数的参数类型列表、-> 和 返回值类型为 (Int , String) -> String, 这就是该函数的类型
我们说过函数类型也是一种数据类型,因此可以声明函数类型的变量,如下:

var myfun : (Int, String) -> String
var ofun : (String) //参数 String 且无返回值的函数

定义了函数类型的变量之后,接下来可以给函数类型的变量赋值为函数,如下所示:

fun test(a: Int, name: String) : String{
	...
	return "我是${name}, 今年 ${a} 岁"
}
//对上面的 myfun 赋值
myfun = ::test
println(myfun(18,"小明"))

fun test2(age: Int, name: String):String{
	return "我是 ${name}, 今年 ${age}"
}
myfun = ::test2
println(myfun(20,"小红"))

上面的代码依次 把 test() 和 test2() 两个函数赋值给了 myfun 函数类型变量 ,程序都是正常运行的,也就是说只要被赋值的函数类型与 myfun 的变量类型一致 ,就可以赋值成功。
当直接访问一个函数的引用,而不是调用函数时,需要在函数名前加上两个冒号,且不能在函数名后加括号,一旦加了括号就变成了函数调用而不是引用了。通过使用函数类型的变量可以让 myfun 在不同的时间指向不同的函数,这样程序变得更加灵活了,也就是说使用函数类型的好处就是能够让程序更加灵活。除此之外,还可以使用函数类型作为形参类型和返回值类型。

1.5.2 函数类型作为形参类型

有些情况,定义一个函数,该函数的大部分逻辑都能确定,但是某些逻辑暂时无法确定,也就是说需要动态的改变,如果希望调用函数时,能够动态的执行某一块代码就需要在函数中定义函数类型的形参,这样就可以在调用该函数是传入不同的函数作为参数,从而实现动态的执行某些代码,因为我们之前说过函数类型也是数据类型,所以可以作为函数的形参。
实例如下:

// 定义函数类型的形参 ,其中 ftest 是 (Int) -> Int  函数类型的形参
fun multip(arr: Array, ftest: (Int) -> Int): Array<Int> { //函数的返回值类型 是 Array<Int>
	var  result = Array<Int>(arr.size, {0})
	for (i in arr.indices) {//遍历 数组
		result[i] = ftest(arr[i]) // 新数组的元素是调用 ftest 返回的结果
	}
	return result // 返回结果
}
//计算平方的函数
fun square(num: Int): Int{
	return num * num
}
// 定义求立方的函数
fun cube(num: Int): Int{
	return num * num * num
}
// 阶乘
fun jiecheng(num: Int) : Int{
	var result  = 1
	for (i in 2..num){
		result *= i
	}
	return result;
}
fun main(args: Array<String>){
	var data = arrayOf(6,9,8,7)
	println(multip(data,::square).contentToString()) //将 square() 函数的引用当做入参
	println(multip(data,::cube).contentToString()) // 入参是 cube() 函数
	println(multip(data,::jiecheng).contentToString()) // 入参是 jiecheng() 函数
}

所以,综上所示,定义了函数类型的形参后,就可以在调用函数时动态的传入指定函数,来实现动态的运行代码。

1.5.3 函数类型作为返回值类型

前面说到函数类型也是一种数据类型,因此也可以被当做返回值,如下所示

// 获取计算方法  该函数会返回一个函数,返回的函数类型 是 (Int) -> Int 函数类型
fun getMathFun(type: String): (Int) -> Int {
	//计算平方的函数
	fun square(num: Int): Int{
		return num * num
	}
	// 定义求立方的函数
	fun cube(num: Int): Int{
		return num * num * num
	}
	// 阶乘
	fun jiecheng(num: Int) : Int{
		var result  = 1
		for (i in 2..num){
			result *= i
		}
		return result;
	}
	// 根据入参type 的值 返回局部函数
	when(type) {
		"square" -> return ::square
		"cube" -> return ::cube
		"jiecheng" -> return ::jiecheng
	}
}
fun main(args: Array<String>){
	var mathFun = getMathFun("cube")// 得到 cube() 函数
	println(mathFun(5))// 得到 125
	mathFun = getMathFun("square") // 得到 square() 函数
	println(mathFun(5)) // 得到 25
	mathFun = getMathFun("jiecheng") // 得到 jiecheng() 函数
	println(mathFun(k5)) // 输出 120
}

2. 局部函数 和 Lambda 表达式

Lambda 表达式是现代编程语言的一种语法,如果说函数是命名的、方便复用的代码块,Lambda 表达式则是功能更灵活的代码块,可以再程序中被传递和调用。
如上面 1.5.3 中的例子可以看出局部函数的作用域默认仅仅停留在其封闭的函数之内,如果不使用 getMathFun() 函数体,局部函数就无法使用了,也就是说这些局部函数也就没什么意义了,因此考虑使用 Lambda 表达式来简化局部函数的写法

2.1 使用 Lambda 表达式代替局部函数

我们将 1.5.3 中的实例采用 Lambda 的方式来实现:

fun getMathFun(type: String) : (Int) -> Int{
	when(type) {
		"square" -> return {num: Int ->
			num * num 
		}
		"cube" -> return {num: Int ->
			num * num * num
		}
		else -> return {num: Int -> 
			var result = 1
			for(i in 2..num){
				result *= i
			}
			result
		}
	}
}
fun main(args: Array<String>){
	var mathFun = getMathFun("cube") //调用方式是一样的
}

从上面的代码我们可以看出,Lambda 表达式与局部函数的代码基本差不多,定义 Lambda 表达式和局部函数的区别主要有如下几点:

  • Lambda 表达式总是被或括号包裹起来
  • 定义 Lambda 不需要 fun 关键字,也不需要指定函数名称
  • 形参列表在 箭头 -> 之前声明,参数类型可以省略
  • 函数体(Lambda 表达式执行体)放在 箭头 -> 之后;
  • 函数体(Lambda 表达式执行体)的最后一个表达式自动被作为 Lambda 表达式的返回值,无需使用 return 关键字。

2.2 Lambda 表达式的脱离

作为函数参数传入的 Lambda 表达式可以脱离函数独立使用。如下所示:

var lambdaList = java.util.ArrayList<(Int) -> Int>()
fun collectFun(fn: (Int) -> Int) {
   //将传入的 fn 参数(函数或者 Lambda 表达式)添加到 lambdaList 集合中,
   //这样的话函数或者 Lambda 表达式就脱离了 collectFun 函数体使用了,
   //可以直接使用 labmdaList 集合进行调用 Lambda 表达式
   lambdaList.add(fn)
}
fun main(args: Arrray<String>){
   collectFun({it * it}) // 向 collectFun  传参,向 lambdaList 添加 Lambda 表达式
   collectFun({it * it * it})
   println(lambdaList.size)
   for(i in lambdaList.indices) {
   	println( lambdaList[i](i + 10))
   }
}

3. Lambda 表达式

上面 提到了 Lambda 表达式,Kotlin 提供了简介的 Lambda 表达式语法,Lambda 表达式的标准与法如下:

{(形参列表) ->
	//执行体
}

Lambda 表达式的本质是功能更灵活的代码块。

3.1 调用 Lambda 表达式

上面提到了 Lambda 其实就是代码块,因此完全可以将 Lambda 表达式赋值给变量或者直接调用 Lambda 表达式。如下所示

//将 Lambda 表达式赋值给一个变量
var square = {num: int ->
	num * num
}
//然后使用 square 变量来调用 Lambda 表达式
println(square(5)) // 输出 25
//定义一个Lambda 表达式,在他后面添加一对小括号可以调用该 Lambda 表达式
var result = {num: Int, maxNum: Int ->
	var resultNum = 1
	for(i in 1 .. maxNum) {
		resultNum *= num
	}
	resultNum
}(5,6) // 这里的 Lambda 表达式并没有赋值给变量,而是被执行的结果值返回给了 result 变量
println(result)

完整的 Lambda 表达式 需要定义形参类型,但是如果可以根据 Lambda 上下文可以推断出形参类型的话,则可以省略形参类型,因此上述的写法可以简写为:

var square :(Int) -> Int = {num -> num * num} //因为限定了 为 (Int) -> Int 类型,也就是说Lambda 表达式的参数也是 Int 类型,所以可以省略
println(square(8))// 使用 square 调用 Lambda 表达式 输出 64
var result = {num: Int, maxNum: Int ->
	var resultNum = 1
	for(i in 1..maxNum) {
		resultNum *= num
	}
	result
}(5,6)// Kotlin 无法推断 该 Lambda 表达式的参数类型,因此必须显示给出

Lambda 表达式除了可以省略形参类型外,当只有一个形参时可以允许省略 Lambda 表达式中的形参名,如果省略了形参 ,箭头要着也没什么意义了,所以干脆都不要了,Lambda 包大师可通过 it 来代表形参,如下所示,更加简洁

var square:(Int)-> Int = {it * it}// 上面的代码只有一个形参可以省略形参名称 使用 it 代替即可

3.1.1 调用 Lambda 表达式的约定

如果函数的最后一个参数是函数类型,且传入一个 Lambda 表达式作为相应的参数,允许在圆括号后指定 Lambda 表达式,这种用法不是 Kotlin 独有,其他语言中这种用法被称作 “尾随闭包”
如下所示:

fun main(args: Array<String>) {
	var testList = listOf("Kotlin","Java","C")
	var newList = testList.dropWhile(){it.length < 2} // 最后一个参数 是 Lambda 表达式,可以写在外面
	println(newList) // ["Kotlin","Java"]
	
}

如果 Lambda 表达式是函数调用的唯一参数,则调用方法时圆括号也可以省略,上面的写法可以简写成如下:

testlist.dropWhile {it.length < 2}

通常建议将函数类型的形参放在形参列表的最后方便以后传入 Lambda 表达式作为参数。

3.1.2 个数可变的参数和 Lambda 参数

Kotlin 约定: 如果调用函数时最后一个参数是 Lambda 表达式,则可将 Lambda 表达式放在圆括号外面,这样就无需使用命名函数了;如果一个函数既包含个数可变的形参,也包含函数类型的形参,那么应该将函数类型的形参放在最后,如下所示:

fun <T> test(vararg names: String, transform: (String) -> T) : List<T> {
	var mutableList: MutableList<T> = mutableListOf()
	for (name in names) {
		mutableList.add(transform(name))
	}
	return mutableList.toList()
}
fun main(args: Array<String>){
	//将 Lambda 表达式放在圆括号后面,无需命名参数
	var lis = test("Kotlin","Java","C"){it.length}
	println(lis)
}

4. 匿名函数

Lambda 表达式虽然简洁、方便,但是它不能指定返回值类型,有些时候 Kotlin 能推断出 Lambda 表达式的返回值类型,因此即使不给 Lambda 表达式指定返回值也没事,但是如果 Kotlin 无法推断出 Lambda 表达式的返回值类型,此时就需要显示指定返回值类型。此时需要使用匿名函数来替代 Lambda 表达式。
匿名函数和普通函数的区别在于,没有函数名,且如果系统可以推断出匿名函数的形参类型,匿名函数允许省略形参类型。

匿名函数的用法

fun main(args: Array<String>) {
	var test = fun(x: Int, y: Int): Int { //将匿名函数赋值给 test 变量
		return x + y
	}
	//通过使用 test 调用匿名函数
	println(test(3,4))
}

5. 内联函数

高阶函数是 参数有函数或者 Lambda 表达式的函数,高阶函数的调用过程即调用( Lambda 表达式或者函数)的过程是: 程序要将执行顺序转移到被调用表达式或函数所在的内存地址,当被调用表达式或函数执行完后,再返回到原函数执行的地方。函数调用会产生一定的时间和空间开销,如果被调用的表达式或函数的代码量本身并不大,而且被调用的频率比较高,那么时间和空间开销的损耗就很不划算。为了避免产生函数调用的过程,可以考虑直接把被调用的表达式或者函数的代码“嵌入”原来的执行流,就是编译器负责去“复制、粘贴”:复制被调用的表达式或函数的代码,粘贴到原来的执行代码中,这种工作可以通过内联函数来实现。

5.1 内联函数的使用

内联函数的使用很简单,只需要将 inline 关键字修饰带函数形参的函数即可,如下所示:

// 用 inline 修饰 带函数形参的函数,test 包含了函数类型的形参, 函数形参 fn 是 (Int) -> Int 类型的函数,所以 test() 是一个内联函数
inline fun test(data: Array<Int>, fn: (Int) -> Int): Array<Int>{
	var result = Array<Int>(data.size,{0})
	for(i in data.indices){
		result[i] = fn(data[i])
	}
	return result
}
fun main(args: Array<String>){
	var arr = arrayOf(34,243,24,54)
	var result = test(arr,{it +3})
	println(result.contentToString())
}

上述编译过程中 只会产生一个 .class 文件,因此可以说明,编译器实际上把 Lambda 表达式的代码复制到了 test() 函数中,也就是说,在调用 test() 函数时,上面的代码其实变成了如下的方式 :

fun test(data: Array<Int>, fn: (Int) -> Int): Array<Int> {
	var result = Array<Int>(data.size,{0})
	for(i in data.indices){
		result[i] = data[i] + 3
	}
	return result
}

也就是说没有存在函数的调用,如果 将 inline 修饰符去掉之后,编译会产生两个 Kt.class 文件,系统为 Lambda 表达式额外程程了一个 函数对象,增加了额外的开销。

5.2 内联函数的优缺点

内联函数的本质是将被调用的 Lambda 表达式或者函数的代码复制、粘贴到原来的执行函数中,因此,如果被调用的 Lambda 表达式或函数的代码量非常之大,且该 Lambda 表达式或函数多次被调用,因为每调用一次,该 Lambda 表达式或函数的代码就会被复制一次,这样带来程序代码量的急剧增加,也就是说内联函数是以目标代码的增加为代价来节省时间开销的,因此什么时候使用内联函数要根据如下原则:如果被调用的 Lambda 表达式或函数包含大量的执行代码,则不应该使用内联函数,如果 被调用的 Lambda 表达式或函数只包含非常简单的执行代码的话(尤其是单行表达式),建议使用内联函数。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值