web-dev-qa-db-fra.com

Kotlin: lateinit to val, ou, alternativement, un var qui peut prendre une fois

Juste curieux: à Kotlin, j'aimerais bien avoir un val qui peut être initialisé par paresseux, mais avec un paramètre. C'est parce que j'ai besoin de quelque chose qui a été créé très tard pour pouvoir l'initialiser.

Plus précisément, j'aimerais avoir:

private lateinit val controlObj:SomeView

ou:

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

et alors:

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


    controlObj = view.findViewById(...)

ou dans le 2e cas controlObj.initWith(view) ou quelque chose comme ça:

return view

Je ne peux pas utiliser by lazy car by lazy n'acceptera pas l'utilisation de paramètres externes lors de l'initialisation. Dans cet exemple, le contenant view.

Bien sûr, j'ai lateinit var mais ce serait bien si je pouvais m'assurer qu'il ne sera lu que après le réglage et que je pourrais le faire en une seule ligne.

Existe-t-il un moyen simple de créer une variable en lecture seule qui ne s'initialise qu'une fois, mais uniquement lorsque d'autres variables sont nées? Un mot clé init once? Qu'après l'init, le compilateur sait que c'est immuable? 

Je suis conscient des problèmes de simultanéité potentiels ici, mais si j'ose y accéder avant de commencer, je mérite sûrement d’être jeté .

14
Maneki Neko

Vous pouvez implémenter votre propre délégué comme ceci:

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

Après cela, vous pouvez l'utiliser comme suit:

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
     }

}

Si vous essayez d’accéder à une valeur avant son initialisation, vous obtiendrez une exception ainsi que lorsque vous essayez de réaffecter une nouvelle valeur.

3
hluhovskyi

Dans cette solution, vous implémentez un délégué personnalisé qui devient une propriété distincte de votre classe. Le délégué a une var à l'intérieur, mais la propriété controlObj a les garanties souhaitées.

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

BTW si vous avez besoin de sécurité du fil, la solution est légèrement différente:

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

Vous pouvez implémenter votre propre délégué comme ceci:

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

Après cela, vous pouvez l'utiliser comme suit:

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

Si vous essayez d’accéder à une valeur avant son initialisation, vous obtiendrez une exception ainsi que lorsque vous essayez de réaffecter une nouvelle valeur.

de plus, vous pouvez écrire DSL pour le rendre lisible.

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)

puis appelez ça comme ça:

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

Vous pouvez utiliser lazy. Par exemple avec TextView

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

view est getView(). Et après onCreateView(), vous pouvez utiliser text comme variable en lecture seule

1
Stanislav Bondar

Vous pouvez implémenter votre propre délégué comme ceci:

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

    }
}

Après cela, vous pouvez l'utiliser comme suit:

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

Si vous essayez d’accéder à une valeur avant son initialisation, vous obtiendrez une exception ainsi que lorsque vous essayez de réaffecter une nouvelle valeur.

0
sunhang

Pour Activity il est possible de suivre:

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

Pour Fragment, il n'est pas logique de créer une variable final avec un type View car vous perdrez la connexion avec cette vue après onDestroyView.

P.S. Utilisez Propriétés synthétiques Kotlin pour accéder aux vues dans Activity et Fragment.

0

Si vous voulez vraiment qu'une variable ne soit définie qu'une seule fois, vous pouvez utiliser un modèle singleton:

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

Ensuite, tout ce que vous avez à faire est d'appeler getInstance(...) et vous obtiendrez toujours le même objet. 

Si vous souhaitez lier la durée de vie de l'objet à l'objet environnant, supprimez simplement l'objet compagnon et placez l'initialiseur dans votre classe. 

Le bloc synchronisé prend également en charge les problèmes de simultanéité.

0
leonardkraemer