在 Kotlin 中仅向具有多个类型参数的扩展函数提供一个类型参数



简介

在 Kotlin 中,我有一个通用的转换扩展函数,它简化了this类型为C的对象到另一种类型T的对象(声明为receiver)的转换,并具有额外的转换action,将receiver视为this并提供对原始对象的访问:

inline fun <C, T, R> C.convertTo(receiver: T, action: T.(C) -> R) = receiver.apply {
action(this@convertTo)
}

它是这样用的:

val source: Source = Source()
val result = source.convertTo(Result()) {
resultValue = it.sourceValue
// and so on...
}

我注意到我经常在由无参数构造函数创建的receivers上使用这个函数,并认为通过创建基于其类型自动构造receiver的附加convertTo()版本来进一步简化它会很好,如下所示:

inline fun <reified T, C, R> C.convertTo(action: T.(C) -> R) = with(T::class.constructors.first().call()) {
convertTo(this, action) // calling the first version of convertTo()
}

不幸的是,我不能这样称呼它:

source.convertTo<Result>() {}

因为 Kotlin 需要提供三个类型参数。

问题

鉴于上述上下文,是否可以在 Kotlin 中创建一个具有多个类型参数的泛型函数,该函数只接受提供一个类型参数,而其他类型则由调用站点确定?

其他示例(按@broot)

想象一下,stdlib 中没有filterIsInstance(),我们想实现它(或者我们是 stdlib 的开发者)。假设我们有权访问@Exact因为这对我们的示例很重要。最好将其声明为:

inline fun <T, reified V : T> Iterable<@Exact T>.filterTyped(): List<V>

现在,像这样使用它是最方便的:

val dogs = animals.filterTyped<Dog>() // compile error

不幸的是,我们必须使用以下解决方法之一:

val dogs = animals.filterTyped<Animal, Dog>()
val dogs: List<Dog> = animals.filterTyped()

最后一个还不错。

现在,我们想创建一个函数来查找特定类型的项目并映射它们:

inline fun <T, reified V : T, R> Iterable<T>.filterTypedAndMap(transform: (V) -> R): List<R>

同样,像这样使用它会很好:

animals.filterTypedAndMap<Dog> { it.barkingVolume } // compile error

相反,我们有这个:

animals.filterTypedAndMap<Animal, Dog, Int> { it.barkingVolume }
animals.filterTypedAndMap { dog: Dog -> dog.barkingVolume }

这仍然不是那么糟糕,但该示例故意相对简单,以使其易于理解。实际上,该函数会更复杂,会有更多的类型参数,lambda 会接收更多的参数,等等,然后它会变得难以使用。收到有关类型推断的错误后,用户必须仔细阅读函数的定义,以了解缺少什么以及在何处提供显式类型。

作为旁注:Kotlin 不允许这样的代码:cat is Dog,但允许这样:cats.filterIsInstance<Dog>(),这不是很奇怪吗?我们自己的filterTyped()不允许这样做。所以也许(但只是也许),filterIsInstance()这样设计正是因为这个问题中描述的问题(它使用*而不是额外的T)。

另一个例子,利用已经存在的reduce()功能。我们有这样的功能:

operator fun Animal.plus(other: Animal): Animal

(别问了,没道理)

现在,减少狗的名单似乎很简单:

dogs.reduce { acc, item -> acc + item } // compile error

不幸的是,这是不可能的,因为编译器不知道如何正确地推断SAnimal。我们不能轻易地只提供S,即使提供返回类型在这里也无济于事:

val animal: Animal = dogs.reduce { acc, item -> acc + item } // compile error

我们需要使用一些尴尬的解决方法:

dogs.reduce<Animal, Dog> { acc, item -> acc + item }
(dogs as List<Animal>).reduce { acc, item -> acc + item }
dogs.reduce { acc: Animal, item: Animal -> acc + item }

类型参数R不是必需的:

