3

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

4

3 回答 3

1

正如文档所述,使用星形投影表明您对实际类型一无所知:

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

[...]

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

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

(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(...)
于 2018-04-19T07:39:19.673 回答
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
}
于 2020-03-28T08:24:49.653 回答
0

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

添加

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

FormFieldAccessor<T>. 因为它的签名不涉及T,所以可以安全地在 a 上调​​用它FormFieldAccessor<*>

如果你也将FormFieldDefinitions 存储在 map 中,你现在可以调用它

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

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

编辑:

FormFieldAccessor即使没有看到它使用原始类型(即,而不是FormFieldAccessor<Something>or FormFieldAccessor<?>)的 Java 代码,我也有理由确定。就像是

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

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

相反,更好的 Java 代码会像 Kotlin 代码那样使用Map<String, FormFieldAccessor<?>>然后需要强制转换。get

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

于 2018-04-19T07:49:12.217 回答