12 mniej znanych cech Kotlina, o których mogłeś nie wiedzieć

Prowadziłem ostatnio warsztaty z Kotlina. Uczestnicy zadawali oczywiście sporo pytań – taki był zresztą plan. Jednak część z tych pytań naprawdę mnie zaskoczyła 😀 I nie chodzi o to, że nie potrafiłem na nie odpowiedzieć – bo potrafiłem 😉 Chodzi o to, że dotyczyły one spraw dla mnie tak „oczywistych”, że po kilku latach kodowania w Kotlinie zupełnie przestałem je zauważać…

Ta sytuacja natchnęła mnie do napisania niniejszego posta. Zebrałem tu listę pozornie małych rzeczy – udogodnień, których na co dzień albo nie zauważamy, albo nawet nie wiemy o ich istnieniu, a które wyraźnie ułatwiają nam życie.

Co więcej, żadna z nich nie jest dziełem przypadku. Wszystkie zostały skrzętnie zaprojektowane w oparciu o błędy i doświadczenia innych języków – przede wszystkim Javy.

Koniec tego przydługiego wstępu. Zapraszam do lektury 😉

1. Operator == działa jak equals() w Javie

Dość częstym błędem w Javie jest porównywanie obiektów za pomocą operatora ==, i oczekiwanie wyniku działania metody equals(), np.:

User user1 = new User("John", "Smith");
User user2 = new User("John", "Smith");
System.out.println(user1 == user2); // ???

Jak będzie wynik działania powyższego kodu?

Pokaż odpowiedź

Dlaczego? Dlatego że w Javie operator == nie porównuje właściwości/struktury obiektów, tylko sprawdza, czy referencje wskazują na to samo miejsce w pamięci. Ponieważ nasze referencje wskazują na dwa różne obiekty, to dostajemy false – niezależnie od faktu, że te obiekty są identyczne.

W Kotlinie nie ma tego problemu. Operator == woła po prostu metodę equals() i tyle. Natomiast do porównywania wskaźników mamy, jak w wielu nowych językach, operator ===.

2. Parametry funkcji są finalne

Kolejną ciekawostką jest finalność parametrów funkcji/metod. Jak myślisz, jaki będzie wynik działania poniższego kodu w Javie?

public void appendTest() {
    String message = "abc";
    append(message, "def");
    System.out.println(message); // ???
}

public void append(String msg, String suffix) {
    msg += suffix;
}

Pokaż odpowiedź

Co się stało? Java źle działa? Oczywiście, że nie 😉 Po prostu użyłeś operatora +=, który „przekierował” referencję msg w inne miejsce. Nadpisałeś ją. Przestała wskazywać na to samo miejsce co referencja message.

Jedynym znanym mi sposobem na tak podstępne błędy jest konsekwentne oznaczanie wszystkich parametrów metod jako final (np. final String msg). I znam kilka osób, które faktycznie tak robią…

