Extension fields in Kotlin

2020-02-08 10:11发布

问题:

It's easy to write extension methods in Kotlin:

class A { }
class B {
    fun A.newFunction() { ... }
}

But is there some way to create extension variable? Like:

class B {
    var A.someCounter: Int = 0
}

回答1:

No - the documentation explains this:

Extensions do not actually modify classes they extend. By defining an extension, you do not insert new members into a class, but merely make new functions callable with the dot-notation on instances of this class.

and

Note that, since extensions do not actually insert members into classes, there’s no efficient way for an extension property to have a backing field. This is why initializers are not allowed for extension properties. Their behavior can only be defined by explicitly providing getters/setters.

Thinking about extension functions/properties as just syntactic sugar for calling a static function and passing in a value hopefully makes this clear.



回答2:

You can create an extension property with overridden getter and setter:

var A.someProperty: Int
  get() = /* return something */
  set(value) { /* do something */ }

But you cannot create an extension property with a backing field because you cannot add a field to an existing class.



回答3:

There's no way to add extension properties with backing fields to classes, because extensions do not actually modify a class.

You can only define an extension property with custom getter (and setter for var) or a delegated property.


However, if you need to define an extension property which would behave as if it had a backing field, delegated properties come in handy. The idea is to create a property delegate that would store the object-to-value mapping:

  • using the identity, not equals()/hashCode(), to actually store values for each object, like IdentityHashMap does;

  • not preventing the key objects from being garbage collected (using weak references), like WeakHashMap does.

Unfortunately, there is no WeakIdentityHashMap in JDK, so you have to implement your own (or take a complete implementation).

Then, based on this mapping you can create a delegate class satisfying the property delegates requirements. Here's an example non-thread-safe implementation:

class FieldProperty<R, T : Any>(
    val initializer: (R) -> T = { throw IllegalStateException("Not initialized.") }
) {    
    private val map = WeakIdentityHashMap<R, T>()

    operator fun getValue(thisRef: R, property: KProperty<*>): T =
            map[thisRef] ?: setValue(thisRef, property, initializer(thisRef))

    operator fun setValue(thisRef: R, property: KProperty<*>, value: T): T {
        map[thisRef] = value
        return value
    }
}

Usage example:

var Int.tag: String by FieldProperty { "$it" }

fun main(args: Array<String>) {
    val x = 0
    println(x.tag) // 0

    val z = 1
    println(z.tag) // 1
    x.tag = "my tag"
    z.tag = x.tag
    println(z.tag) // my tag
}

When defined inside a class, the mapping can be stored independently for instances of the class or in a shared delegate object:

private val bATag = FieldProperty<Int, String> { "$it" }

class B() {
    var A.someCounter: Int by FieldProperty { 0 } // independent for each instance of B
    var A.tag: String by bATag // shared between the instances, but usable only inside B
}

Also, please note that identity is not guaranteed for Java's primitive types due to boxing.

And I suspect the performance of this solution to be significantly worse than that of regular fields, most probably close to normal Map, but that needs further testing.

For nullable properties support and thread-safe implementation please refer to here.



回答4:

You can't add a field, but you can add a property, that delegates to other properties/methods of the object to implement its accessor(s). For example suppose you want to add a secondsSinceEpoch property to the java.util.Date class, you can write

var Date.secondsSinceEpoch: Long 
    get() = this.time / 1000
    set(value) {
        this.time = value * 1000
    }


标签: kotlin