'T & Any' en Kotlin: Tipos definitivamente no nulables

Ayer tuve una sesión con un alumno de la última edición de Architect Coders, y me comentó que al implementar una interfaz genérica escrita en Java, en Kotlin le estaba obligando a poner ‘T & Any’.
Sinceramente, era la primera vez que se me cruzaba esto en Kotlin. No conocía de su existencia.
Así que aquí vengo con respuestas.
Pero si lo que quieres es aprender Kotlin a fondo y Kotlin Multiplatform, te invito a mi masterclass gratuita que puedes encontrar aquí
¿Dónde se usa T & Any y qué es?
Donde más utilidad tiene esto es en la interoperabilidad con Java.
Cuando usas un tipo T genérico en Java, y este llega a Kotlin, puedes decidir si usarlo como un tipo nulable (T?) o no nulable (T!!), y esto puede generar bastante confusión en la implementación desde Kotlin.
Especialmente cuando desde Java se han utilizado las anotaciones @NotNull
o @NonNull
, donde ya Java nos está informando de que en teoría no deberían venir valores nulos.
Tipos definitivamente no nulables
Para definir la función de tal manera que el sistema de tipos funcione correctamente, hay que informar a Kotlin de que ese tipo nunca puede ser nulable. Usar simplemente una T
no nos sirve, porque para Kotlin esto es sinónimo de T : Any?
, y por tanto, podría implicar que le llega un nulo.
Para identificar que este tipo no puede tener nulos, se usan los “Definitely non-nullable types”.
Esto identifica que el tipo hace la intersección con Any
(el no nulable, sin ?
)
Un ejemplo en Android
Imagina que quieres crear una implementación genérica de esta interfaz de Java:
public abstract static class ItemCallback<T> {
public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);
public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
}
Este interfaz nos ayuda a dar información al adapter de un RecyclerView
sobre qué valores han cambiado, para que se puedan hacer las animaciones correspondientes.
Si intentamos hacer una función genérica en Kotlin que nos provea una implementación:
inline fun <T> basicDiffUtil(
crossinline areItemsTheSame: (T, T) -> Boolean = { old, new -> old == new },
crossinline areContentsTheSame: (T, T) -> Boolean = { old, new -> old == new }
) = object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean =
areItemsTheSame(oldItem, newItem)
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean =
areContentsTheSame(oldItem, newItem)
}
Verás que esto da error. Nos indica que no se ha sobrescrito correctamente la función.
Esto es porque los valores de entrada de las funciones areItemsTheSame()
y areContentsTheSame()
están marcados con @NonNull
.
Si hacemos ctrl+click
o cmd+click
sobre el override
de cualquiera de las dos funciones, veremos esto:
¿Por qué ocurre este error realmente?
La realidad es que hemos sido muy flexibles con la definición de nuestra función. Al usar simplemente T
, estamos dejando espacio a valores no nulables:
inline fun <T> basicDiffUtil(...)
La solución más simple aquí sería identificar que el tipo solo puede ser no nulable. Esto nos lo daba la primera opción del desplegable:
inline fun <T: Any> basicDiffUtil(...)
Pero puede ser que nosotros sí que queramos permitir tipos nulables dentro de la función, pero la interfaz que hemos implementado no nos va a devolver nunca nulos. Entonces tendríamos que tomar la segunda solución del desplegable, y aquí es donde entra en juego T & Any
:
override fun areItemsTheSame(oldItem: T & Any, newItem: T & Any): Boolean =
areItemsTheSame(oldItem, newItem)
override fun areContentsTheSame(oldItem: T & Any, newItem: T & Any): Boolean =
areContentsTheSame(oldItem, newItem)
Ahora otras partes de nuestra función podrían usar valores nulos, pero lo que está claro que es que oldItem
y newItem
nunca podrán serlo.
Para esta función en particular, tendría más sentido la primera opción, ya que no entra en nuestros planes que nada de ella pueda ser nulo.
Conclusión
Cuando estamos usando tipos genéricos nulables en Kotlin que involucran el uso de alguna funcionalidad tipada en Java, y esta función está marcada con @NonNull
, T & Any
informará al compilador de que ese tipo para esa parte del código no puede ser nulable.
Hay pocas situaciones en las que esto hace falta, y por eso no se me había dado hasta ahora, pero conviene tenerlo en cuenta por si surge la necesidad.
En cualquier caso, las propias opciones de Android Studio te ayudarán a completarlo.
Pero ahora ya sabes de dónde viene.
Cómo conseguir la localización amplia en Android
Cómo pedir permisos en Jetpack Compose