kotlin 实战之函数与 lambada 表达式总结

2,782 阅读8分钟

工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~

特别说明,kotlin 系列文章均以 Java 差异为核心进行提炼,与 Java 相同部分不再列出。随着 kotlin 官方版本的迭代,文中有些语法可能会发生变化,请务必留意,语言领悟精髓即可,差异只是语法层面的事情,建议不要过多关注语法。

kotlin 默认参数函数

在 kotlin 中调用 java 方法时不能使用具名参数语法,因为 java 字节码并不总是会保留方法参数信息。kotlin 默认参数函数的定义与使用案例如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//重载一个 test 方法
fun test() = println("other test")

fun test(a: Int = 0, b: Int = 1, c: String = "666") = println("$a$b$c")

/**
 调用结果:
 other test
 121666
 012666
 121ccc
 */
fun testRun() {
    test()  //调用了重载的无参 test 方法
    test(12)
    test(b = 12)  //具名参数调用方式
    //编译报错 Type mismatch. Required: Int Found: String
    //test("888")
    test(a=12, c="ccc")
}

对于重写的方法来说,子类所拥有的重写方法会使用与父类相同的默认参数值。在重写一个拥有默认参数值的方法时,方法签名中必须要将默认参数值省略掉。如下案例:

open class AnimBase {
    open fun test(a: Int, b: Int = 1, c: String = "666") = println("$a$b$c")
}

class AnimDog: AnimBase() {
    override fun test(a: Int, b: Int, c: String) {
        super.test(a, b, c)
    }

    //重写方法不允许为参数值指定默认值,无论这里 a 或者 b 都不行
    //编译报错 An overriding function is not allowed to specify default values for its parameters
    //override fun test(a: Int = 1, b: Int = 1, c: String) {
    //    super.test(a, b, c)
    //}
}

/**
 调用结果:
 121666
 */
fun testRun() {
    val anim = AnimDog()
    anim.test(a = 12)
}

kotlin lambada 表达式

kotlin 中 lambada 表达式整体和 java 很像,但也有自己的特殊。kotlin lambada 表达式格式要求如下:

  • 一个 lambada 表达式总是被一个花括号所包围。
  • 其参数(如果存在)位于->之前,参数类型是可以省略掉的。
  • 执行体位于->之后。

默认情况下,lambada 表达式中最后一个表达式的值会隐式作为该 lambada 表达式的返回值。我们可以通过全限定的语法来限定从 lambada 表达式返回值。如下案例:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//compute 是定义的一个 lambada 表达式参数
fun testLambada(i: Int = 1, j: Int = 1, compute: (x: Int, y: Int) -> Unit) {
    compute(i, j)
}

fun callBack(a: Int, b: Int): Unit {
    println("a=$a, b=$b")
}

/**
 调用结果:
 1 - 1 = 0
 2 + 4 = 6
 a=2, b=6
 */
fun testRun() {
    testLambada { x, y -> println("$x - $y = ${x - y}") }

    testLambada(2, 4) { x, y -> println("$x + $y = ${x + y}") }

    testLambada(2, 6, ::callBack)
}

再来看一个例子:

fun test1(num: Int, exec: () -> Unit) {
    exec()
}

fun test2(num: Int, exec: (i: Int) -> Unit) {
    exec(num)
}

fun test3(num: Int, exec: (i: Int, j: Int) -> Unit) {
    exec(num, 3)
}

fun main() {
    test1(2) {
        //这里没有 $it
        println("666")
    }

    //当只有一个参数时此时会隐式传递 it 参数
    test2(3) {
        println("777 $it")
    }

    //多个参数时必须显式指定
    test3(4) { _, _ ->
        println("888")
    }
}

kotlin 可变参数函数

一个方法中只允许一个有一个可变参数定义,且通常位于参数列表最后一个。如果可变参数不是最后一个参数,那么其后的参数都需要通过具名参数形式传递,如果其后的参数是函数类型,还可以在圆括号外传递 lambada 表达式来实现而不用指定具名。如下案例:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//可变参数定义使用 vararg
fun testArgs(vararg strs: String) {
    println(strs.javaClass) //字符串数组类型
    strs.forEach { println(it) }
}

/**
 调用结果:
 class [Ljava.lang.String;
 666
 777
 ------------
 class [Ljava.lang.String;
 11
 22
 */
fun testRun() {
    testArgs("666", "777")
    println("------------")

    val args1 = arrayOf<String>("11", "22")
    //编译报错 Type mismatch. Required: String Found: kotlin.collections.ArrayList<String> /* = java.util.ArrayList<String> */
    //testArgs(args1)

    //使用分散运算符*才可以
    testArgs(*args1)
}

kotlin 中缀符号(infix notation)

kotlin 的函数还可以通过中缀符号来调用,需要满足如下三个条件:

  • 是成员函数或者扩展函数。
  • 拥有单个参数。
  • 声明时使用 infix 关键字。

如下是一个案例:

class InfixT (private var age: Int) {
    infix fun add(num: Int) = this.age + num
}

/**
 调用结果:
 24
 33
 */
fun testRun() {
    val test = InfixT(12)
    //如下两种调用等价,常出现在 kotlin 底层库中
    println(test.add(12))   //普通调用
    println(test add 21)    //中缀调用
}

kotlin 内联函数

