Kotlin:lateinit 为 val,或者可以设置一次的 var

问题描述 投票:0回答:12

只是好奇:在 Kotlin 中,我希望获得一些可以通过惰性初始化但带有参数的 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 之前访问它,我肯定应该被扔掉

android kotlin immutability lazy-evaluation kotlin-lateinit
12个回答
22
投票

您可以像这样实现自己的委托:

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
     }

}

如果在初始化之前尝试访问值,以及尝试重新分配新值时,都会出现异常。


7
投票

在此解决方案中,您实现了一个自定义委托,它成为您的类上的一个单独的属性。委托内部有一个

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

5
投票

您可以使用

lazy
。例如与
TextView

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

其中

view
getView()
。 在
onCreateView()
之后,您可以使用
text
作为只读变量


3
投票

您可以像这样实现自己的委托:

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
}

2
投票

安全授权,同步且有用的消息

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

interface InitOnce<T> : ReadWriteProperty<Any?, T> {

    val isInitialized: Boolean

    val value: T

}

class SynchronizedInitOnceImpl<T> : InitOnce<T> {

    object UNINITIALIZED_VALUE

    private var name: String? = null

    @Volatile
    private var _value: Any? = UNINITIALIZED_VALUE

    override val value: T
        @Suppress("UNCHECKED_CAST")
        get() {

            val _v = synchronized(this) { _value }

            if (_v !== UNINITIALIZED_VALUE) return _v as T
            else error("'$name' not initialized yet")

        }

    override val isInitialized: Boolean
        get() = _value !== UNINITIALIZED_VALUE

    override operator fun getValue(thisRef: Any?, property: KProperty<*>): T {

        if(name == null) name = property.name

        return value

    }

    override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {

        synchronized(this) {

            val _v = _value
            if (_v !== UNINITIALIZED_VALUE) error("'${property.name}' already initialized")
            else _value = value

        }

    }

}

fun <T> initOnce(): InitOnce<T> = SynchronizedInitOnceImpl()

使用方法

var hello: String by initOnce()

1
投票

您可以像这样实现自己的委托:

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

如果您尝试在初始化之前访问值,以及尝试重新分配新值时,您将收到异常。


1
投票

你可以这样做:

var thing: Int? = null
set(value) {
    if (field == null) field = value
    else throw RuntimeException("thing is already set")
}


0
投票

如果您确实希望仅设置一次变量,则可以使用单例模式:

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
投票

对于

Activity
,可以执行以下操作:

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

对于

Fragment
,使用任何
final
类型创建
View
变量是没有意义的,因为在
onDestroyView
之后您将失去与该视图的连接。

附注使用 Kotlin 合成属性 访问

Activity
Fragment
中的视图。


0
投票

只需使用初始容量为 1 的

HashMap
(如果考虑线程,则使用 ConcurrentHashMap)和
computeIfAbsent
(只会初始化给定键的值一次)

val fooByBar = HashMap<Bar, Foo>(1)

fun getFoo(bar: Bar) = fooByBar.computeIfAbsent(bar) {
    fooFactory.create(bar)
}

try {
  bazes.forEach { baz ->
     val foo = getFoo(baz.bar) // assuming bar is same for all bazes, if not then it's still fine
     foo.quux()
  }
} catch (ex: Exception) {
    logger.error(ex.message, ex)
    return@whatever
} finally {
    fooByBar.forEach { (_, foo) ->
       foo.close() // if Foo : AutoCloseable
    }
}

0
投票

另一种需要考虑的模式(受到 Koin 的启发)

inline fun <reified V: Any> WeakHashMap<KClass<Any>, Any>.get(): V {
    val key = V::class as KClass<Any>
    val value: V? = this[key] as V?
    assert(value is V)
    return value!!
}

inline fun <reified V: Any> WeakHashMap<KClass<Any>, Any>.put(value: V) {
    val key = V::class as KClass<Any>
    this[key] = value
}


class A(val b: B, val c: C)
class B(val c: C, injectA: () -> A) {
    val a: A by lazy { injectA() }
}
class C(injectA: () -> A, injectB: () -> B) {
    val a by lazy { injectA() }
    val b by lazy { injectB() }
}


val scope = WeakHashMap<KClass<Any>, Any>()
val c = C({ scope.get<A>() }, { scope.get<B>() })
scope.put(c)
val b = B(c, { scope.get<A>() })
scope.put(b)
val a = A(b, c)
scope.put(a)

assert(a.b === b)
assert(a.c === c)
assert(b.c === c)
assert(b.a === a)
assert(c.a === a)
assert(c.b === b)

-2
投票

我相信不存在“一次初始化”这样的事情。变量要么是最终的,要么不是。 Lateinit 变量不是最终的。因为,在初始化阶段之后,您可以为其重新分配一些其他值。

如果有人找到解决方案,我会为这个问题加注星标

© www.soinside.com 2019 - 2024. All rights reserved.