Nauka języka programowania Kotlin

Kotlin to język programowania używany przez programistów aplikacji na Androida na całym świecie. Ten temat to krótki kurs Kotlina, który pozwoli Ci szybko zacząć z niego korzystać.

Deklaracja zmiennej

Do zadeklarowania zmiennych Kotlin używa 2 różnych słów kluczowych: val i var.

  • Użyj val w przypadku zmiennej, której wartość nigdy się nie zmienia. Nie możesz ponownie przypisać wartości do zmiennej, która została zadeklarowana za pomocą funkcji val.
  • Użyj var w przypadku zmiennej, której wartość może się zmieniać.

W poniższym przykładzie count to zmienna typu Int, która ma przypisaną wartość początkową 10:

var count: Int = 10

Int to typ reprezentujący liczbę całkowitą, czyli jeden z wielu rodzajów liczb, które można przedstawić w Kotlin. Podobnie jak w przypadku innych języków, w zależności od danych liczbowych możesz też używać Byte, Short, Long, Float i Double.

Słowo kluczowe var oznacza, że w razie potrzeby możesz ponownie przypisać wartości do elementu count. Na przykład możesz zmienić wartość count z 10 na 15:

var count: Int = 10
count = 15

Niektórych wartości nie należy jednak zmieniać. Rozważmy String o nazwie languageName. Jeśli chcesz mieć pewność, że languageName zawsze będzie zawierać wartość „Kotlin”, możesz zadeklarować właściwość languageName za pomocą słowa kluczowego val:

val languageName: String = "Kotlin"

Te słowa kluczowe jednoznacznie informują o tym, co można zmienić. W razie potrzeby używaj ich na swoją korzyść. Jeśli odwołanie do zmiennej musi być możliwe do ponownego przypisania, zadeklaruj je jako var. W przeciwnym razie użyj elementu val.

Wnioskowanie typu

Podobnie jak w poprzednim przykładzie: gdy przypiszesz wartość początkową do languageName, kompilator Kotlin może wywnioskować typ na podstawie typu przypisanej wartości.

Ponieważ wartość "Kotlin" jest typu String, kompilator ustala, że languageName jest również typu String. Pamiętaj, że Kotlin to język zapisany statycznie. Oznacza to, że typ jest rozpoznawany w czasie kompilacji i nigdy się nie zmienia.

W poniższym przykładzie funkcja languageName jest ustalana jako String, więc nie można wywoływać funkcji, które nie należą do klasy String:

val languageName = "Kotlin"
val upperCaseName = languageName.toUpperCase()

// Fails to compile
languageName.inc()

toUpperCase() to funkcja, którą można wywoływać tylko w przypadku zmiennych typu String. Ponieważ kompilator Kotlin ustalił, że languageName to String, możesz bezpiecznie wywołać toUpperCase(). inc() to jednak funkcja operatora Int, więc nie można jej wywołać w metodzie String. Podejście Kotlina do wnioskowania z użyciem wpisywania zapewnia zarówno zwięzłość, jak i bezpieczeństwo typów.

Bezpieczeństwo zerowe

W niektórych językach zmienną typu odniesienia można zadeklarować bez podawania początkowej, jawnej wartości. W takich przypadkach zmienne zwykle zawierają wartość null. Zmienne Kotlin nie mogą domyślnie przechowywać wartości null. Oznacza to, że ten fragment jest nieprawidłowy:

// Fails to compile
val languageName: String = null

Aby zmienna mogła zawierać wartość null, musi być typem nullable. Możesz określić zmienną jako dopuszczalną (null), dodając jej typ przyrostkiem ?, jak w tym przykładzie:

val languageName: String? = null

Z typem String? możesz przypisać wartość String lub null do właściwości languageName.

Musisz ostrożnie obchodzić się ze zmiennymi dopuszczającymi wartość null, ponieważ w przeciwnym razie ryzykujesz budzenie zainteresowania NullPointerException. Na przykład w Javie próba wywołania metody dla wartości null powoduje błąd programu.

Kotlin udostępnia szereg mechanizmów umożliwiających bezpieczną pracę ze zmiennymi z wartością null. Więcej informacji znajdziesz w artykule o typowych wzorcach Kotlin na Androidzie: wartość nullability.