inline fun <C, T> C.convertTo(receiver: T, action: T.(C) -> Unit) = receiver.apply {
action(this@convertTo)
}
inline fun <reified T, C> C.convertTo(action: T.(C) -> Unit) = with(T::class.constructors.first().call()) {
convertTo(this, action) // calling the first version of convertTo()
}

如果使用Unit,即使传入的函数具有非Unit返回类型,编译器仍允许您传递该函数。

还有其他方法可以帮助编译器推断类型参数,而不仅仅是直接在<>中指定它们。您还可以为变量的结果类型添加注释:

val result: Result = source.convertTo { ... }

您还可以将convertTo的名称更改为类似convert的名称,以使其更具可读性。

另一种选择是:

inline fun <T: Any, C> C.convertTo(resultType: KClass<T>, action: T.(C) -> Unit) = with(resultType.constructors.first().call()) {
convertTo(this, action)
}
val result = source.convertTo(Result::class) { ... }

但是,这将与第一个重载冲突。所以你必须以某种方式解决它。您可以重命名第一个重载,但我想不出任何好名字。我建议您像这样指定参数名称

source.convertTo(resultType = Result::class) { ... }

旁注:我不确定无参数构造函数是否始终是构造函数列表中的第一个。我建议你实际找到无参数构造函数。

这个答案并没有解决所述问题,而是结合了来自@Sweeper的输入,以提供至少简化结果对象实例化的解决方法。

首先,如果我们显式声明变量的结果类型(即val result: Result = source.convertTo {}),但这还不足以解决@broot描述的情况。

其次,使用KClass<T>作为结果参数类型提供了使用KClass<T>.createInstance()确保我们找到无参数构造函数的能力(如果有 – 如果没有,则结果实例化convertTo()不符合使用条件)。我们还可以从 Kotlin 的默认参数值中受益,以使结果参数类型在调用中可省略,我们只需要考虑到action可能作为 lambda(调用的最后一个参数)或函数引用提供——这将需要两个版本的结果实例化convertTo()

因此,考虑到上述所有因素,我想出了convertTo()的实现:

// version A: basic, expects explicitly provided instance of `receiver`
inline fun <C, T> C.convertTo(receiver: T, action: T.(C) -> Unit) = receiver.apply {
action(this@convertTo)
}
// version B: can instantiate result of type `T`, supports calls where `action` is a last lambda
inline fun <C, reified T : Any> C.convertTo(resultType: KClass<T> = T::class, action: T.(C) -> Unit) = with(resultType.createInstance()) {
(this@convertTo).convertTo(this@with, action)
}
// version C: can instantiate result of type `T`, supports calls where `action` is passed by reference
inline fun <C, reified T : Any> C.convertTo(action: T.(C) -> Unit, resultType: KClass<T> = T::class) = with(resultType.createInstance()) {
(this@convertTo).convertTo(T::class, action)
}

所有三个版本根据特定用例协同工作。下面是一组示例,说明在什么情况下使用哪个版本。

class Source { var sourceId = "" }
class Result { var resultId = "" }
val source = Source()
fun convertX(result: Result, source: Source) {
result.resultId = source.sourceId
}
fun convertY(result: Result, source: Source) = true
fun Source.toResultX(): Result = convertTo { resultId = it.sourceId  }
fun Source.toResultY(): Result = convertTo(::convertX)
val result0 = source.convertTo(Result()) { resultId = it.sourceId } // uses version A of convertTo()
val result1: Result = source.convertTo { resultId = it.sourceId } // uses version B of convertTo()
val result2: Result = source.convertTo(::convertX) // uses version C of convertTo()
val result3: Result = source.convertTo(::convertY) // uses version C of convertTo()
val result4: Result = source.toResultX() // uses version B of convertTo()
val result5: Result = source.toResultY() // uses version C of convertTo()

PS:正如@Sweeper所注意到的,对于结果实例化版本来说,convertTo可能不是一个好名字(因为它不像基本版本那样可读),但这是一个次要问题。