Jak to jest w Kotlinie? Otóż wszystkie parametry są finalne. W ogóle nie ma możliwości ich „nadpisania”. Przy każdej próbie użycia operatora =/+=/*= itp. dostaniemy błąd kompilacji. Dlatego powyższy problem nigdy nie wystąpi.

3. override jest słowem kluczowym, a nie adnotacją

O ile adnotacja @Override w Javie NIE jest obowiązkowa, o tyle słowo kluczowe override w Kotlinie – już jest. Dzięki temu wykluczamy kolejną grupę podstępnych błędów, np.:

// Klasa bazowa
public class BaseClass {
    public void doSomething() { ... }
}

// Java
// Obie poniższe klasy się kompilują, mimo iż jedna zawiera literówkę

public class MyClass1 extends BaseClass {
    public void doSomething() { ... }
}

public class MyClass2 extends BaseClass {
    public void doSomethng() { ... }
}

// Kotlin

class MyClass1 : BaseClass() {
    fun doSomething() { ... } // błąd kompilacji - brak "override"
}

class MyClass2 : BaseClass() {
    override fun doSomething() { ... } // OK
}

class MyClass3 : BaseClass() {
    override fun doSomethng() { ... } // błąd kompilacji - literówka
}

4. Nazwy metod mogą zawierać spacje…

Tak, to prawda 😉 Istnieje specjalna notacja umożliwiająca tworzenie metod, których nazwy mogą zawierać spacje i inne dziwne znaczki… Jednak trzeba tu jasno zaznaczyć, że została ona stworzona wyłącznie z myślą o testach – i tylko tam powinno się jej używać, np.:

@Test
fun `when password is wrong then throw an error`() {
    //your test logic
}

5. System typów i typ Nothing

Kotlin ma genialny, bardzo spójny system typów. Wszystko jest obiektem. Każdy typ dziedziczy po typie bazowym Any. Każdy typ ma swoją wersję nullowalną (np. String?, Int?) i nie-nullowalną (np. String, Int). Jest także singleton Unit, który (w dużym uproszczeniu) odpowiada słowu kluczowemu void z Javy.

Jednak nie każdy wie, że istnieje też bardzo ciekawy typ o nazwie Nothing. Cóż to takiego? Otóż, podczas gdy wszystkie typy dziedziczą po Any, to Nothing dziedziczy po WSZYSTKICH typach. Spróbujmy to zobrazować:

     ->  String   ->
     ->  Int      ->
Any  ->  List<T>  ->  Nothing
     ->  User     -> 
     ->  ...      ->

Nie będę tutaj wchodził w szczegóły (można o nich poczytać tutaj). Powiem tylko, że dzięki takiej koncepcji, możemy tworzyć konstrukcje jak ta poniżej:

fun getAddress(user: User): Address {
    return user.address ?: throw Exception("Address required")
}

throw jest tutaj użyte jako wyrażenie (a nie instrukcja!). Wyrażenia zwracają wartość. Zgadnij jakiego typu jest wartość zwracana przez throw? Dokładnie – Nothing! A ponieważ Nothing dziedziczy po wszystkich typach, to dziedziczy również po  naszym Address – i właśnie taki typ zwracamy z funkcji.

Również funkcja TODO(), którą na co dzień wykorzystujemy jako placeholder dla niezaimplementowanych funkcji, ma prawo bytu wyłącznie dzięki istnieniu typu Nothing:

fun doSomething(param: Boolean): String {
    if (param) {
        return "something"
    } else {
        TODO("Not implemented yet")
    }
}

I znów, możemy jej użyć niezależnie od typu wracanego przez funkcję.

6. switch na sterydach

Kto nigdy nie zapomniał o umieszczeniu break’a w switch-case, niech pierwszy rzuci kamień 😀 Według niektórych rankingów jest to najczęstszy błąd popełniany w Javie.

W Kotlinie pozbyto się go w prosty sposób – po prostu w ogóle nie trzeba pisać break’a 😉

Wyrażenie when (często określane mianem switch’a na sterydach) porównuje kolejno każdy warunek, i gdy któryś jest zgodny – wykonuje dany branch. I tylko ten jeden branch – a nie wszystkie kolejne w dół. A jeśli chcemy by ten sam branch wykonał się dla wielu warunków, to oddzielamy je przecinkiem:

when (x) {
    1    -> print("one")
    2, 3 -> print("two or three")
    else -> print("something else")
}

7. Sprecyzowanych typów nie trzeba ręcznie rzutować

Kiedy sprawdzamy typ obiektu za pomocą operatora is, kompilator Kotlina może się popisać funkcją określaną mianem „smart-casting”. Polega ona na tym, że tak sprawdzonego typu nie musimy już ręcznie rzutować (tak jakbyśmy musieli w Javie), np.:

val result = when (x) {
    is String  -> x.length > 5
    is Int     -> x > 5
    is Boolean -> x.xor(true)
    else       -> false
}

Zauważ że po prawej stronie -> dostajemy odpowiednio: String’a, Int’a i Bool’a, mimo iż niczego jawnie nie rzutowaliśmy.

8. when jako alternatywa dla if-else-if…

Mało kto wie, że when może też przybrać specjalną, bezparametrową formę. Poszczególne warunki stają się wtedy zwykłymi wyrażeniami zwracającymi Bool’a, i mogą przyjmować dowolną logikę:

when {
    x > 5     -> print("greater than 5")
    isEven(x) -> print("is even")
    else      -> print("none of the above")
}

9. Wieloliniowe Stringi

Chyba wszyscy wiedzą o interpolacji stringów – czyli funkcji, która pozwala zamieszczać wewnątrz Stringów symbole poprzedzone znakiem $:

val a = 1; val b = 2
println("$a + $b = ${a+b}")  //prints: 1 + 2 = 3

Natomiast nie wszyscy już wiedzą, że Kotlin posiada także wieloliniowe Stringi:

val msg = """abc
def
ghi"""

…oraz że w nich również możemy zamieszczać symbole:

val name = "Grzegorz"
val msg = """Hello, $name!
How are you?
Best regards, Admin"""

A jakby tego było mało, możemy jeszcze użyć funkcji trimIndent(), która wycina wiodące spacje i puste linie:

val withoutIndent = """
    ABC
    123
    456
""".trimIndent() // ABC\n123\n456

10. Przekazywanie tablicy w miejsce vararg

Załóżmy że mamy funkcję ze zmienną liczbą argumentów (vararg):

fun calculateSum(vararg elements: Int): Int { ... }

Możemy do niej przekazywać pojedyncze elementy:

calculateSum(1, 2, 3)

Lub, jeśli mamy już gotową tablicę elementów, to możemy ją przekazać bezpośrednio, korzystając z tzw. „spread operator” (czyli prefiksu *):

val elements = arrayOf(1, 2, 3)
calculateSum(*elements)

11. Wygodna iteracja po Mapach

W Kotlinie możemy w łatwy sposób iterować jednocześnie po kluczach i wartościach:

for ((k, v) in map) {
    println("$k -> $v")
}

Warto zwrócić uwagę, że NIE jest to żaden „przypadek szczególny”, żaden specjalny for dla Map. Korzystamy tu po prostu z mechanizmu dekonstrukcji obiektów – tego samego, który pozwala nam „rozkładać” na czynniki pierwsze np. data class’y.

12. Kotlin to nie tylko JVM!

Będąc developerem JVM łatwo zapomnieć, że twórcy Kotlina nie ograniczają się tylko do tego jednego ekosystemu – jest jeszcze Kotlin/JS i Kotlin/Native.

Kotlin/JS transpiluje kod Kotlina do JavaScriptu, co pozwala m.in. na pisanie front-end’ów webowych w React w Kotlinie (więcej info tutaj). Natomiast Kotlin/Native umożliwia kompilację kodu Kotlina do kodu natywnego dla Linuksa, macOS, iOS i Windowsa.

Tak więc już dziś możemy pisać w Kotlinie produkcyjne backendy (Spring, Vert.x, Ktor), frontendy (jQuery, React) i apki na Androida. A już niedługo także apki na iOS i desktopy. Aż strach pomyśleć, co będzie następne…? 😉

Podsumowanie

Zdaję sobie sprawę, że powyższa lista jest subiektywna, i zdecydowanie NIE jest wyczerpująca 😉

Mimo to ciekaw jestem, czy wiedziałeś o wszystkich z powyższych? A może masz swoje ulubione „małe” rzeczy, ułatwiające codzienne życie developera? Podziel się w komentarzu 😀

Z mojej strony to wszystko. Do następnego wpisu!

 

Nie przegap kolejnych wpisów - subskrybuj!

2 komentarzy

Dodaj komentarz