Kotlin自定义Scope函数whenNull返回类型未按泛型约束生效问题
你当前的实现无法强制lambda返回类型匹配的核心原因是:Kotlin 泛型推断会综合所有调用位置的类型信息,推导满足所有约束的最宽适配类型。
在你的示例中,接收者x是Int?类型,传入的lambda返回String时,编译器会自动找到二者的公共父类Any作为泛型T的实际类型,完全符合你声明的() -> T约束,自然不会抛出类型错误。
这和Kotlin内置的Elvis运算符?:的行为逻辑一致:println(x ?: "string")本身也不会报错,最终返回类型是Any。
要强制lambda必须返回和接收者一致的T类型,核心是限制泛型T只能从扩展函数的接收者类型推断,不能从lambda返回值反向推导,有两种常用实现:
方案1:使用@OnlyInputTypes注解(推荐)
Kotlin提供了@OnlyInputTypes注解专门实现这个效果:标注在泛型参数上后,该泛型只会从函数的输入位置(接收者、入参)推断类型,不会从返回值、lambda返回值等输出位置推导类型。
虽然这个注解原本定义在kotlin.internal包下属于内部API,但JetBrains官方在多个公开标准库函数(比如filterIsInstance)中都使用了这个能力,你可以自行在项目中定义同名注解即可生效:
@Target(AnnotationTarget.TYPE_PARAMETER) @Retention(AnnotationRetention.BINARY) internal annotation class OnlyInputTypes import kotlin.contracts.* inline fun <@OnlyInputTypes T> T?.whenNull(block: () -> T): T { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } return if (this == null) block() else this }
修改后你示例中的第三行代码会直接报类型不匹配错误:
Type mismatch: inferred type is String but Int was expected
完全符合你要的强制类型校验效果。
方案2:不使用注解的泛型约束方案
如果你不想使用内部注解,可以通过增加无实际作用的推断锚点类,阻断编译器从lambda返回值反向推导T的路径,不需要依赖任何内部API:
import kotlin.contracts.* // 仅用于锚定泛型推断,无实际业务作用 class InferenceToken<T> inline fun <T> T?.whenNull(block: (InferenceToken<T>) -> T): T { contract { callsInPlace(block, InvocationKind.AT_MOST_ONCE) } return if (this == null) block(InferenceToken()) else this } fun main() { val x : Int? = null println(x ?: 42) println(x.whenNull { 42 }) // 正常通过 // 下面这行直接报错,因为lambda返回String不匹配Int要求 // println(x.whenNull { "why is kotlin not enforcing return of the same type?" }) }
这个方案的原理是:InferenceToken<T>作为lambda的入参,会让编译器优先从接收者确定T的类型,不会再因为lambda返回其他类型自动把T推导为宽类型Any,调用时如果不需要用到入参可以直接省略参数声明,写法和原函数完全一致。
内容的提问来源于stack exchange,提问作者egrobler

