重学Kotlin(三)函数

51 阅读10分钟

重学Kotlin(三)函数

一、函数的基本定义

在 Kotlin 中,函数用 fun 关键字声明:

fun sayHello() {
    println("Hello Kotlin")
}
  • fun 是关键字
  • sayHello 是函数名
  • () 表示参数列表(这里为空)
  • {} 内是函数体

调用函数时:

sayHello()

二、带参数与返回值的函数

带参数

fun greet(name: String) {
    println("Hello, $name")
}

调用:

greet("Tom")  // 输出:Hello, Tom

带返回值

fun add(a: Int, b: Int): Int {
    return a + b
}

Kotlin 支持类型推断:

fun add(a: Int, b: Int) = a + b  // 推断返回类型 Int

三、默认参数 & 命名参数

默认参数:

fun greet(name: String = "World") {
    println("Hello, $name")
}

greet()          // Hello, World
greet("Tom")     // Hello, Tom

命名参数:

fun connect(host: String, port: Int) {
    println("Connecting to $host:$port")
}

connect(port = 8080, host = "localhost")

四、可变参数(vararg)

允许传入任意数量的参数:

fun sumAll(vararg numbers: Int): Int {
    return numbers.sum()
}

println(sumAll(1, 2, 3, 4))  // 10

五、局部函数(函数里定义函数)

fun outer() {
    fun inner() {
        println("I'm inner")
    }
    inner()
}

局部函数只能在外部函数里使用。


六、顶层函数(不需要类)

在 Kotlin 中,函数不必放在类里,可以直接定义在文件顶层:

// MyUtils.kt
fun printHello() = println("Hello")

然后其他文件中可以直接导入使用。


七、扩展函数

给现有类“添加新方法”,而无需继承:

fun String.addSmile(): String {
    return this + "😊"
}

println("Hello".addSmile())  // Hello😊

八、Lambda 表达式与高阶函数

Kotlin 的函数可以作为参数或返回值。

注意: Lambda 表达式的详细介绍见上一章。

高阶函数示例:

fun operate(a: Int, b: Int, op: (Int, Int) -> Int): Int {
    return op(a, b)
}

Lambda 传入:

val result = operate(3, 4) { x, y -> x + y }
println(result) // 7

九、函数类型

函数也有类型:

val f: (Int, Int) -> Int = { a, b -> a + b }

十、内联函数(inline)

用于减少函数调用的性能开销(常与 Lambda 搭配):

inline fun runTwice(action: () -> Unit) {
    action()
    action()
}

十一、匿名函数

除了 Lambda,也可以用 fun 声明匿名函数:

val multiply = fun(x: Int, y: Int): Int = x * y
println(multiply(2, 3))

十二、函数与方法(成员函数)的区别

1. 从定义上区分

函数(Function): 独立存在的可执行代码块。

方法(Method): 属于某个类或对象的函数。

2. 从字节码角度区分

所有 Kotlin "函数" 都会变成 JVM 方法(method)。

区别只在于:

  • 顶层函数、扩展函数 → 编译成 静态方法
  • 类中函数 → 编译成 实例方法(带 this)
fun test() {}

fun String.testExt() {}
  public final static test()V
   L0
    LINENUMBER 15 L0
    RETURN
    MAXSTACK = 0
    MAXLOCALS = 0

  public final static testExt(Ljava/lang/String;)V
    // annotable parameter count: 1 (invisible)
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
   L0
    ALOAD 0
    LDC "<this>"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 19 L1
    RETURN
   L2
    LOCALVARIABLE $this$testExt Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

3. 函数的类型

class Test {
    fun test0(a: Int, b: Long): Any {
        print("a=$a,b=$b")
        return a + b
    }

    fun test1(c: (Test, Int, Long) -> Any): Any {
        return c(this, 0, 1L)
    }

    fun test2(c: Test.(Int, Long) -> Any): Any {
        return c(this, 0, 2L) 
    }

    fun test3(c: Function3<Test, Int, Long, Any>): Any {
        return c(this, 0, 3L)
    }

    fun main() {
        test1(Test::test0) // a=0,b=1
        test2(Test::test0) // a=0,b=2
        test3(Test::test0) // a=0,b=3
    }
}

