Mis 4 casos de uso principales para las clases en línea de Kotlin | de Simon Wirtz | septiembre 2022

Contenidos

Aprenda a usar la palabra clave de valor para crear clases en línea y aplicarlas en 4 escenarios diferentes

Kotlin introdujo clases en línea con la versión 1.3 como característica experimental. Mucho ha cambiado mientras tanto. Kotlin cambió la palabra clave en línea original a valor en su lugar. Además, debe agregar una anotación a su clase de valor en la JVM para que funcione como se esperaba. La terminología sigue siendo válida, por lo que este artículo se refiere a «clases en línea» aunque las palabras clave se nombran de forma ligeramente diferente. Las clases en línea agregan una herramienta simple que nos permite agregar un contenedor alrededor de otro tipo sin aumentar la sobrecarga del tiempo de ejecución con asignaciones de almacenamiento dinámico adicionales. En este artículo, veamos cómo funcionan las clases en línea en Kotlin y cuándo tiene sentido usarlas. Al final de este artículo, analizaré 4 escenarios diferentes que se benefician de esta construcción. Las clases en línea no son muy complicadas para comenzar. De hecho, simplemente agrega la palabra clave value a su clase y aplica la anotación @JvmInline: las clases en línea deben especificar exactamente una propiedad en el constructor principal, como se muestra con value. No puede envolver múltiples valores en una clase en línea. Además, no puede tener propiedades con campos compatibles y no se admite la delegación de propiedades. Sin embargo, las clases en línea pueden tener propiedades computables simples, que veremos más adelante en este artículo. En tiempo de ejecución, el tipo envuelto de una clase en línea se usa sin su contenedor siempre que sea posible. Mirando el ejemplo anterior, esto significa que el compilador intentará usar el valor: Int siempre que pueda. Esto es similar a los tipos en caja de Java como Integer o Boolean, que se representan como su tipo primitivo correspondiente cada vez que el compilador puede hacerlo. Este es precisamente el gran punto de venta de las clases en línea en Kotlin: cuando crea una clase en línea, la clase en sí no se usa en el código de bytes a menos que sea absolutamente necesario. Las clases integradas reducen drásticamente la sobrecarga de memoria en tiempo de ejecución. En tiempo de ejecución, una clase en línea se puede representar como un tipo contenedor y un tipo subyacente. Como se mencionó en el párrafo anterior, el compilador prefiere usar el tipo subyacente (envuelto) de una clase en línea para optimizar el código tanto como sea posible. Esto es similar al boxeo entre int y integer. Sin embargo, en ciertas situaciones, el compilador necesita usar el propio contenedor, por lo que se genera durante la compilación: https://gist.github.com/53e05dbebb1d5d95c14d285fbc20d187 Este fragmento muestra el código de bytes simplificado representado como código Java para mostrar cómo se ve una clase en línea . Junto con algunas cosas obvias como el campo de valor y su captador, el constructor es privado y, en cambio, se crean nuevos objetos a través de constructor_impl que en realidad no usa el tipo contenedor sino que solo devuelve el tipo subyacente pasado. Finalmente, puede ver las funciones box_impl y unbox_impl utilizadas para el boxeo. Ahora veamos cómo usar este contenedor de clase en línea cuando usamos la clase en línea en nuestro código. En este fragmento, creamos unWrappedInt y lo pasamos a una función que devuelve su valor envuelto. El código de bytes correspondiente, de nuevo como código Java, se ve así: WrappedInt no se instancia en el código compilado. Aunque se usa el constructor_impl estático, solo devuelve un int, que luego se pasa a la función de toma, que tampoco sabe nada sobre el tipo de la clase en línea que originalmente teníamos en nuestro código fuente. Tenga en cuenta que las funciones que aceptan parámetros de clase en línea tienen sus nombres expandidos con un código hash bytecode generado. Esto los mantiene distinguibles de las funciones sobrecargadas que toman el tipo subyacente como parámetro: para exponer ambos métodos de toma en el código de bytes de JVM y evitar colisiones de firmas, el compilador cambia el nombre del primero a algo como tomar-wIOJKEE. Esta técnica se llama mutilación. Tenga en cuenta que la representación del código de bytes de Java anterior muestra un «_» en lugar de un «-» porque Java no permite que los nombres de los métodos contengan el guión. Todavía puede llamar a estas funciones desde Java, pero tiene una peculiaridad; Debe nombrar explícitamente la función, deshabilitando así la manipulación automática: vimos anteriormente que las funciones box_impl y unbox_impl se crean para clases en línea. Entonces, ¿cuándo los necesitamos? Los documentos de Kotlin citan una regla general que establece:

Las clases en línea se enmarcan cuando se usan como un tipo diferente.

