Kotlin: lateinit to val, или, альтернативно, var, который может установить один раз

Просто любопытно: В Котлин я хотел бы получить некоторый val, который можно инициализировать ленивым, но с параметром. Это потому, что мне нужно что-то, что создано очень поздно, чтобы инициализировать его.

В частности, я бы хотел:

private lateinit val controlObj:SomeView

или же:

private val controlObj:SomeView by lazy { view:View->view.findViewById(...)}

а потом:

override fun onCreateView(....) {
    val view = inflate(....)


    controlObj = view.findViewById(...)

или во втором случае controlObj.initWith(view) или что-то вроде этого:

return view

Я не могу использовать by lazy потому что by lazy не будет принимать внешние параметры, которые будут использоваться при инициализации. В этом примере - содержащий view.

Конечно, у меня есть lateinit var но было бы неплохо, если бы я мог убедиться, что он будет прочитан только после настройки, и я мог бы сделать это в одной строке.

Есть ли довольно чистый способ создания переменной только для чтения, которая инициализируется только один раз, но только когда рождаются некоторые другие переменные? Любое ключевое слово init once? Что после init компилятор знает, что он неизменен?

Я знаю о возможных проблемах параллелизма здесь, но если я осмелюсь обратиться к нему перед init, я, безусловно, заслуживаю того, чтобы его бросили.

Ответы

Ответ 1

Вы можете реализовать собственный делегат следующим образом:

class InitOnceProperty<T> : ReadWriteProperty<Any, T> {

    private object EMPTY

    private var value: Any? = EMPTY

    override fun getValue(thisRef: Any, property: KProperty<*>): T {
        if (value == EMPTY) {
            throw IllegalStateException("Value isn't initialized")
        } else {
            return value as T
        }
    }

    override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
        if (this.value != EMPTY) {
            throw IllegalStateException("Value is initialized")
        }
        this.value = value
    }
}

После этого вы можете использовать его следующим образом:

inline fun <reified T> initOnce(): ReadWriteProperty<Any, T> = InitOnceProperty()

class Test {

     var property: String by initOnce()

     fun readValueFailure() {
         val data = property //Value isn't initialized, exception is thrown
     }

     fun writeValueTwice() {
         property = "Test1" 
         property = "Test2" //Exception is thrown, value already initalized
     }

     fun readWriteCorrect() {
         property = "Test" 
         val data1 = property
         val data2 = property //Exception isn't thrown, everything is correct
     }

}

В случае, если вы попытаетесь получить доступ к значению до его инициализации, вы получите исключение, а также при попытке переназначить новое значение.

Ответ 2

В этом решении вы реализуете пользовательский делегат, и он становится отдельным свойством в вашем классе. У делегата есть var внутри, но свойство controlObj имеет необходимые вам гарантии.

class X {
    private val initOnce = InitOnce<View>()
    private val controlObj: View by initOnce

    fun readWithoutInit() {
        println(controlObj)
    }

    fun readWithInit() {
        initOnce.initWith(createView())
        println(controlObj)
    }

    fun doubleInit() {
        initOnce.initWith(createView())
        initOnce.initWith(createView())
        println(controlObj)
    }
}

fun createView(): View = TODO()

class InitOnce<T : Any> {

    private var value: T? = null

    fun initWith(value: T) {
        if (this.value != null) {
            throw IllegalStateException("Already initialized")
        }
        this.value = value
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T =
            value ?: throw IllegalStateException("Not initialized")
}

Кстати, если вам нужна безопасность потоков, решение немного отличается:

class InitOnceThreadSafe<T : Any> {

    private val viewRef = AtomicReference<T>()

    fun initWith(value: T) {
        if (!viewRef.compareAndSet(null, value)) {
            throw IllegalStateException("Already initialized")
        }
    }

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T =
            viewRef.get() ?: throw IllegalStateException("Not initialized")
}

Ответ 3

Вы можете использовать lazy. Например, с TextView

    val text by lazy<TextView?>{view?.findViewById(R.id.text_view)}

где view is getView(). А после onCreateView() вы можете использовать text как переменную только для чтения

Ответ 4

Вы можете реализовать собственный делегат следующим образом:

private val maps = WeakHashMap<Any, MutableMap<String, Any>>()

object LateVal {
    fun bindValue(any: Any, propertyName: String, value: Any) {
        val map = maps.getOrPut(any) { mutableMapOf<String, Any>() }

        if (map[propertyName] != null) {
            throw RuntimeException("Value is initialized")
        }

        map[propertyName] = value
    }

