代码之家  ›  专栏  ›  技术社区  ›  Maneki Neko

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

  •  37
  • Maneki Neko  · 技术社区  · 7 年前

    只是好奇:在Kotlin中,我很想得到一些可以由lazy初始化的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 因为 通过懒惰 初始化时不接受要使用的外部参数。在本例中,包含 view .

    我当然有 lateinit var 但如果我能确保它在设置后变成只读,并且我可以在一行中完成,那就太好了。

    有没有一种非常干净的方法来创建只读变量,该变量只初始化一次,但只有在生成其他变量时才初始化?任何 init once 关键字?在初始化之后,编译器知道它是不可变的?

    我知道这里存在潜在的并发问题,但如果我敢在init之前访问它,我当然应该被抛出 .

    10 回复  |  直到 7 年前
        1
  •  17
  •   akafred hluhovskyi    6 年前

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

    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
  •  6
  •   Marko Topolnik    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")
    }
    
        3
  •  5
  •   Stanislav Bondar    7 年前

    您可以使用 lazy . 例如,使用 TextView

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

    哪里 view getView() . 以及之后 onCreateView() 您可以使用 text 作为只读变量

        4
  •  3
  •   sunhang    6 年前

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

    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
  •  2
  •   Alireza Ghasemi    5 年前

    安全委派、同步和帮助信息

    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()
    
        6
  •  1
  •   sunhang    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)
        }
    }
    

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

        7
  •  0
  •   leonardkraemer    7 年前

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

    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(...) 你总是会得到相同的物体。

    如果要将对象的生存期绑定到周围的对象,只需删除伴生对象并将初始值设定项放入类中即可。

    同步块还负责处理并发问题。

        8
  •  0
  •   assylias    7 年前

    对于 Activity 可以执行以下操作:

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

    对于 Fragment 创造是没有意义的 final 带任意变量 View 键入,因为在 onDestroyView .

    P、 美国使用 Kotlin synthetic properties 在中访问视图的步骤 活动 碎片 .

        9
  •  0
  •   Novaterata    4 年前

    只需使用 HashMap 初始容量为1(如果涉及线程,则为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
        }
    }
    
        10
  •  -3
  •   Rezo Shalikashvili    7 年前

    我相信没有“一次初始化”这样的事情。变量是否为最终变量。 lateinit变量不是最终变量。因为,在初始化阶段之后,您会重新为其分配一些其他值。

    如果有人找到了解决方案,我会把这个问题放在首位