Kotlin 相当于 java 的 Function<*, String>

Kotlin equivalent to java's Function<*, String>

我有一个用例,我需要创建一个从 KClass 到 (lambda) 函数的映射映射,该函数将那个 class 的实例转换为其他东西(在我的例子中是一个字符串)。在 java 中,我会按照以下方式写一些东西:

private Map<Class<?>, Function<?, String>> mappers = new HashMap<>();

在 Kotlin 中,lambda 参数的语法应该是 (Something...) -> ReturnType,但这既不接受 *,也不接受 inout。请注意,地图应该能够包含任何采用指定类型 或更具体 的任何参数的映射器,以便最宽松(但是由于这是动态的,它不会进行验证。了解铸造目的很重要)。

如何正确表达?

我最后是怎么解决的

感谢@broot 和@Sweeper,我能够实现我的用例。因为我遇到了一些其他问题,并且我假设发现此问题的任何人都有类似的用例,所以我想添加其余有问题的代码。我最终选择了以下内容(简化为基本要素):

// note that this map is public because it is accessed by a public 
// inline function. Making it private won't compile.
val mappers = HashMap<KClass<*>, (Any) -> String>()

// this is used to add mappers to the map of mappers.
// note the noinline!
inline fun <reified T> mapping (noinline mapper: (T) -> String) {
    mappers[T::class] = mapper as (Any) -> String
}

// This is the only method accessing the map to extract mappers and 
// directly use them.
fun mapToString(obj: Any): String {
    // some stuff here...

    // Attempt to map to the id via a predefined mapper.
    candidate = mappers[obj::class]?.let { it.invoke(obj) }
    if (candidate != null) return candidate

    // some other fallback here...
}

另请注意,以上所有内容都嵌套在另一个 class 中,为了参数调用 Cache,我将其嵌套。这是单元测试:

@Test
fun `test simple extractor`() {
    class SomeClass(val somethingToExtract: String)
    val someInstance = SomeClass("someValue")
    
    val cache = Cache()
    // defining the extractor
    cache.mapping<SomeClass> { it.somethingToExtract }

    // using the extractor
    val id = cache.mapToString(someInstance)
    assertEquals(id, someInstance.somethingToExtract)
}

默认情况下,Java 和 Kotlin 都不允许这种操作,因为它不是类型安全的。问题是你可以拿一个接收整数的函数为例,存储在映射中,然后在稍后将字符串传递给它来使用它。

我们可以强制编译器通过执行未经检查的转换来禁用类型保证。我们需要使用 Any 而不是 *:

private val mappers = mutableMapOf<KClass<*>, (Any) -> String>()

fun main() {
    mappers[User::class] = ::getUserName as (Any) -> String // unchecked cast

    val john = User("John")
    val username = mappers[User::class]?.invoke(john)
    println(username) // John
}

fun getUserName(user: User): String = user.name

data class User(val name: String)

然后我们必须确保正确使用类型。可能最好的办法是将此映射包装在我们自己的实用程序中,该实用程序对类型执行运行时检查以提供类型安全。

你Java的类型写的有点奇怪。特别地,函数的类型是:

Function<? extends Object, String>

请注意 ?? extends Object 相同。这会破坏 PECS,您将无法安全地将 null 以外的任何内容传递给它的 apply 方法。

Function的第一个类型参数是函数接受(消费)的类型,根据PECS,应该标示为super(逆变),而不是extends (协变):

Function<? super Object, String>

如果第二个类型参数也遵循 PECS,它将是 Function<? super Object, ? extends String>

现在,Kotlin 不会让您首先破坏 PECS。 Kotlin 中的每个函数类型自动具有逆变参数类型和协变 return 类型。这就是为什么您可以将 (Any) -> String 分配给 (String) -> Any 而无需任何转换。

Function<? super Object, ? extends String> 等价的 Kotlin 函数类型是:

(Any) -> String

(Any?) -> String 取决于您对可空值的喜欢程度。

您应该使您的地图具有这些类型之一作为值类型,并且由于这会改变方差,您将需要更改进行未检查转换的位置,但这应该很简单。

正如 broot 在评论中提醒我的那样,Kotlin 有一个 Nothing 类型而 Java 没有。这是所有类型的子类型(将其与 Any - 所有类型的超类型进行比较)。当您尝试在 Kotlin 中破坏 PECS 时,您会看到这一点,例如尝试在 Function<*, String> 上调用 apply,您会看到 apply 需要 Nothing.

因此,你可以写(Nothing) -> String来表示Function<?, String>,但我不建议这样做。 “一个以 'nothing' 作为参数的函数”有点难以阅读和混淆。它是否需要参数? :D