简介
在 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
不幸的是,这是不可能的,因为编译器不知道如何正确地推断S
Animal
。我们不能轻易地只提供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
可能不是一个好名字(因为它不像基本版本那样可读),但这是一个次要问题。