5.8.9.5 函数式(SAM)接口

原文链接: https://kotlinlang.org/docs/fun-interfaces.html

5.8.9.5 函数式(SAM)接口

只有一个抽象成员函数的接口称为函数式接口,或单一抽象方法(SAM)接口。函数式接口可以有多个非抽象成员函数,但只能有一个抽象成员函数。

要在 Kotlin 中声明函数式接口,请使用 fun 修饰符。

1
2
3
fun interface KRunnable {
    fun invoke()
}

SAM 转换

对于函数式接口,你可以使用 SAM 转换,借助 lambda 表达式让代码更简洁、更易读。

你不必手动创建一个实现函数式接口的类,而可以使用 lambda 表达式。通过 SAM 转换,Kotlin 可以把任何签名与该接口唯一方法签名匹配的 lambda 表达式转换为动态实例化该接口实现的代码。

例如,考虑下面这个 Kotlin 函数式接口:

1
2
3
fun interface IntPredicate {
    fun accept(i: Int): Boolean
}

如果不使用 SAM 转换,你就需要写这样的代码:

1
2
3
4
5
6
// 创建一个类的实例
val isEven = object : IntPredicate {
    override fun accept(i: Int): Boolean {
        return i % 2 == 0
    }
}

借助 Kotlin 的 SAM 转换,你可以改写成下面这段等价的代码:

1
2
// 使用 lambda 创建实例
val isEven = IntPredicate { it % 2 == 0 }

一个简短的 lambda 表达式取代了所有多余的代码。

1
2
3
4
5
6
7
8
9
fun interface IntPredicate {
    fun accept(i: Int): Boolean
}

val isEven = IntPredicate { it % 2 == 0 }

fun main() {
    println("Is 7 even? - ${isEven.accept(7)}")
}

你也可以对 Java 接口使用 SAM 转换。

从带构造函数函数的接口迁移到函数式接口

从 1.6.20 开始,Kotlin 支持指向函数式接口构造函数的可调用引用,这为从带构造函数函数的接口迁移到函数式接口提供了一种源码兼容的方式。请看下面的代码:

1
2
3
4
5
6
7
interface Printer {
    fun print()
}

fun Printer(block: () -> Unit): Printer = object : Printer {
    override fun print() = block()
}

启用指向函数式接口构造函数的可调用引用后,这段代码可以简化为一个函数式接口声明:

1
2
3
fun interface Printer {
    fun print()
}

它的构造函数会被隐式创建,任何使用 ::Printer 函数引用的代码都能编译通过。例如:

1
documentsStorage.addPrinter(::Printer)

用 @Deprecated 注解并把 DeprecationLevel.HIDDEN 应用于旧的 Printer 函数,即可保持二进制兼容性:

1
2
@Deprecated(message = "Your message about the deprecation", level = DeprecationLevel.HIDDEN)
fun Printer(...) {...}

函数式接口与类型别名

你也可以直接用函数类型的类型别名改写上面的写法:

1
2
3
4
5
6
7
typealias IntPredicate = (i: Int) -> Boolean

val isEven: IntPredicate = { it % 2 == 0 }

fun main() {
    println("Is 7 even? - ${isEven(7)}")
}

不过,函数式接口和类型别名的用途并不相同。类型别名只是已有类型的名称——它们不会创建新类型,而函数式接口会。你可以为某个特定的函数式接口提供扩展,使这些扩展不适用于普通函数或它们对应的类型别名。

类型别名只能有一个成员,而函数式接口可以有多个非抽象成员函数和一个抽象成员函数。函数式接口还可以实现和继承其他接口。

函数式接口比类型别名更灵活、能力更强,但无论从语法上还是运行时来看都可能代价更高,因为它们可能需要转换为特定接口。在代码中选择使用哪一种时,请考虑你的需求:

  • 如果你的 API 需要接受一个具有特定参数和返回类型的函数(任意函数),就使用简单的函数类型,或定义类型别名来为相应函数类型起一个更短的名字。
  • 如果你的 API 接受的是比函数更复杂的实体——例如它带有无法用函数类型签名表达的复杂契约和/或操作——就为它单独声明一个函数式接口。