Por ejemplo, el boxeo ocurre cuando usa su clase en línea como un tipo genérico o anulable: en este código, modificamos la función de toma para aceptar un WrappedInt anulable y devolver el tipo subyacente si el argumento no es nulo. take ya no acepta directamente el tipo subyacente. En su lugar, debe funcionar con el tipo contenedor. Cuando se imprime el contenido, se llama a unbox_impl. En el sitio de la persona que llama, podemos ver que box_impl se usa para crear una instancia envuelta de WrappedInt. Debería ser obvio que queremos evitar el boxeo siempre que sea posible. Tenga en cuenta que ciertos usos de clases en línea, y también tipos primitivos en general, se basan en esta técnica y es posible que deban reconsiderarse para evitar asignaciones de almacenamiento dinámico. Pero, ¿cuándo queremos usar tipos de envoltura? Imagine un método de autenticación en una API que se vea así: Por supuesto, si fuéramos ingenuos, podríamos pensar que cada cliente está pasando aquí valores razonables, es decir, un nombre de usuario y la contraseña. Sin embargo, no es demasiado descabellado suponer que ciertos usuarios llamarán a este método de manera diferente: auth(«12345», «usuario1») Dado que ambos parámetros son del tipo Cadena, puede estropear su orden, lo que se vuelve más probable a medida que el número aumenta de argumentos. Los contenedores alrededor de estos tipos pueden ayudarlo a mitigar este riesgo, por lo que las clases en línea son una gran herramienta: la lista de parámetros se ha vuelto menos confusa y, en el lado de la persona que llama, el compilador no permite ninguna desviación. Las clases en línea nos brindan contenedores simples y con seguridad de tipo sin introducir asignaciones de almacenamiento dinámico adicionales. En estas situaciones, se deben preferir las clases en línea siempre que sea posible. No obstante, las clases en línea pueden ser aún más inteligentes, como lo demuestra el siguiente caso de uso. Consideremos un método que toma una cadena numérica y la analiza en BigDecimal mientras también ajusta su escala: el código es bastante simple y funcionaría bien, pero un requisito podría ser que necesite realizar un seguimiento de la cadena original utilizada para analizar el se utilizó el número. Para resolver esto, puede crear un tipo de contenedor o simplemente usar la clase Pair existente para devolver un par de valores de esta función. Estos enfoques serían válidos, aunque obviamente asignan memoria adicional, lo que debe evitarse en ciertas situaciones. Las clases en línea pueden ayudarte con esto. Ya hemos establecido que las clases en línea no pueden tener múltiples propiedades con campos de fondo. Sin embargo, puede tener miembros calculados simples en forma de propiedades y funciones. Podemos crear una clase en línea para nuestro caso de uso que envolverá la cadena original y proporcionará un método o propiedad que analizará nuestro valor cuando sea necesario. Para el usuario, esto parece un contenedor de datos normal en torno a dos tipos, mientras que en el mejor de los casos no agrega sobrecarga de tiempo de ejecución: como puede ver, la función getParsableNumber devuelve una instancia de nuestra clase en línea que tiene dos propiedades originales (el tipo subyacente) y analizado (el número analizado calculado). Este es un caso de uso interesante que vale la pena ver nuevamente a nivel de código de bytes:

Más código de bytes

La clase contenedora ParsableNumber generada se parece bastante a la clase WrappedInt que se mostró anteriormente. Sin embargo, una diferencia importante es la función getParsed_impl, que representa nuestra propiedad computable analizada. Como puede ver, la función se implementa como una función estática que toma una cadena y devuelve BigDecimal. Entonces, ¿cómo se usa esto en el código de la persona que llama? Como era de esperar, getParsableNumber no tiene ninguna referencia a nuestro tipo de contenedor. Simplemente devuelve la cadena sin introducir un nuevo tipo. Esencialmente, vemos que el getParsed_impl estático se usa para analizar la cadena dada en un BigDecimal. Nuevamente, no se usa ParsableNumber. Un problema común con las funciones de extensión es que pueden contaminar su espacio de nombres cuando se definen en tipos generales como String. Por ejemplo, es posible que desee tener una función de extensión que convierta una cadena JSON en un tipo apropiado: Para convertir una cadena específica en un contenedor de datos JsonData, haría lo siguiente: Sin embargo, la función de extensión está disponible para cadenas que otros también representar datos, aunque podría no tener mucho sentido: «lo que sea».asJson // falla con error. Este código falla porque la cadena no contiene datos JSON válidos. ¿Qué podemos hacer para que la extensión que se muestra arriba solo esté disponible para cadenas específicas? Sí, las clases en línea pueden ayudar con esto:

Limite el alcance de la extensión con la clase en línea

Si introducimos un contenedor para cadenas que contienen datos JSON y cambiamos la extensión para usar un receptor JsonString en consecuencia, el problema descrito anteriormente se resolvió. La extensión ya no aparecerá en ningún String y, en su lugar, solo extenderá aquellos que envolvimos deliberadamente en un JsonString. Como puede ver, la clase UInt se define como una clase sin firmar que envuelve datos enteros regulares con signo. Puede obtener más información sobre esta característica en las clases KEEP relevantes. Las clases en línea son una gran herramienta que podemos usar para reducir las asignaciones de almacenamiento dinámico para los tipos de contenedores y ayudarnos a resolver diferentes tipos de problemas. Tenga en cuenta, sin embargo, que ciertos escenarios, como el uso de clases en línea como tipos que aceptan valores NULL, requieren boxeo. Aún así, es bueno conocer esta pequeña pero poderosa herramienta y tenerla en cuenta la próxima vez que se encuentre con uno de los casos de uso discutidos.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.