Kotlin et permet estendre una classe amb noves funcions sense tenir que modificar la classe o crear una classe nova que estengui la classe a la cual vols afegir una nova funcionalitat.

Introducció

Kotlin et permet "afegir" funcions a una classe sense tenir que modificar la classe o crear una classe nova que extengui la classe a la cual vols afegir una nova funcionalitat.

D'aquesta manera podem adaptar classes que no pertanyen al nostre codi a les necessitats específiques del nostre projecte sense tenir que modificar la classe corresponent, i el que és més important, de tal manera que aquestes funcions es puguin executar com si fossin part de l'API de la classe.

Això és molt important perquè et permet crear una classe sense tenir-la que omplir de funcions que potser mai es faran servir.

Flexibilitat

És molt habitual troba una classe Circle com la que es mostra a continuació:

class Circle(val radius: Double) {
    fun area(): Double {
        return Math.PI * radius * radius
    }
}

Qui va dissenyar la classe va pensar que era important l'àrea d'un cercle, però res més.

Si pel teu projecte és important el perimetre d'un cercle tu has d'escriure una funció que computi el perimetre:

class Circle(val radius: Double) {
    fun area(): Double {
        return Math.PI * radius * radius
    }
}

fun Circle_perimeter(circle: Circle): Double {
    return 2 * Math.PI * circle.radius
}

fun main() {
    val circle = new Circle(2.5)
    println("Area is ${circle.area()}")
    println("Perimeter is ${Circle_perimeter(circle)}")
}

Pots veure que la funció Circle_perimeter només funciona amb objectes de la classe Circle, però no és un mètode de la classe Circle.

Kotlin et permet crear una funció com una extensió d'una classe per tal que sintàcticament no hi hagi diferència.

Per tant, afegeix la funció perimeter() a la classe Circle com una funció d'extensió tal com es mostra a continuació:

class Circle(val radius: Double) {
    fun area(): Double {
        return Math.PI * radius * radius
    }
}

fun Circle.perimeter(): Double {
    return 2 * Math.PI * radius
}

fun main() {
    val circle = new Circle(2.5)
    println("Area is ${circle.area()}")
    println("Perimeter is ${circle.perimeter()}")
}

D'aquesta manera pots invocar la funció perimeter() com si l'hagués declarat la classe Circle.

El que hem fet és:

  1. Afegir el prefix Circle. al nom de la funció.

  2. Eliminar l'argument circlede la funció ja que les propietats públiques de la classe Circle es poden accedir amb una sintaxi igual que si la funció fos un mètode de la classe.

A més que el resultat és més fàcil de llegir, les IDEs poden oferir la funció mitjançant la funció d'autocompletar com si fos un mètode estàndar de la classe Circle.

Accessibilitat

Encara que Kotlin ens permet escriure una funció d'extensió com si fos un mètode de la classe en veritat no és un mètode, sinó una funció externa que només pot accedir a les propietats de la classe que es poden accedir desde fora de la classe.

Aquest codi no és compila directament:

val circle = new Circle(3)
circle.perimeter()

Sinò que abans es transforma en:

val circle = new Circle(3)
Circle.perimeter(circle)

Per tant, pots veure a efectes pràctiques perqué les funcions d'extensió només tenene accés a les propietats públiques de l'objecte.

Extensibilitat

Un dels principals errors en programació és que per les limitacions del llenguatge (no és del cas de Kotlin o Scala), tens que definir les funcions aplicables en un objecte en la mateixa declaració de l'objecte quant aquestes són específiques de cada ús de l'objecte i no característiques pròpies de l'objecte.

I com que suposo que no has acabat d'entendere el que acabo de dir, millor un exemple clar i directe.

Aquesta és una classe Circle ben escrita:

class Circle(val radius: Double)

Però no té cap mètode?

Correcte: Un cercle es caracteritza per un radi i punt, i el seu valor és inmutable.

Aquesta classe està dissenyda perquè jo o qualsevol altre la pugui extendre amb les funcions que es necessitin en cada cas concret.

Com autor de la classes Circle també puc afegir la funció area com una funció d'extensió, no com un mètode.

class Circle(val radius: Double)
    