Warunkowe

Kotlin udostępnia kilka mechanizmów służących do implementowania logiki warunkowej. Najczęściej jest to stwierdzenie „if-else”. Jeśli wyrażenie ujęte w nawiasy obok słowa kluczowego if zwraca wartość true, zostaje wykonany kod znajdujący się w tej gałęzi (tj. kod zawarty w nawiasach klamrowych znajdujący się bezpośrednio po nim). W przeciwnym razie zostanie wykonany kod w gałęzi else.

if (count == 42) {
    println("I have the answer.")
} else {
    println("The answer eludes me.")
}

Wiele warunków możesz podać za pomocą funkcji else if. Dzięki temu możesz przedstawić bardziej szczegółową, złożoną logikę w ramach jednej instrukcji warunkowej, jak pokazano w tym przykładzie:

if (count == 42) {
    println("I have the answer.")
} else if (count > 35) {
    println("The answer is close.")
} else {
    println("The answer eludes me.")
}

Wyrażenia warunkowe przydają się do reprezentowania logiki stanowej, ale czasami zdarza się, że się powtarzasz. W przykładzie powyżej wydrukujesz po prostu String w każdej gałęzi. Aby uniknąć takich powtórzeń, Kotlin oferuje wyrażenia warunkowe. Ostatni przykład można napisać ponownie w ten sposób:

val answerString: String = if (count == 42) {
    "I have the answer."
} else if (count > 35) {
    "The answer is close."
} else {
    "The answer eludes me."
}

println(answerString)

W sumie każda gałąź warunkowa zwraca wynik wyrażenia na ostatnim wierszu, więc nie trzeba używać słowa kluczowego return. Wynik wszystkich 3 gałęzi jest typu String, dlatego wynik wyrażenia if-else jest też typu String. W tym przykładzie do funkcji answerString przypisano wartość początkową na podstawie wyniku wyrażenia if-else. Za pomocą wnioskowania typu można pominąć jawną deklarację typu dla właściwości answerString, ale często warto ją uwzględnić, aby zwiększyć przejrzystość.

W miarę jak rosną złożoność instrukcji warunkowej, możesz rozważyć zastąpienie wyrażenia „if-else” wyrażeniem when, jak w tym przykładzie:

val answerString = when {
    count == 42 -> "I have the answer."
    count > 35 -> "The answer is close."
    else -> "The answer eludes me."
}

println(answerString)

Każda gałąź w wyrażeniu when jest reprezentowana przez warunek, strzałkę (->) i wynik. Jeśli warunek po lewej stronie strzałki ma wartość „prawda”, zwracany jest wynik wyrażenia po prawej stronie. Pamiętaj, że wykonanie nie przechodzi z jednej gałęzi do kolejnej. Kod w przykładowym wyrażeniu when jest funkcjonalnie taki sam jak w poprzednim przykładzie, ale jest prawdopodobnie łatwiejszy do odczytania.

Warunki w filmie Kotlin podkreślają jedną z najbardziej przydatnych funkcji aplikacji – inteligentne przesyłanie. Zamiast używać operatora Secure-call lub operatora assertion o wartości niezerowej do pracy z wartościami null, możesz za pomocą instrukcji warunkowej sprawdzić, czy zmienna zawiera odwołanie do wartości null:

val languageName: String? = null
if (languageName != null) {
    // No need to write languageName?.toUpperCase()
    println(languageName.toUpperCase())
}

W gałęzi warunkowej languageName może być traktowany jako niedopuszczalny. Kotlin jest na tyle sprytny, że rozpoznaje, że warunkiem wykonania gałęzi jest to, że languageName nie ma wartości null, więc nie musisz traktować pola languageName jako funkcji null w tej gałęzi. To inteligentne przesyłanie działa przy sprawdzaniu wartości null, sprawdzaniu typu i innych warunkach, które spełniają umowy.

Funkcje

Możesz zgrupować jedno lub kilka wyrażeń w funkcję. Zamiast powtarzać tę samą serię wyrażeń za każdym razem, gdy potrzebujesz wyniku, możesz opakować te wyrażenia w funkcję i jej wywołać.