内联提升调用效率,但是增大了字节码体积,其优缺点及机制原理同其他语言的内联函数等价。如下案例:

inline fun add(a: Int, b: Int) = a + b

关于内联函数原理其实很简单,建议直接 javap -c 去看下字节码区别就能理解优缺点了。

kotlin 高阶函数

在一个函数参数中可以传递另一个函数作为参数,则这个函数就是高阶函数,经常使用 lambada 表达式达到这个目的,常见的 lambada 表达式定义:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//add变量是一个函数类型,其值等于一个 lambada 函数
val add: (Int, Int) -> Int = { a: Int, b: Int -> a + b }
//简写
val add: (Int, Int) -> Int = { a, b -> a + b }

//自动类型推断写法
val sub = { a: Int, b: Int -> a - b }

//print 变量也是一个 lambada 表达式,没有参数而已
val print = { println("666") }

//_ 表示是一个参数,但是参数在函数体中不会被用到
val mayNull: (Int, Int) -> Int? = { _, _ -> null }

//表示这个 lambada 表达式函数类型的变量可以为 null,这里赋值了 null
val functionMayNull: (Int, Int) -> Int)? = null

下面定义一个高阶函数及使用:

//高阶函数定义
fun funcHeight(a: Int, b: Int, calculate: (Int, Int) -> Int): Int {
    return calculate(a, b)
}

/**
 调用结果:
 22
 2
 */
fun testRun() {
    val add = { a: Int, b: Int -> a + b }
    println(funcHeight(12, 10, add))

    println(funcHeight(12, 10) { x, y -> x - y })
}

kotlin 匿名函数

kotlin 的匿名函数主要用在 lambada 表达式中。如下案例:

fun funcHeight(a: Int, b: Int, calculate: (Int, Int) -> Int): Int {
    return calculate(a, b)
}

//编译报错,匿名函数不能定义在外部 Function declaration must have a name
//fun (x: Int, y: Int) = x + y

/**
 调用结果:
 7
 7
 */
fun testRun() {
    //匿名函数
    fun (x: Int, y: Int) = x + y

    //lambada表达式方式
    var ret = funcHeight(3, 4, { x, y -> x + y })
    println(ret)

    //匿名函数用在 lambada 表达式中
    ret = funcHeight(3, 4, fun (x, y): Int = x + y)
    println(ret)
}

kotlin 闭包

kotlin 允许在 lambada 表达式中修改外层的变量值(java 不允许,且外部变量需为 final),这种特性其实就是一种闭包特性,如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

/**
 调用结果:
 123412345
 */
fun testRun() {
    var sum = ""

    val strs = arrayOf("1234", "123", "12345")
    strs.filter { it.length > 3 }.forEach {
        //闭包
        sum += it
    }
    println(sum)
}

kotlin 带接收者的函数字面值

kotlin 提供了一种功能,可以通过指定的接收者对象来调用一个函数字面值。在函数字面值内部,你可以调用接收者对象的方法而无需使用任何额外的修饰符,譬如 kotlin 提供的 apply 函数机制就是通过这个实现的。这一点非常类似扩展函数。如下是一个案例:

//表示 sub 是 Int 的一个函数类型变量
val sub: Int.(arg: Int) -> Int = { arg -> this - arg }

/**
 调用结果:
 70
 */
fun testRun() {
    //Int 类型变量才可以调用 sub 函数
    println(100.sub(30))
}

匿名函数语法可以让我们指定函数字面值的接收类型,这样我们就可以先去声明一个带有接收者的函数类型变量,然后再去使用它。如下案例:

/**
 调用结果:
 70
 */
fun testRun() {
    //带接收者的函数字面值 匿名函数方式,注意与上一个例子对比
    val sub1 = fun Int.(arg: Int): Int = this - arg
    println(100.sub1(30))
}

带有接收者类型的函数的非字面值可以作为参数进行传递,前提是所需要接收函数的地方应该有一个接收者类型的参数,反之亦然。

比如说类型String.(Int)->Boolean(表示带接收者的一个函数的类型,用字符串调用,接收一个 Int 参数,返回 Boolean 值),它与(String, Int) -> Boolean(表示这个函数本身接收两个参数,一个 String,一个 Int,返回 Boolean,其第一个参数 String 就等于前面带接收者类型函数的调用者)等价。

/**
 调用结果:
 true
 true
 true
 true
 true
 */
fun testRun() {
    //表示带接收者的字面值,判断 string 是否等于一个 Int 值
    val equalNum: String.(Int) -> Boolean = { arg -> this.toIntOrNull() == arg }
    println("123".equalNum(123))
    println(equalNum("123", 123))

    fun equalNum1(string: String, int: Int): Boolean = string.toIntOrNull() == int
    println(equalNum1("123", 123))
    //编译报错,不存在 equalNum1
    //println("123".equalNum1( 12))

    //定义一个函数(上面概念解说部分的场景)
    fun equalNum2(op: (String, Int) -> Boolean, a: String, b: Int, c: Boolean) = println(op(a, b) == c)
    //可以直接传递表示带接收者的字面值,等价的(上面概念解说部分的场景)
    equalNum2(equalNum, "123", 123, true)
    //传递普通函数回调
    equalNum2(::equalNum1, "123", 123, true)
}

到此函数与 lambada 部分结束。

【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题 未经允许严禁转载 blog.csdn.net/yanbober