Kotlin通用Out-projection类型禁止使用

时间:2018-04-19 07:10:32

标签: generics kotlin kotlin-interop

我正在使用来自后端的一种动态表单系统。为了能够映射我的表单,我有一个带有泛型的访问者模式,我使用Java工作,但我不能让它在Kotlin中工作。

我有这个界面:

internal interface FormFieldAccessor<T> {

    fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<T>

    fun setValueToBuilder(builder: Builder, value: T)

    fun accept(visitor: FormFieldVisitor)

    fun getValue(personalInfo: PersonalInfo): T
}

然后我有这样的访问者列表:

val accessors = mutableMapOf<String, FormFieldAccessor<*>>()
accessors[FIRST_NAME] = object : FormFieldAccessor<String> {
            override fun getValue(personalInfo: PersonalInfo): String {
                return personalInfo.surname
            }

            override fun accept(visitor: FormFieldVisitor) {
                visitor.visitString(this)
            }

            override fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<String> {
                //not relevant
            }

            override fun setValueToBuilder(builder: Builder, value: String) {
                builder.withSurname(value)
            }
        }
//more other accessors with different type like Int or Boolean

想要像这样使用它:

accessors[FIRST_NAME]!!.setValueToBuilder(builder, field.value )

但这不起作用并给我:

Out-projected type 'FormFieldAccessor<*>' prohibits the use of 'public abstract fun setValueToBuilder(builder: Builder, value: T): Unit defined in FormFieldAccessor'

如果你知道我做错了什么就会很酷:)

编辑:这里有一个较小的结构要点https://gist.github.com/jaumard/1fd1ccc9db0374cb5d08f047414a6bc8

我不想通过使用Any来松开类型,与Java相比感到沮丧,因为它很容易实现。我现在明白了星形投影的问题,但除此之外还有什么可以达到和java一样吗?

3 个答案:

答案 0 :(得分:1)

使用star-projection表示您对实际类型一无所知,正如文档所述:

  

有时您想说您对类型参数一无所知,但仍希望以安全的方式使用它。这里安全的方法是定义泛型类型的这种投影,该泛型类型的每个具体实例都是该投影的子类型。

     

[...]

     

对于Foo<out T : TUpper>,其中T是具有上限TUpper的协变类型参数,Foo<*>等同于Foo<out TUpper>。这意味着当T 未知时,您可以TUpper安全地读取<{1}}的值。

你可以做的是投射到合适的类型:

Foo<*>

然而,这些类型的演员阵容容易出错,更安全的方式如下;

(accessors[FIRST_NAME] as FormFieldAccessor<String>).setValueToBuilder(builder, field.value)

对星形投影地图的访问被包裹在一个对象中,使用此解决方案访问这些值是安全的。

你可以像这样使用它:

object FormFieldProvider {
    private val accessors = mutableMapOf<String, FormFieldAccessor<*>>()
    fun <T : Any> addAccessor(key: String, fieldValidator: FormFieldAccessor<T>) {
        accessors[key] = fieldValidator
    }

    @Suppress("UNCHECKED_CAST")
    operator fun <T : Any> get(key: String): FormFieldAccessor<T> =
            accessors[key] as? FormFieldAccessor<T>
                    ?: throw IllegalArgumentException(
                            "No accessor found for $key")
}

答案 1 :(得分:0)

@ s1m0nw1的答案为您提供问题的原因和简单的修复。但是,根据您的设置,可能还有另一种可能性。

添加

fun setValueFromForm(builder: Builder, fieldDefinition: FormFieldDefinition) { 
    setValueToBuilder(builder, getFormField(fieldDefinition).value)
}

FormFieldAccessor<T>。由于其签名不涉及T,因此可以在FormFieldAccessor<*>上安全地调用它。

如果您还将FormFieldDefinition存储在地图中,现在可以将其称为

accessors[FIRST_NAME]!!.setValueToBuilder(builder, fieldDefinitions[FIRST_NAME]!!)

进一步的改进取决于您系统的细节。

编辑:

即使没有看到Java代码使用原始类型(即FormFieldAccessor而不是FormFieldAccessor<Something>FormFieldAccessor<?>),我也相当肯定。像

这样的东西
Map<String, FormFieldAccessor> accessors = new HashMap<>();
...
accessors.get(FIRST_NAME).setValueToBuilder(builder, field.value);

但这也不安全,编译器只是忽略了问题,而不是告诉你它们。 accessors.get(FIRST_NAME)的价值实际上仍可以是一个FormFieldAccessor<Boolean>,在这种情况下,setValueToBuilder将失败并显示ClassCastException。您可以通过不小心将错误的名称传递给get或在地图中存储错误的访问者来看到这一点:它不会阻止任何编译。

相反,更好的Java代码将使用Map<String, FormFieldAccessor<?>>,然后需要在get之后进行转换,就像Kotlin代码一样。

原始类型的存在主要是为了允许非常旧的Java-5前代码仍然编译。 Kotlin没有这个考虑因素,所以它不支持原始类型,你唯一的选择就是用 在Java中做的事情。

答案 2 :(得分:0)

在这种情况下,您可以添加一个将Any用作值并检查其类型的包装器方法

internal interface FormFieldAccessor<T> {


    fun getFormField(formFieldDefinition: FormFieldDefinition): FormField<T>

    fun _setValueToBuilder(builder: Builder, value: T)

    fun setValueToBuilder(builder: Builder, value: Any){
        val value  = value as? T ?: return
        _setValueToBuilder(builder, value)
    }

    fun accept(visitor: FormFieldVisitor)

    fun getValue(personalInfo: PersonalInfo): T
}