Расширение полей в Котлине

Легко писать методы расширения в Котлине:

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

Но есть ли способ создать переменную расширения? Как:

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

Ответы

Ответ 1

Нет - документация объясняет это:

Расширения фактически не изменяют классы, которые они распространяют. Определяя расширение, вы не вставляете новые члены в класс, а просто делаете новые функции вызываемыми с помощью точечной нотации для экземпляров этого класса.

и

Обратите внимание, что, поскольку расширения фактически не вставляют элементы в классы, нет эффективного способа для свойства расширения иметь фоновое поле. Вот почему инициализаторы не допускаются для свойств расширения. Их поведение может быть определено только путем явного предоставления getters/setters.

Мысль о функциях/свойствах расширения как просто синтаксического сахара для вызова статической функции и передачи в ценности, надеюсь, делает это ясным.

Ответ 2

Вы можете создать свойство расширения с переопределенным getter и setter:

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

Но вы не можете создать свойство расширения с помощью поля поддержки, потому что вы не можете добавить поле в существующий класс.

Ответ 3

не добавлять свойства расширения с полями поддержки в классы, поскольку расширения фактически не изменяют класс.

Вы можете определить свойство расширения с помощью настраиваемого getter (и setter для var) или делегированное свойство.


Однако, если вам нужно определить свойство расширения, которое будет вести себя так, как если бы оно имело поле поддержки, делегированные свойства пригождаются. Идея состоит в том, чтобы создать делегат свойств, который будет хранить сопоставление объектов со значением:
  • используя идентификатор, а не equals()/hashCode(), чтобы фактически хранить значения для каждого объекта, например IdentityHashMap does;

  • не препятствует сбору объектов-объектов (используя слабые ссылки), например WeakHashMap.

К сожалению, в JDK нет WeakIdentityHashMap, поэтому вам нужно реализовать свою собственную (или выполнить полную реализацию).

Затем на основе этого сопоставления вы можете создать класс делегата, удовлетворяющий требованиям делегатов . Вот пример небезопасной реализации:

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

Пример использования:

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
}

При определении внутри класса сопоставление может храниться независимо для экземпляров класса или совместно используемого объекта делегирования:

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
}

Также обратите внимание, что идентификация не гарантируется для примитивных типов Java из-за бокса.

И я подозреваю, что производительность этого решения значительно хуже, чем у обычных полей, скорее всего, близких к нормальным Map, но это требует дальнейшего тестирования.

Для поддержки свойств NULL и потокобезопасной реализации см. здесь.

Ответ 4

Вы не можете добавить поле, но вы можете добавить свойство, которое делегирует другим свойствам/методам объекта для реализации своих аксессуаров. Например, предположим, что вы хотите добавить свойство secondsSinceEpoch в класс java.util.Date, вы можете написать

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