【问题标题】:Kotlin generic Out-projected type prohibits the use ofKotlin 泛型 Out-projected 类型禁止使用
【发布时间】:2018-09-29 14:08:17
【问题描述】:

我正在使用一种来自后端的动态表单系统。为了能够映射我的表单,我有一个带有泛型的访问者模式,我让它在 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 相同的吗?

【问题讨论】:

    标签: generics kotlin kotlin-interop


    【解决方案1】:

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

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

    [...]

    对于Foo&lt;out T : TUpper&gt;,其中T 是具有上限TUpper 的协变类型参数,Foo&lt;*&gt; 等价于Foo&lt;out TUpper&gt;。这意味着当T未知 时,您可以安全地从Foo&lt;*&gt; 读取 TUpper 的值。

    你可以做的是转换为适当的类型:

    (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")
    }
    

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

    你可以这样使用它:

    FormFieldProvider.addAccessor(FIRST_NAME, object : FormFieldAccessor<String> {
        //...
    })
    
    FormFieldProvider.get<String>(FIRST_NAME).setValueToBuilder(...)
    

    【讨论】:

    • 我尝试测试您的一些解决方案,但也无法编译,它是一些用 Java 编写的遗留系统的一部分,我需要在 Kotlin 中保持相同的结构 :( 我更新了如果您不介意尝试一下,请使用简单的 gist 文件向您展示我的结构
    【解决方案2】:

    @s1m0nw1 的回答为您提供了问题的原因和简单的解决方法。但是,根据您的设置,可能还有另一种可能性。

    添加

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

    FormFieldAccessor&lt;T&gt;。因为它的签名不涉及T,所以可以安全地在FormFieldAccessor&lt;*&gt; 上调用它。

    如果您还将FormFieldDefinitions 存储在地图中,您现在可以调用它

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

    进一步的改进将取决于您系统的详细信息。

    编辑:

    即使没有看到它使用原始类型的 Java 代码(即 FormFieldAccessor 而不是 FormFieldAccessor&lt;Something&gt;FormFieldAccessor&lt;?&gt;),我也相当确定。像

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

    但这也是不安全的,编译器只是忽略了问题而不是告诉你它们。 accessors.get(FIRST_NAME) 的值实际上仍然可以是例如FormFieldAccessor&lt;Boolean&gt;,在这种情况下,setValueToBuilder 将失败并返回 ClassCastException。您可以通过不小心将错误的名称传递给get 或在映射中存储错误的访问器来看到这一点:它不会阻止任何东西的编译。

    相反,更好的 Java 代码会使用 Map&lt;String, FormFieldAccessor&lt;?&gt;&gt;,然后像 Kotlin 代码一样需要在 get 之后进行强制转换。

    原始类型的存在主要是为了让非常古老的 Java-5 之前的代码仍然可以编译。 Kotlin 没有这种考虑,因此它不支持原始类型,并且您唯一的选择是执行您在 Java 中应该执行的操作。

    【讨论】:

    • 我也尝试测试您的解决方案,但也无法编译,它是一些用 Java 编写的遗留系统的一部分,我需要在 Kotlin 中保持相同的结构 :( 我更新了如果您不介意尝试一下,请使用简单的 gist 文件向您展示我的结构
    • 我不存储 formFieldDefinition 因为我已经在循环下一个 fieldDefinition 在我想 setValueToBuilder :(
    • 我已经编辑了我的答案,以解释为什么您的 Java 代码可能只是隐藏了问题。
    • "因为我已经在一个循环下一个 fieldDefinition 在我想要 setValueToBuilder 的时候" 这实际上更好,这意味着它已经可用并且不需要存储。
    • 非常感谢您的解释,你说得对,它使用旧的原始类型...我说 fieldDefinition 但它是 formField,不幸的是我当时没有 formDefinition :(跨度>
    【解决方案3】:

    在这种情况下,您可以添加一个将 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
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-04-05
      • 2018-06-02
      • 1970-01-01
      • 1970-01-01
      • 2019-09-09
      • 1970-01-01
      相关资源
      最近更新 更多