以下三种写法是等效的,都能表示方法的类型:

  • (Test, Int, Long) -> Any
  • Test.(Int, Long) -> Any
  • Function3<Test, Int, Long, Any>

十三、函数的引用

函数引用就是把函数当作值传递。 用符号 :: 来表示。

1. 普通函数引用:直接调用

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

val sum: (Int, Int) -> Int = ::add
println(sum(2, 3))  // 输出 5

2. 成员函数引用(带接收者)

class Person(val name: String) {
    fun sayHello(age: Int) = "$name is $age years old"
}

val p = Person("Tom")

// 引用成员函数
val f: Person.(Int) -> String = Person::sayHello

// 调用方式 1:通过接收者
println(p.f(20))  // Tom is 20 years old

// 调用方式 2:使用 invoke
println(f.invoke(p, 25))  // Tom is 25 years old

3. 构造函数引用

class Person(val name: String, val age: Int)

val creator: (String, Int) -> Person = ::Person
val p = creator("Tom", 20)
println(p.name)  // Tom

4. 静态成员函数引用

class MathUtil {
    companion object {
        fun square(x: Int) = x * x
    }
}

// 函数引用
val sq: (Int) -> Int = MathUtil.Companion::square
println(sq(4))  // 16


十四、内联函数

Kotlin 中使用 inline 修饰的函数,会在编译期把 函数体代码直接拷贝到调用处,而不是通过函数调用跳转执行。

1. 为什么要使用内联函数

结论:能节省以下开销

  1. 函数调用开销
  2. Lambda 创建开销
  3. 对象分配开销

举例说明:

(1)不使用内联函数

会创建 Lambda 函数,并且有两次函数调用:

fun test() {
    println("Hello")
    runBlock {
        println("World")
    }
}

 fun runBlock(block: () -> Unit) {
    block()
}

// 为方便理解,把字节码反编译成了java
public static final void test() {
    System.out.println("Hello");
    runBlock(TestKt::test$lambda$0);
}

public static final void runBlock(@NotNull Function0 block) {
    Intrinsics.checkNotNullParameter(block, "block");
    block.invoke();
}

private static final Unit test$lambda$0() {
    System.out.println("World");
    return Unit.INSTANCE;
}

(2)使用内联函数

fun test() {
    println("Hello")
    runBlock {
        println("World")
    }
}

inline fun runBlock(block: () -> Unit) {
    block()
}

public static final void test() {
    System.out.println("Hello");
    int $i$f$runBlock = 0;
    int var1 = 0;
    System.out.println("World");
}

public static final void runBlock(@NotNull Function0 block) {
    Intrinsics.checkNotNullParameter(block, "block");
    int $i$f$runBlock = 0;
    block.invoke();
}

其实发生了两次内联:

  1. 把参数内联到内联函数中
  2. 内联函数内联到调用处

并且不会生成 Lambda 函数。

2. 内联函数的缺点

  1. 代码体积变大(代码膨胀):因为每次调用都展开
  2. 不能递归:inline 不允许递归函数(会无限展开)
  3. 过多 inline 会增大 dex / apk 大小:所以并不是所有函数都应该 inline

3. 内联函数使用场景

结合以上优缺点,内联函数需要在适当的时候去使用。典型的使用场景:

(1)高阶函数(最常用)

  • 例如:letapplyalsorunwith 全是 inline
  • 减少 Lambda 开销
  • 提高性能

(2)控制结构 DSL

  • 例如:repeatmeasureTimelocktransaction

(3)协程的 suspendCancellableCoroutine

  • 内联在协程框架中大量使用

4. inline 带来的额外能力:non-local return

inline fun run(block: () -> Unit) {
    block()
}

fun foo() {
    run {
        println("run")
        return   // ← 直接从 foo() 返回,普通高阶函数无法做到
    }
    println("foo") // 这一行不会被执行
}

这是因为 lambda 被直接展开到调用处(普通高阶函数是无法做到的)

5. crossinline 与 noinline

(1)crossinline

禁止 lambda 中使用 return,但仍然 inline:

