可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Just curious: In Kotlin, I would love to get some val that can be initialized by lazy, but with a parameter. That's because I need something that's created very late in order to initialize it.
Specifically, I wish I had:
private lateinit val controlObj:SomeView
or:
private val controlObj:SomeView by lazy { view:View->view.findViewById(...)}
and then:
override fun onCreateView(....) {
val view = inflate(....)
controlObj = view.findViewById(...)
or in the 2nd case controlObj.initWith(view)
or something like that:
return view
I cannot use by lazy
because by lazy
won't accept external parameters to be used when initialising. In this example - the containing view
.
Of course I have lateinit var
but it would be nice if I could make sure it becomes read only after setting and I could do it in one line.
Is there a pretty clean way to create a read only variable that initializes only once but only when some other variables are born? Any init once
keyword? That after init the compiler knows it's immutable?
I am aware of the potential concurrency issues here but if I dare to access it before init, I surely deserve to be thrown.
回答1:
You can implement own delegate like this:
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
}
}
After that you can use it as following:
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
}
}
In case when you try to access value before it is initialized you will get exception as well as when you try to reassign new value.
回答2:
In this solution you implement a custom delegate and it becomes a separate property on your class. The delegate has a var
inside, but the controlObj
property has the guarantees you want.
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 if you need thread safety, the solution is just slightly different:
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:
You can use lazy
. For example with TextView
val text by lazy<TextView?>{view?.findViewById(R.id.text_view)}
where view
is getView()
.
And after onCreateView()
you can use text
as read only variable
回答4:
You can implement own delegate like this:
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)
}
After that you can use it as following:
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()
}
In case when you try to access value before it initialized you will get exception as well as when you try to reassign new value.
also,you can write DSL to make it readable.
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)
and then call it like this:
fun setHat(h: Hat) {
h assigned to property ::hat of this
}
回答5:
You can implement own delegate like this:
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")
}
}
}
After that you can use it as following:
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)
}
}
In case when you try to access value before it initialized you will get exception as well as when you try to reassign new value.
回答6:
If you really want a variable to be set only once you can use a singleton pattern:
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)
}
Then all you have to do is call getInstance(...)
and you will always get the same object.
If you want to bind the lifetime of the object to the surrounding object just drop the companion object and put the initializer in your class.
Also the synchronized block takes care of the concurrency issues.
回答7:
For Activity
it is OK to do follow:
private val textView: TextView by lazy { findViewById<TextView>(R.id.textView) }
For Fragment
it is not makes sense to create final
variable with any View
type, because you will lost connection with that view after onDestroyView
.
P.S. Use Kotlin synthetic properties to access views in Activity
and Fragment
.
回答8:
I believe there is no such thing "init once". Variables are either final or not.
lateinit variables are not final. Because, well, you reassign it some other value after initialization phase.
If someone finds the solution, I'm starring this question