3

介绍

在 Kotlin 中,我有一个通用的转换扩展函数,它简化了将类型对象转换为this另一种类型C的对象(T声明为receiveractionreceiverthis

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附加版本来进一步简化它会很好,如下所示:convertTo()receiver

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 会接收更多参数等等,然后它就会变得难以使用。在收到关于类型推断的错误后,用户必须彻底阅读函数的定义以了解缺少什么以及在哪里提供显式类型。

cat is Dog附带说明:Kotlin 不允许这样的代码: ,但允许这样的代码:这不是很奇怪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 }
4

2 回答 2

1

该答案并未解决所述问题,而是结合了来自@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的,结果实例化版本可能不是一个好名字(因为它不像基本版本那样可读),但这是次要问题。

于 2021-10-15T13:17:38.857 回答
1

类型参数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) { ... }

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

于 2021-10-15T10:33:25.723 回答