Как использовать типобезопасные компоновщики с неизменяемыми типами в Котлине - PullRequest
0 голосов
/ 03 июня 2018

Я хочу использовать типобезопасные компоновщики в Kotlin для типа, который имеет неизменные свойства.

class DataClass(val p1:String, val p2:String) {}

fun builder(buildBlock: DataClass.() -> Unit) {
    return DataClass(p1 = "",p2 = "").also(block)
}
//...
builder {
    p1 = "p1" // <-- This won't compile, since p1 is a val
    p2 = "p2" // <-- This won't compile, since p2 is a val
}

Я подумал о двух решениях, чтобы обойти это:

Вариант 1: Создать класс построителя:

class DataClass(val p1: String, val p2: String) {}

class DataClassBuilder(){
    lateinit var p1: String
    lateinit var p2: String
    fun build() = DataClass(p1, p2)
}

fun builder(buildBlock: DataClassBuilder.() -> Unit) {
    return DataClassBuilder().also(block).build()
}

Вариант 2: Создайте пользовательский делегат для предотвращения повторного задания значения:

class InitOnceDelegate: ReadWriteProperty<DTest, String> {
    private var state: String? = null
    override fun getValue(thisRef: DTest, property: KProperty<*>): String {
        return state ?: throw IllegalStateException()
    }

    override fun setValue(thisRef: DTest, property: KProperty<*>, value: String) {
        if (state == null) {
            state = value
        } else {
            throw IllegalStateException("${property.name} has already been initialized")
        }

    }
}

class DataClass() {
    var p1: String by InitOnceDelegate()
    var p2: String by InitOnceDelegate()
}

fun builder(buildBlock: DataClass.() -> Unit) {
    return DataClass(p1 = "",p2 = "").also(block)
}
//...
val d = builder {
    p1 = "p1" 
    p2 = "p2" 
}
d.p1 = "another value" // <-- This will throw an exception now.

У варианта 1 есть недостаток, заключающийся в том, что я должен поддерживать два класса, у варианта 2 есть недостаток, который разрешает компиляторустановите значение в DataClass еще раз, и проверка будет выполняться только во время выполнения.

Есть ли лучший способ решить эту проблему без указанных недостатков?

1 Ответ

0 голосов
/ 04 июня 2018

Это своего рода хакерское решение, которое все еще требует от вас поддержки двух классов:

interface DataClass
{
    companion object
    {
        fun builder(callback : DataClassImpl.() -> Unit) : DataClass
            = DataClassImpl().apply { callback() }
    }

    val p1 : String
    val p2 : String
}

class DataClassImpl : DataClass
{
    override lateinit var p1 : String
    override lateinit var p2 : String
}

В отличие от варианта 1, в этом примере вы создаете только один экземпляр, и в отличие от варианта 2 компилятор скажетЕсли вы попытаетесь изменить значение p1 или p2 после блока builder.

Чтобы избежать возможности приведения результата к DataClassImpl и изменения значений после того, как вымог бы делегировать интерфейс следующим образом:

interface DataClass
{
    companion object
    {
        fun builder(callback : DataClassBuilder.() -> Unit) : DataClass
            = DataClassBuilder().apply { callback() }.let { DataClassImpl(it) }
    }

    val p1 : String
    val p2 : String
}

class DataClassBuilder : DataClass
{
    override lateinit var p1 : String
    override lateinit var p2 : String
}

class DataClassImpl(
    private val delegate : DataClass
) : DataClass by delegate
...