reflection - usos - Kotlin: ¿Cómo se accede a los métodos get y setValue de un delegado?
tipos de delegates (2)
Me he estado preguntando cómo las propiedades delegadas ("por" -palabra clave) funcionan bajo el capó.
Obtuve eso por contrato. El delegado (lado derecho de "por") tiene que implementar un método get y setValue (...), pero ¿cómo puede garantizarlo el compilador y cómo se puede acceder a esos métodos en el tiempo de ejecución?
Mi primer pensamiento fue que, obviamente, los delegados deben implementar algún tipo de "SuperDelegado" -Interfaz, pero parece que no es el caso.
Entonces, la única opción que queda (de la que soy consciente) sería utilizar Reflection para acceder a esos métodos, posiblemente implementados en un nivel bajo dentro del lenguaje mismo. Encuentro que es algo extraño, ya que, según entiendo, sería bastante ineficiente. Además, Reflection API ni siquiera es parte de stdlib, lo que lo hace aún más extraño.
Estoy asumiendo que este último ya es (parte de) la respuesta. Así que permítanme además preguntarle lo siguiente: ¿Por qué no hay una interfaz SuperDelegate que declare los métodos getter y setter que estamos obligados a usar de todos modos? ¿No sería eso mucho más limpio?
Lo siguiente no es esencial para la pregunta
Las interfaces descritas ya están definidas en ReadOnlyProperty y ReadWriteProperty . Decidir cuál usar podría ser confiable si tenemos un val / var. O incluso omita eso ya que el compilador está evitando llamar al método setValue en val y solo usa ReadWriteProperty-Interface como SuperDelegate.
Podría decirse que cuando se requiere que un delegado implemente una determinada interfaz, la construcción sería menos flexible. Sin embargo, eso supondría que la Clase utilizada como Delegado posiblemente no esté consciente de ser utilizada como tal, lo que me parece poco probable dados los requisitos específicos para los métodos necesarios. Y si todavía insistes, aquí está una locura: ¿por qué no llegar a hacer que la clase implemente la interfaz requerida a través de la extensión (soy consciente de que no es posible a partir de ahora, pero diablos, ¿por qué no? Probablemente haya una buena ''¿por qué no?'', por favor, hágamelo saber como nota al margen).
Si miras el bytecode de Kotlin generado, verás que se crea un campo privado en la clase que contiene el delegado que estás usando, y el método get
y set
para la propiedad solo llama al método correspondiente en ese campo de delegado.
Como la clase del delegado se conoce en tiempo de compilación, no tiene que ocurrir reflexión, solo llamadas a métodos simples.
La convención de delegados ( getValue
+ setValue
) se implementa en el lado del compilador y básicamente ninguna de sus lógicas de resolución se ejecuta en tiempo de ejecución: las llamadas a los métodos correspondientes de un objeto delegado se colocan directamente en el bytecode generado.
Echemos un vistazo al bytecode generado para una clase con una propiedad delegada (puede hacerlo con la herramienta de visualización de códigos de bytes integrada en IntelliJ IDEA):
class C {
val x by lazy { 123 }
}
Podemos encontrar lo siguiente en el bytecode generado:
Este es el campo de la clase
C
que almacena la referencia al objeto delegado:// access flags 0x12 private final Lkotlin/Lazy; x$delegate
Esta es la parte del constructor (
<init>
) que inicializó el campo de delegado, pasando la función al constructorLazy
: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;
Y este es el código de
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
Puede ver la llamada al método
getValue
deLazy
que se coloca directamente en el bytecode. De hecho, el compilador resuelve el método con la firma correcta para la convención de delegado y genera el getter que llama a ese método.
Esta convención no es la única implementada en el lado del compilador: también hay iterator
, compareTo
, invoke
y otros operadores que pueden estar sobrecargados, todos son similares, pero la lógica de generación de código para ellos es más simple que la de los delegados. .
Sin embargo, tenga en cuenta que ninguno de ellos requiere la implementación de una interfaz : el operador compareTo
puede definirse para un tipo que no implementa Comparable<T>
, y el iterator()
no requiere que el tipo sea una implementación de Iterable<T>
, de todos modos, se resuelven en tiempo de compilación.
Si bien el enfoque de las interfaces podría ser más limpio que la convención de los operadores, permitiría una menor flexibilidad: por ejemplo, las funciones de extensión no se podrían usar porque no se pueden compilar en métodos que anulen los de una interfaz.