    fun <T> lateValDelegate(): MyProperty<T> {
        return MyProperty<T>(maps)
    }

    class MyProperty<T>(private val maps: WeakHashMap<Any, MutableMap<String, Any>>) : ReadOnlyProperty<Any?, T> {

        override fun getValue(thisRef: Any?, property: KProperty<*>): T {
            val ret = maps[thisRef]?.get(property.name)
            return (ret as? T) ?: throw RuntimeException("Value isn't initialized")
        }
    }
}

fun <T> lateValDelegate(): LateVal.MyProperty<T> {
    return LateVal.MyProperty<T>(maps)
}

fun Any.bindValue(propertyName: String, value: Any) {
    LateVal.bindValue(this, propertyName, value)
}

После этого вы можете использовать его следующим образом:

class Hat(val name: String = "casquette") {
    override fun toString(): String {
        return name
    }
}

class Human {
    private val hat by lateValDelegate<Hat>()

    fun setHat(h: Hat) {
        this.bindValue(::hat.name, h)
    }

    fun printHat() {
        println(hat)
    }

}

fun main(args: Array<String>) {
    val human = Human()
    human.setHat(Hat())
    human.printHat()
}

В случае, если вы попытаетесь получить доступ к значению до его инициализации, вы получите исключение, а также при попытке переназначения нового значения.

Кроме того, вы можете написать DSL, чтобы сделать его читабельным.

object to

infix fun Any.assigned(t: to) = this

infix fun Any.property(property: KProperty<*>) = Pair<Any, KProperty<*>>(this, property)

infix fun Pair<Any, KProperty<*>>.of(any: Any) = LateVal.bindValue(any, this.second.name, this.first)

и затем назовите это так:

fun setHat(h: Hat) {
    h assigned to property ::hat of this
}

Ответ 5

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

companion object {
    @Volatile private var INSTANCE: SomeViewSingleton? = null

    fun getInstance(context: Context): SomeViewSingleton =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: buildSomeViewSingleton(context).also { INSTANCE = it }
            }

    private fun buildSomeViewSingleton(context: Context) =
            SomeViewSingleton(context)
}

Тогда все, что вам нужно сделать, это вызвать getInstance(...) и вы всегда получите тот же объект.

Если вы хотите привязать время жизни объекта к окружающему объекту, просто отпустите объект-компаньон и поместите инициализатор в свой класс.

Также синхронизированный блок заботится о проблемах параллелизма.

Ответ 6

Для Activity все в порядке:

private val textView: TextView by lazy { findViewById<TextView>(R.id.textView) }

Для Fragment не имеет смысл создать final переменную с любым View типа, потому что вы потеряли связь с этой точки зрения после onDestroyView.

PS Используйте синтетические свойства Kotlin для доступа к представлениям в Activity и Fragment.

Ответ 7

Вы можете реализовать собственный делегат следующим образом:

class LateInitVal {
    private val map: MutableMap<String, Any> = mutableMapOf()

    fun initValue(property: KProperty<*>, value: Any) {
        if (map.containsKey(property.name)) throw IllegalStateException("Value is initialized")

        map[property.name] = value
    }

    fun <T> delegate(): ReadOnlyProperty<Any, T> = MyDelegate()

    private inner class MyDelegate<T> : ReadOnlyProperty<Any, T> {

        override fun getValue(thisRef: Any, property: KProperty<*>): T {
            val any = map[property.name]
            return any as? T ?: throw IllegalStateException("Value isn't initialized")
        }

    }
}

После этого вы можете использовать его следующим образом:

class LateInitValTest {
    @Test
    fun testLateInit() {
        val myClass = MyClass()

        myClass.init("hello", 100)

        assertEquals("hello", myClass.text)
        assertEquals(100, myClass.num)
    }
}

class MyClass {
    private val lateInitVal = LateInitVal()
    val text: String by lateInitVal.delegate<String>()
    val num: Int by lateInitVal.delegate<Int>()

    fun init(argStr: String, argNum: Int) {
        (::text) init argStr
        (::num) init argNum
    }

    private infix fun KProperty<*>.init(value: Any) {
        lateInitVal.initValue(this, value)
    }
}

В случае, если вы попытаетесь получить доступ к значению до его инициализации, вы получите исключение, а также при попытке переназначить новое значение.

Ответ 8

Я считаю, что такой вещи не существует "один раз". Переменные являются окончательными или нет. постоянные переменные не являются окончательными. Потому что, ну, вы переназначите это другое значение после фазы инициализации.

Если кто-то найдет решение, я играю главную роль в этом вопросе