inline fun doJob(crossinline block: () -> Unit) {
    Thread { block() }.start()  // 不能 non-local return
}

(2)noinline

禁止 inline(不展开):

inline fun funA(a: () -> Unit, noinline b: () -> Unit) {}
  • a 会被 inline
  • b 不会被 inline,可以传到别的地方当普通对象

Kotlin 内联函数(inline function) 有一些非常明确的限制,否则会引发编译错误或逻辑 bug。

6. 内联函数的限制

(1)不能递归(最重要)

因为 inline 会在编译期展开,如果递归则会无限展开 → 编译失败或爆炸。

inline fun f() = f()   // ❌ 递归,不能 inline
(2)不能 inline open / override 的函数

因为 open/override 依赖 运行时动态分派(虚方法表),与 inline 的“编译期展开”冲突。

open inline fun test() {}     // ❌
override inline fun test() {} // ❌

原因:虚函数在运行时才能确定实际类型,而 inline 必须在编译期就完全确定。

(3)inline 函数不能包含局部类(local class)

因为局部类依赖函数栈帧,inline 后栈结构改变。

inline fun foo() {
    class LocalClass {} // ❌ 不能定义局部类
}
(4)inline 函数不能包含局部的 non-captured object

比如:

inline fun foo() {
    object : Runnable {       // ❌
        override fun run() {}
    }
}

因为 inline 展开后会复制 object 声明,导致行为不一致。

(5)不能在 inline 的 lambda 中使用非 inline 的 return
inline fun run(block: () -> Unit) {
    block()
}

fun test() {
    run {
        return   // ← non-local return(允许)
    }
}

如果 lambda 不能 non-local return 就会报错,需要 crossinline

(6)无法 inline 的参数要标记 noinline

如果你传的 lambda 要:

  • 存储到变量
  • 传给另一个函数
  • 放到另一个对象里
  • 作为属性保存

就不能 inline,必须加 noinline

inline fun foo(a: () -> Unit, noinline b: () -> Unit) {
    doSomething(b)  // ❌ 如果不加 noinline 会报错
}

因为 inline 的 lambda 在调用处没有“对象”,所以不能当作对象处理。

(7)reified 类型参数必须依赖 inline

也可以当作一种限制:

fun <T> test() { }        // ❌ 无法获得 T 的真实类型

inline fun <reified T> test() { }  // ✔ 必须 inline

十五、常用高阶函数

1. 作用域函数

举例分析 letrun 函数
// 将调用者当做参数传递
public inline fun <T, R> T.let(block: (T) -> R): R {}
// 类似拓展函数,像在给对象添加一个临时方法
public inline fun <T, R> T.run(block: T.() -> R): R {}
class Person {
    var name = "Tom"
    var age = 28
    override fun toString(): String {
        return "Person(name='$name', age=$age)"
    }
}

fun main() {
    var person: Person? = Person()
    val f1: Person.() -> Unit = {
        this.name = "" //this 可省略
        println(name)
    }

    val f2: (Person) -> Unit = {
        it.age = 30 //it 代表当前 Person 对象
    }

    // 由上文十二节函数的类型分析, f1 和 f2 函数类型是完全等效的,所以可以进行如下调用
    person?.let(f1)
    person?.let(f2)
    // 也可以直接在 let 的 lambda 中调用,但需要满足函数类型为(Person) -> Unit
    val age = person?.let {
        it.age = 30
        it.age // 最后一行为返回值
    }

    // 同样的,我们也可以使用 run 来调用它们
    person?.run(f1)
    person?.run(f2)
    val name = person?.run {
        name = "Jerry"
        name
    }
}
其他常见作用域函数对比
函数this / it返回值典型用途
letit最后一行对可空对象做安全操作、链式调用
runthis最后一行初始化对象、转换值
applythisthis(对象本身)配置对象(构建对象)
alsoitthis(对象)对象链式调试、side effect
withthis最后一行对已有对象做一系列操作
takeIf / takeUnlessit条件成立返回对象过滤对象是否继续链式调用

2. 集合常用高阶函数

函数含义
map映射
filter过滤
flatMap展开
reduce累积(无初始值)
fold累积(有初始值)
groupBy分组
sortedBy排序
any/all/none判断
find找一个

