it-roy-ru.com

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

Просто любопытно: в Kotlin мне бы хотелось получить некоторое значение val, которое можно инициализировать с помощью lazy, но с параметром. Это потому, что мне нужно что-то, что создано очень поздно, чтобы инициализировать это.

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

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, я, безусловно, заслуживаю быть брошенным .

14
Maneki Neko

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

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

}

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

3
hluhovskyi

В этом решении вы реализуете собственный делегат, и он становится отдельным свойством в вашем классе. Внутри делегата есть 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")
}
4
Marko Topolnik

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

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

object LateVal {
    fun bindValue(any: Any, propertyName: String, value: Any) {
        var map = maps[any]
        if (map == null) {
            map = mutableMapOf<String, Any>()
            maps[any] = map
        }

        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
}
1
sunhang

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

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

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

1
Stanislav Bondar

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

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

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

0
sunhang

Для Activity можно выполнить следующее:

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

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

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

0
Constantin Chernishov

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

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(...), и вы всегда получите один и тот же объект. 

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

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

0
leonardkraemer