Kotlin: Как получить доступ к методам доступа для делегатов и setValue?


Мне было интересно, как делегированные свойства ( "by -Keyword" ) работают под капотом.
Я получаю, что по контракту делегат (правая сторона "by" ) должен реализовать метод get и setValue (...), но как это может обеспечить компилятор и как эти методы могут быть доступны во время выполнения?
Моя первоначальная мысль заключалась в том, что, очевидно, делегаты должны мне внедрить какой-то "SuperDelegate" -Interface, но, похоже, это не так.
Таким образом, единственный вариант, который я знаю, - это использовать Reflection для доступа к этим методам, возможно, реализованным на низком уровне внутри самого языка. Я считаю это несколько странным, поскольку, по моему мнению, это будет довольно неэффективно. Также API Reflection не является даже частью stdlib, что делает его еще более странным.

Я предполагаю, что последний уже является (частью) ответа. Поэтому позвольте мне также задать вам следующее: почему нет интерфейса SuperDelegate, который объявляет методы getter и setter, которые мы вынуждены использовать в любом случае? Разве это не было бы намного чище?

В вопросе

не имеет значения следующее:

Описанный интерфейс уже определен в ReadOnlyProperty и ReadWriteProperty. Чтобы решить, какой из них использовать, можно было бы сделать зависимым от того, есть ли у нас val/var. Или даже опустите это, поскольку вызов метода setValue в val предотвращается компилятором и использует только интерфейс ReadWriteProperty как SuperDelegate.

Возможно, когда требуется, чтобы делегат реализовал определенный интерфейс, конструкция была бы менее гибкой. Хотя это предполагает, что класс, используемый в качестве делегата, возможно, не осознает того, что он используется как таковой, что я считаю маловероятным с учетом конкретных требований к необходимым методам. И если вы все еще настаиваете, вот сумасшедшая мысль: почему бы даже не зайти так далеко, чтобы этот класс реализовал требуемый интерфейс через Extension ( Я знаю, что это невозможно сейчас, но, черт возьми, почему бы и нет? Вероятно, там хороший "почему бы нет", пожалуйста, дайте мне знать как примечание).

Ответ 1

Соглашение делегатов (getValue + setValue) реализовано со стороны компилятора, и в действительности во время выполнения не выполняется ни одна из его логики разрешения: вызовы соответствующих методов объекта-делегата помещаются непосредственно в сгенерированный байт-код,

Посмотрим на байт-код, сгенерированный для класса с делегированным свойством (вы можете сделать это с помощью инструмента просмотра байт-кода, встроенного в IntelliJ IDEA):

class C {
    val x by lazy { 123 }
}

В сгенерированном байт-коде мы можем найти следующее:

  • Это поле класса C, в котором хранится ссылка на объект делегата:

    // access flags 0x12
    private final Lkotlin/Lazy; x$delegate
    
  • Это часть конструктора (<init>), которая инициализировала поле делегата, передавая функцию конструктору Lazy:

    ALOAD 0
    GETSTATIC C$x$2.INSTANCE : LC$x$2;
    CHECKCAST kotlin/jvm/functions/Function0
    INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
    PUTFIELD C.x$delegate : Lkotlin/Lazy;
    
  • И это код getX():

    L0
      ALOAD 0
      GETFIELD C.x$delegate : Lkotlin/Lazy;
      ASTORE 1
      ALOAD 0
      ASTORE 2
      GETSTATIC C.$$delegatedProperties : [Lkotlin/reflect/KProperty;
      ICONST_0
      AALOAD
      ASTORE 3
    L1
      ALOAD 1
      INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object;
    L2
      CHECKCAST java/lang/Number
      INVOKEVIRTUAL java/lang/Number.intValue ()I
      IRETURN
    

    Вы можете увидеть вызов метода getValue Lazy, который помещается непосредственно в байт-код. Фактически, компилятор разрешает метод с правильной сигнатурой для соглашения делегата и генерирует getter, который вызывает этот метод.

Это соглашение не единственное, реализованное на стороне компилятора: есть также iterator, compareTo, invoke и другие операторы, которые могут быть перегружены - все они схожи, но генерация кода логика для них проще, чем у делегатов.

Обратите внимание, однако, что ни один из них не требует реализации интерфейса: оператор compareTo может быть определен для типа, не реализующего Comparable<T>, а iterator() не требует тип будет реализацией Iterable<T>, они в любом случае разрешены во время компиляции.

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

Ответ 2

Если вы посмотрите на сгенерированный байт-код Kotlin, вы увидите, что в классе, в котором используется делегат, создается приватное поле, а метод get и set для свойства просто вызывает соответствующий метод в этом поле делегата.

Поскольку класс делегата известен во время компиляции, никакого отражения не должно быть, просто вызовы метода.