十六、从字节码角度理解 Kotlin 函数也是一种类型

Kotlin 函数类型在 JVM 上被编译成接口实例:

  • (Int) -> StringFunction1<Int, String>
  • (A, B) -> CFunction2<A, B, C>

所以 Kotlin 函数“是类型”→ 实际就是一个 接口的实现类

例如:

val f: (Int) -> String = { it.toString() }

会编译成:

Function1<Integer, String> f = new Function1<Integer, String>() {
    @Override
    public String invoke(Integer it) {
        return it.toString();
    }
};

也就是匿名类。


十七、SAM(Single Abstract Method Interface)

1. SAM 是什么

SAM = 单一抽象方法接口(Single Abstract Method Interface)

也就是:只有一个抽象方法的接口(或 Java 抽象类)。

Java 里的例子:

interface Runnable {
    void run();
}

只有一个抽象方法 run() → SAM 类型。

2. Kotlin 为什么需要 SAM?

因为 Kotlin 里 Lambda 本质是 FunctionN 对象:

() -> Unit      // Function0<Unit>
(Int) -> String // Function1<Int, String>

但很多 Java 库要求传入一个接口实例,比如 Runnable、Callable、OnClickListener。

为了让 Kotlin 能像 Java 一样写得简洁,比如:

Thread { println("Hello") }

就需要把:

lambda → SAM 接口对象

这叫做 SAM 转换

3. Kotlin 的 SAM 转换规则

只能作用于 Java 接口

不能作用于 Kotlin 接口(除非 fun interface)。

例如:

button.setOnClickListener { ... }   // ✅ OK

但你自己写的 Kotlin 接口:

interface A { fun run() }

却不能:

val a = A { println("hi") }  // ❌ 错误

因为 Kotlin 默认不做 SAM 转换。

4. Kotlin 要启用 SAM,需要 fun interface

Kotlin 加了关键字:

fun interface Foo {
    fun bar()
}

只有加了 fun 才是 SAM 接口。

现在你可以这样写:

val foo: Foo = { println("hello") }

5. Kotlin 为什么默认不支持接口 SAM?

因为 Kotlin 接口可以有:

  • 多个抽象方法
  • 默认方法
  • 扩展方法
  • 属性

担心歧义,所以必须手动标记 fun interface

6. SAM 转换是什么样的?

例子:

Thread { println("running") }

Kotlin 会编译成:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("running");
    }
})

7. SAM 转换与 Lambda 的本质区别

Lambda 是一个函数对象(FunctionN)

类型:

() -> Unit

SAM 转换结果是一个接口实例

类型:

Runnable

所以它们完全不是一回事!

8. SAM 与 Kotlin 的函数类型不兼容

不能这样:

fun test(r: Runnable) { }

val f: () -> Unit = { }
test(f)  // ❌ 不能自动 SAM

必须:

test { } // OK:直接写 lambda 才会 SAM 转换

9. SAM 转换可能带来的隐患

// java类
public class EventManager {
    private final List<onEventListener> listeners = new ArrayList<>();
    public interface onEventListener {
        void onEvent(String event);
    }
    public void registerEvent(onEventListener listener) {
        listeners.add(listener);
    }
    public void unregisterEvent(onEventListener listener) {
        listeners.remove(listener);
    }
}


 val eventManager = EventManager()
    // 本质是匿名内部类
    eventManager.registerEvent(object : EventManager.onEventListener {
        override fun onEvent(event: String?) {
            println(event)
        }
    })

    // SAM转化为 lambda 形式
    eventManager.registerEvent {
        println("$it")
    }

    // ❌普通的 lambda 表达式, 无法作为接口实现,类型是 (String) -> Unit
    val onEvent1 = { event: String ->
        println()
    }
    eventManager.registerEvent(onEvent1)
    eventManager.unregisterEvent(onEvent1) // 无法注销onEvent1

    // ✅lambda 表达式实现接口
    val onEvent2 = EventManager.onEventListener { event ->
        println(event)
    }

    // ✅匿名对象实现接口
    val event3 = object : EventManager.onEventListener {
        override fun onEvent(event: String?) {
            println(event)
        }
    }