Aby zadeklarować funkcję, użyj słowa kluczowego fun z nazwą funkcji. Następnie zdefiniuj rodzaje danych wejściowych, których używa Twoja funkcja (jeśli jakieś występują), i zadeklaruj typ zwracanych przez nią danych wyjściowych. Treść funkcji to miejsce, w którym definiuje się wyrażenia, które są wywoływane po jej wywołaniu.

Na podstawie poprzednich przykładów oto pełna funkcja Kotlin:

fun generateAnswerString(): String {
    val answerString = if (count == 42) {
        "I have the answer."
    } else {
        "The answer eludes me"
    }

    return answerString
}

Funkcja w przykładzie powyżej ma nazwę generateAnswerString. Nie wymaga wprowadzania danych. Zwraca wynik typu String. Aby wywołać funkcję, użyj jej nazwy, po której następuje operator wywołania (()). W przykładzie poniżej zmienna answerString jest zainicjowana wynikiem ze źródła generateAnswerString().

val answerString = generateAnswerString()

Funkcje mogą przyjmować argumenty jako dane wejściowe, tak jak w tym przykładzie:

fun generateAnswerString(countThreshold: Int): String {
    val answerString = if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }

    return answerString
}

Podczas deklarowania funkcji możesz podać dowolną liczbę argumentów i ich typy. W przykładzie powyżej generateAnswerString() przyjmuje 1 argument o nazwie countThreshold typu Int. W obrębie funkcji możesz się odwoływać do argumentu, używając jego nazwy.

Wywołując tę funkcję, musisz umieścić argument w nawiasach jej wywołania:

val answerString = generateAnswerString(42)

Uproszczenie deklaracji funkcji

Funkcja generateAnswerString() jest stosunkowo prosta. Funkcja deklaruje zmienną, a następnie od razu zwraca. Gdy z funkcji zwracany jest wynik pojedynczego wyrażenia, możesz pominąć zadeklarowanie zmiennej lokalnej, bezpośrednio zwracając wynik wyrażenia if-else zawartego w funkcji, jak pokazano w tym przykładzie:

fun generateAnswerString(countThreshold: Int): String {
    return if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }
}

Możesz też zastąpić zwracane słowo kluczowe operatorem przypisania:

fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
        "I have the answer"
    } else {
        "The answer eludes me"
    }

Funkcje anonimowe

Nie każda funkcja musi mieć nazwę. Niektóre funkcje są bardziej bezpośrednio identyfikowane przez dane wejściowe i wyjściowe. Są to tak zwane funkcje anonimowe. Możesz zachować odniesienie do funkcji anonimowej, aby użyć jej później do jej wywołania. Możesz też przekazywać odniesienie do aplikacji, tak jak w przypadku innych typów plików referencyjnych.

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

Podobnie jak funkcje nazwane, funkcje anonimowe mogą zawierać dowolną liczbę wyrażeń. Zwrócona wartość funkcji jest wynikiem wyrażenia końcowego.

W powyższym przykładzie funkcja stringLengthFunc zawiera odwołanie do funkcji anonimowej, która jako dane wejściowe przyjmuje String i zwraca długość danych wejściowych String jako danych wyjściowych typu Int. Z tego względu typem funkcji jest (String) -> Int. Ten kod nie wywołuje jednak funkcji. Aby pobrać wynik funkcji, musisz ją wywołać w ten sam sposób, jak w przypadku funkcji nazwanej. Przy wywoływaniu metody stringLengthFunc musisz podać parametr String, jak pokazano w tym przykładzie:

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

val stringLength: Int = stringLengthFunc("Android")

Funkcje wyższego rzędu

Funkcja może przyjąć inną funkcję jako argument. Funkcje, które wykorzystują inne funkcje jako argumenty, nazywane są funkcjami wyższego rzędu. Ten wzorzec przydaje się do komunikacji między komponentami w taki sam sposób jak interfejs wywołania zwrotnego w języku Java.

Oto przykład funkcji wyższego rzędu:

fun stringMapper(str: String, mapper: (String) -> Int): Int {
    // Invoke function
    return mapper(str)
}