fun Circle.area(): Double {
    return Math.PI * radius * radius
}

fun main() {
    val circle = new Circle(2.5)
    println("Area is ${circle.area()}")
}

Tampoc hi ha massa diferència, potser la funció area() ... NO!

Si una classe està dissenyada per ser extensible tu has de ser el primer en tractar-la com a tal.

Inmutabilitat

Un dels principals errors en programació és crear objecetes mutables quan aquests haurien de ser inmutables.

Per exemple, aquesta classe està mal dissenyada:

class Cicle(var radius: Double) {
    fun add(value: Double) {
        this.radius += value
    }
}

Entre altres coses perquè a qui l'interessa aquest mètode? A molt pocs.

I més important, estem dient que un cercle pot variar de tamany en qualsevol moment sense que la nostre secció de codi intervingui, si el codi s'executa en un entorn concurrent.

Que les "figures" tinguin estat intern només té sentit en una intefície gràfica, o un disseny 3D, que per això es van crear els objectes amb estat intern mutable, i per això les interfícies gràfiques només es poden executar un sol fil de processament.

Pel reste dels casos, les "figures" són inmutables.

Si vol un cercle més gran et crees un nou cercle més gran amb una funció d'extensió:

class Cicle(val radius: Double)

fun Circle.add(value: Double): Circle {
    return Circle(this.radius + value)
}

Llibreries

Kotlin et permet ampliar qualsevol classe definida en una llibreria Java o Kotlin.

Aquí tens un exemple en que afegim una nova funció abs() a la classe Int de la llibreria estàndard de Kotlin:

fun main(){
 
    // Extension function defined for Int type
    fun Int.abs() : Int{
        return if (this < 0) -this else this
    }
 
    println((-4).abs())
    println(4.abs())
}

Resolució estàtica

Un punt important a tenir en compte sobre les funcions d'extensió és que es resolen de forma estàtica.

A continuació tens un exemple en que la classe B hereta de la classe A.

open class A(val a:Int, val b:Int) {}
 
class B(): A(5, 5) {}
 
fun main(){
     
    fun A.operate(): Int {
        return a + b
    }
 
    fun B.operate(): Int {
        return a * b
    }
 
    fun display(a: A){
        print(a.operate())
    }
 
    val b = B()
    display(b)
}

La funció display accepta com argument un objecte de classe A, i com que B hereta d'A també accepta objectes de classe B.

Si executem aquest codi quin resultat es mostrarà: 10 o 25?

Les funcions d'extensió no són mètodes reals de la classe, sinò una ajuda sintàctica per escriure una funció com si fos un mètode de la classe.

En temps de compilació, una de les primeres coses que fa el compilador és transformar la funció display per escriure "correctament" la invocació de la funció A.operate:

fun display(a: A) {
    print(A.operate(a))
}

Per tant no importa el tipus real de paràmetre a, sempre s'aplicarà la funció A.operate mai la funció B.operate encara que el tipus dinàmic sigui B.

Per tant, el resultat és 10.

Les funcions d'extensió es resolen de manera estàtica, no dinàmica.

TODO eliminar B.operate que passa ; fer operate poliorfica en el tipus.

Nullable Receiver

Les funcions d'extensió també es poden definir amb el tipus de classe que es "nullable".

TODO explicar que és una classe "nullable"

En aquest cas, s'ha d'afegeir la comprovació de null dins de la funció d'extensió i tornar el valor corresponent.

class Hello(val name: String){
    override fun toString(): String {
        return "Hello $name"
    }
}

fun main(){
    fun Hello?.output(){
        if(this == null){
            println("Hello World!")
        } else {
            println(this.toString())
        }
    }

    val david = Hello("David")

    david.output()
    null.output()
}

Si executem aquest codi, el resultat serà:

Hello David!
Hello World!

Companion Object Extensions

Si una classe té un objecte complementari, també pots definir funcions i propietats d'extensió per a l'objecte complementari.

A continuació tens un exemple:

class Message {
    companion object {
        fun hello(){
            println("Hello")
        }
    }
}

Message.Companion.bye() {
    println("Bye!")
}

fun main() {
   Message.hello()
   Message.bye()
}

Pots veure que no hi ha diferència.

TODO