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
Я считаю, что такой вещи не существует "один раз". Переменные являются окончательными или нет. постоянные переменные не являются окончательными. Потому что, ну, вы переназначите это другое значение после фазы инициализации.
Если кто-то найдет решение, я играю главную роль в этом вопросе