Funkcja stringMapper() przyjmuje element String wraz z funkcją, która pobiera wartość Int z przekazanego do niej obiektu String.

Możesz wywołać stringMapper(), przekazując String i funkcję, która spełnia drugi parametr wejściowy, czyli funkcję, która pobiera String jako dane wejściowe i zwraca Int, jak w tym przykładzie:

stringMapper("Android", { input ->
    input.length
})

Jeśli funkcja anonimowa jest parametrem last zdefiniowanym w funkcji, możesz ją przekazać poza nawiasami użytymi do jej wywołania, jak w tym przykładzie:

stringMapper("Android") { input ->
    input.length
}

Funkcje anonimowe można znaleźć w standardowej bibliotece Kotlin. Więcej informacji znajdziesz w artykule Funkcje wyższego poziomu i lambda.

Zajęcia

Wszystkie wymienione do tej pory typy są wbudowane w język programowania Kotlin. Jeśli chcesz dodać własny typ niestandardowy, możesz zdefiniować klasę za pomocą słowa kluczowego class, jak w tym przykładzie:

class Car

Właściwości

Klasy przedstawiają stan przy użyciu właściwości. Właściwość to zmienna na poziomie klasy, która może zawierać obiekt pobierający, metodę ustawiającą i pole backendu. Samochód potrzebuje koła, więc możesz dodać listę obiektów Wheel jako właściwość Car, jak w tym przykładzie:

class Car {
    val wheels = listOf<Wheel>()
}

Pamiętaj, że wheels to typ public val, co oznacza, że dostęp do wheels jest dostępny spoza klasy Car i nie można go ponownie przypisać. Aby uzyskać instancję Car, musisz najpierw wywołać jej konstruktor. Tam możesz przejść do wszystkich dostępnych właściwości.

val car = Car() // construct a Car
val wheels = car.wheels // retrieve the wheels value from the Car

Jeśli chcesz dostosować koła, możesz zdefiniować własny konstruktor określający sposób inicjowania właściwości klas:

class Car(val wheels: List<Wheel>)

W przykładzie powyżej konstruktor klas wykorzystuje List<Wheel> jako argument konstruktora i używa go do zainicjowania swojej właściwości wheels.

Funkcje klas i ich herbata

Klasy wykorzystują funkcje do modelowania zachowań. Funkcje mogą zmieniać stan, ułatwiając ujawnianie tylko tych danych, które chcesz udostępnić. Ta kontrola dostępu jest częścią większej koncepcji zorientowanej na obiekty znane jako enkapsulacja.

W poniższym przykładzie właściwość doorLock jest prywatna i chroniona przed wszystkimi spoza klasy Car. Aby odblokować samochód, musisz wywołać funkcję unlockDoor(), podając prawidłowy kluczyk, jak w tym przykładzie:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Jeśli chcesz dostosować sposób, w jaki odwołują się do właściwości, możesz udostępnić niestandardowy obiekt pobierający i ustawiający. Jeśli na przykład chcesz ujawnić pobieranie usługi, ograniczając dostęp do jej metody ustawiającej, możesz oznaczyć ją jako private:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    var gallonsOfFuelInTank: Int = 15
        private set

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Dzięki połączeniu właściwości i funkcji możesz tworzyć klasy, które modelują wszystkie typy obiektów.

Interoperacyjność

Jedną z najważniejszych cech platformy Kotlin jest płynna interoperacyjność z Javą. Kod Kotlin kompiluje się do kodu bajtowego JVM, więc może wywoływać kod bezpośrednio w Javie i odwrotnie. Oznacza to, że możesz używać istniejących bibliotek Java bezpośrednio z Kotlin. Dodatkowo większość interfejsów API dla Androida jest napisana w Javie i można je wywoływać bezpośrednio z aplikacji Kotlin.

Dalsze kroki

Kotlin to elastyczny, pragmatyczny język, który cieszy się rosnącym wsparciem i zasięgiem. Jeśli jeszcze nie znasz tej funkcji, zachęcam do jej wypróbowania. Dalsze kroki znajdziesz w oficjalnej dokumentacji aplikacji Kotlin oraz w przewodniku dotyczącym stosowania typowych wzorców Kotlin w aplikacjach na Androida.