Kotlin Bootcamp for Programmers 5.1: Extensions

Te warsztaty są częścią kursu Kotlin Bootcamp for Programmers. Najwięcej korzyści przyniesie Ci ukończenie wszystkich ćwiczeń w kolejności. W zależności od swojej wiedzy możesz pominąć niektóre sekcje. Ten kurs jest przeznaczony dla programistów, którzy znają język obiektowy i chcą nauczyć się Kotlin.

Wprowadzenie

W tym laboratorium poznasz wiele przydatnych funkcji Kotlina, w tym pary, kolekcje i funkcje rozszerzeń.

Lekcje w tym kursie nie są powiązane z jedną przykładową aplikacją. Zostały zaprojektowane tak, aby poszerzać Twoją wiedzę, ale jednocześnie były od siebie częściowo niezależne. Dzięki temu możesz przeglądać sekcje, które już znasz. Aby je ze sobą powiązać, w wielu przykładach użyto motywu akwarium. Jeśli chcesz poznać pełną historię akwarium, zapoznaj się z kursem Kotlin Bootcamp for Programmers na platformie Udacity.

Co warto wiedzieć

  • Składnia funkcji, klas i metod w języku Kotlin
  • Jak korzystać z REPL (Read-Eval-Print Loop) w Kotlinie w IntelliJ IDEA
  • Jak utworzyć nową klasę w IntelliJ IDEA i uruchomić program

Czego się nauczysz

  • Praca z parami i trójkami
  • Więcej informacji o kolekcjach
  • Definiowanie i używanie stałych
  • Pisanie funkcji rozszerzeń

Jakie zadania wykonasz

  • Informacje o parach, trójkach i mapach skrótów w REPL
  • Poznaj różne sposoby organizowania stałych
  • Napisz funkcję rozszerzającą i właściwość rozszerzającą

W tym ćwiczeniu dowiesz się, czym są pary i trójki oraz jak je rozpakowywać. Pary i trójki to gotowe klasy danych dla 2 lub 3 ogólnych elementów. Może to być przydatne np. wtedy, gdy funkcja ma zwracać więcej niż jedną wartość.

Załóżmy, że masz List ryb i funkcję isFreshWater(), która sprawdza, czy ryba jest słodkowodna czy słonowodna. List.partition() zwraca 2 listy: jedną z produktami, w których warunek to true, a drugą z produktami, w których warunek to false.

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

Krok 1. Utwórz kilka par i trójek

  1. Otwórz REPL (Narzędzia > Kotlin > Kotlin REPL).
  2. Utwórz parę, przypisując sprzęt do jego zastosowania, a następnie wydrukuj wartości. Parę możesz utworzyć, tworząc wyrażenie łączące 2 wartości, np. 2 ciągi znaków, za pomocą słowa kluczowego to, a następnie używając .first lub .second, aby odwołać się do każdej wartości.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. Utwórz trójkę i wydrukuj ją za pomocą toString(), a następnie przekształć ją w listę za pomocą toList(). Trójkę tworzysz za pomocą symbolu Triple() z 3 wartościami. Aby odwoływać się do poszczególnych wartości, używaj symboli .first, .second.third.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

W powyższych przykładach wszystkie części pary lub trójki są tego samego typu, ale nie jest to wymagane. Elementy mogą być ciągiem tekstowym, liczbą lub listą, a nawet inną parą lub trójką.

  1. Utwórz parę, której pierwszy element jest sam w sobie parą.
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

Krok 2. Rozpakuj niektóre pary i trójki

Rozdzielanie par i trójek na części składowe nazywa się destrukturyzacją. Przypisz parę lub trójkę do odpowiedniej liczby zmiennych, a Kotlin przypisze wartość każdej części w odpowiedniej kolejności.

  1. Rozpakuj parę i wydrukuj wartości.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. Rozpakuj trójkę i wydrukuj wartości.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

Pamiętaj, że rozpakowywanie par i trójek działa tak samo jak w przypadku klas danych, co zostało omówione w poprzednim laboratorium.

Z tego zadania dowiesz się więcej o kolekcjach, w tym o listach, oraz o nowym typie kolekcji – mapach skrótów.

Krok 1. Dowiedz się więcej o listach

  1. Listy i listy modyfikowalne zostały omówione w poprzedniej lekcji. Są one bardzo przydatną strukturą danych, dlatego Kotlin udostępnia wiele wbudowanych funkcji do obsługi list. Zapoznaj się z tą częściową listą funkcji dotyczących list. Pełne listy znajdziesz w dokumentacji Kotlin dotyczącej ListMutableList.

Funkcja

Purpose

add(element: E)

Dodaj element do listy modyfikowalnej.

remove(element: E)

Usuń element z listy modyfikowalnej.

reversed()

Zwraca kopię listy z elementami w odwrotnej kolejności.

contains(element: E)

Zwraca wartość true, jeśli lista zawiera element.

subList(fromIndex: Int, toIndex: Int)

Zwraca część listy, od pierwszego indeksu do drugiego indeksu (bez niego).

  1. Nadal pracując w REPL, utwórz listę liczb i wywołaj na niej funkcję sum(). Sumuje wszystkie elementy.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Utwórz listę ciągów tekstowych i oblicz sumę elementów na liście.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. Jeśli element nie jest czymś, co List potrafi zsumować bezpośrednio, np. ciągiem znaków, możesz określić sposób sumowania za pomocą funkcji .sumBy() z funkcją lambda, np. aby sumować według długości każdego ciągu znaków. Domyślna nazwa argumentu lambda to it, a it odnosi się do każdego elementu listy podczas jej przeglądania.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. Listy mają wiele innych zastosowań. Aby sprawdzić dostępne funkcje, utwórz listę w IntelliJ IDEA, dodaj kropkę, a potem przejrzyj listę autouzupełniania w etykiecie. Działa to w przypadku każdego obiektu. Wypróbuj go na liście.

  1. Wybierz z listy listIterator(), a następnie przejrzyj listę z instrukcją for i wydrukuj wszystkie elementy oddzielone spacjami.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

Krok 2. Wypróbuj mapy skrótów

W języku Kotlin możesz zmapować niemal wszystko na wszystko inne za pomocą funkcji hashMapOf(). Mapy haszujące przypominają listę par, w której pierwsza wartość pełni funkcję klucza.

  1. Utwórz mapę mieszającą, która będzie dopasowywać objawy (klucze) do chorób ryb (wartości).
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. Następnie możesz pobrać wartość choroby na podstawie klucza objawu, używając get() lub krótszych nawiasów kwadratowych [].
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. Spróbuj podać objaw, którego nie ma na mapie.
println(cures["scale loss"])
⇒ null

Jeśli klucza nie ma na mapie, próba zwrócenia pasującej choroby zwraca wartość null. W zależności od danych mapy często zdarza się, że nie ma dopasowania do możliwego klucza. W takich przypadkach Kotlin udostępnia funkcję getOrDefault().

  1. Spróbuj wyszukać klucz, który nie ma odpowiednika, używając znaku getOrDefault().
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

Jeśli chcesz zrobić coś więcej niż tylko zwrócić wartość, Kotlin udostępnia funkcję getOrElse().

  1. Zmień kod, aby używać getOrElse() zamiast getOrDefault().
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

Zamiast zwracać prostą wartość domyślną, wykonywany jest kod znajdujący się między nawiasami klamrowymi {}. W tym przykładzie else zwraca po prostu ciąg znaków, ale może też wyszukiwać stronę internetową z lekarstwem i ją zwracać.

Podobnie jak w przypadku mutableListOf możesz też utworzyć mutableMapOf. Mapa modyfikowalna umożliwia dodawanie i usuwanie elementów. Zmienne oznaczają, że można je zmieniać, a niezmienne, że nie można.

  1. Utwórz mapę asortymentu, którą można modyfikować, mapując ciąg znaków sprzętu na liczbę elementów. Utwórz go, dodając do niego siatkę na ryby, a następnie dodaj do asortymentu 3 czyściki do akwarium za pomocą ikony put() i usuń siatkę na ryby za pomocą ikony remove().
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

W tym ćwiczeniu dowiesz się, czym są stałe w Kotlinie i jak je porządkować.

Krok 1. Dowiedz się więcej o różnicach między const a val

  1. W REPL spróbuj utworzyć stałą liczbową. W języku Kotlin możesz tworzyć stałe najwyższego poziomu i przypisywać im wartość w czasie kompilacji za pomocą symbolu const val.
const val rocks = 3

Wartość jest przypisywana i nie można jej zmienić, co przypomina deklarowanie zwykłej val. Czym różni się wersja const val od val? Wartość const val jest określana w czasie kompilacji, a wartość val – podczas wykonywania programu, co oznacza, że val może być przypisana przez funkcję w czasie działania.

Oznacza to, że do zmiennej val można przypisać wartość z funkcji, ale do zmiennej const val nie.

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

Poza tym adnotacja const val działa tylko na najwyższym poziomie i w klasach singleton zadeklarowanych za pomocą adnotacji object, a nie w zwykłych klasach. Możesz go użyć do utworzenia pliku lub obiektu singleton, który zawiera tylko stałe, i w razie potrzeby importować je.

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

Krok 2. Utwórz obiekt towarzyszący

Kotlin nie ma koncepcji stałych na poziomie klasy.

Aby zdefiniować stałe w klasie, musisz umieścić je w obiektach towarzyszących zadeklarowanych za pomocą słowa kluczowego companion. Obiekt towarzyszący to w zasadzie pojedynczy obiekt w klasie.

  1. Utwórz klasę z obiektem towarzyszącym zawierającym stałą tekstową.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

Podstawowa różnica między obiektami towarzyszącymi a zwykłymi obiektami polega na tym, że:

  • Obiekty towarzyszące są inicjowane przez statyczny konstruktor klasy zawierającej, czyli są tworzone w momencie tworzenia obiektu.
  • Zwykłe obiekty są inicjowane leniwie przy pierwszym dostępie do nich, czyli gdy są używane po raz pierwszy.

Jest ich więcej, ale na razie wystarczy, że stałe należy umieszczać w klasach w obiekcie towarzyszącym.

W tym ćwiczeniu dowiesz się, jak rozszerzać działanie klas. Bardzo często pisze się funkcje narzędziowe, aby rozszerzyć działanie klasy. Kotlin udostępnia wygodną składnię do deklarowania tych funkcji narzędziowych: funkcji rozszerzających.

Funkcje rozszerzeń umożliwiają dodawanie funkcji do istniejącej klasy bez konieczności uzyskiwania dostępu do jej kodu źródłowego. Możesz je na przykład zadeklarować w pliku Extensions.kt, który jest częścią pakietu. Nie modyfikuje to klasy, ale umożliwia używanie notacji kropkowej podczas wywoływania funkcji na obiektach tej klasy.

Krok 1. Napisz funkcję rozszerzenia

  1. Nadal pracując w REPL, napisz prostą funkcję rozszerzenia hasSpaces(), która sprawdzi, czy ciąg znaków zawiera spacje. Nazwa funkcji jest poprzedzona nazwą klasy, na której działa. Wewnątrz funkcji this odnosi się do obiektu, na którym jest wywoływana, a it odnosi się do iteratora w wywołaniu find().
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. Możesz uprościć funkcję hasSpaces(). Symbol this nie jest wyraźnie potrzebny, a funkcję można sprowadzić do jednego wyrażenia i zwrócić, więc nie są też potrzebne otaczające je nawiasy klamrowe {}.
fun String.hasSpaces() = find { it == ' ' } != null

Krok 2. Poznaj ograniczenia rozszerzeń

Funkcje rozszerzające mają dostęp tylko do publicznego interfejsu API klasy, którą rozszerzają. Do zmiennych, które są private, nie można uzyskać dostępu.

  1. Spróbuj dodać funkcje rozszerzeń do usługi oznaczonej jako private.
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. Sprawdź poniższy kod i określ, co zostanie wydrukowane.
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print() odbitekGreenLeafyPlant. Możesz oczekiwać, że aquariumPlant.print() też wydrukuje GreenLeafyPlant, ponieważ przypisano mu wartość plant. Typ jest jednak określany w momencie kompilacji, więc wyświetlany jest znak AquariumPlant.

Krok 3. Dodaj właściwość rozszerzenia

Oprócz funkcji rozszerzających Kotlin umożliwia też dodawanie właściwości rozszerzających. Podobnie jak w przypadku funkcji rozszerzających, określasz klasę, którą rozszerzasz, a następnie dodajesz kropkę i nazwę właściwości.

  1. Nadal pracując w REPL, dodaj właściwość rozszerzenia isGreen do AquariumPlant, która ma wartość true, jeśli kolor jest zielony.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

Do właściwości isGreen można uzyskać dostęp tak samo jak do zwykłej właściwości. Gdy to zrobisz, wywoływana jest funkcja pobierająca wartość isGreen.

  1. Wyświetl właściwość isGreen zmiennej aquariumPlant i sprawdź wynik.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

Krok 4. Dowiedz się więcej o odbiorcach dopuszczających wartość null

Klasa, którą rozszerzasz, jest nazywana odbiorcą i można ją ustawić jako dopuszczającą wartość null. Jeśli to zrobisz, zmienna this użyta w treści może mieć wartość null, więc pamiętaj, aby to sprawdzić. Warto użyć odbiornika dopuszczającego wartość null, jeśli oczekujesz, że wywołujący będą chcieli wywołać metodę rozszerzającą na zmiennych dopuszczających wartość null lub jeśli chcesz zapewnić domyślne działanie, gdy funkcja jest stosowana do null.

  1. Nadal pracując w REPL, zdefiniuj metodę pull(), która przyjmuje odbiornik dopuszczający wartość null. Jest to oznaczone znakiem zapytania ? po typie, przed kropką. W treści możesz sprawdzić, czy this nie jest równe null, używając znaku zapytania z kropką i funkcji apply ?.apply..
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. W tym przypadku po uruchomieniu programu nie pojawią się żadne dane wyjściowe. Ponieważ plant to null, wewnętrzna funkcja println() nie jest wywoływana.

Funkcje rozszerzające są bardzo przydatne, a większość biblioteki standardowej Kotlina jest zaimplementowana jako funkcje rozszerzające.

Z tej lekcji dowiedziałeś się więcej o kolekcjach i stałych oraz poznałeś możliwości funkcji i właściwości rozszerzeń.

  • Pary i trójki mogą służyć do zwracania więcej niż jednej wartości z funkcji. Na przykład:
    val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin ma wiele przydatnych funkcji do List, takich jak reversed(), contains()subList().
  • Za pomocą znaku HashMap można mapować klucze na wartości. Na przykład:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • Deklaruj stałe czasu kompilacji za pomocą słowa kluczowego const. Możesz umieścić je na najwyższym poziomie, uporządkować w obiekcie singleton lub umieścić w obiekcie towarzyszącym.
  • Obiekt towarzyszący to obiekt singleton w definicji klasy, zdefiniowany za pomocą słowa kluczowego companion.
  • Funkcje i właściwości rozszerzenia mogą dodawać funkcje do klasy. Na przykład:
    fun String.hasSpaces() = find { it == ' ' } != null
  • Odbiornik dopuszczający wartość null umożliwia tworzenie rozszerzeń klasy, które mogą być null. Operatora ?. można połączyć z operatorem apply, aby przed wykonaniem kodu sprawdzić, czy występuje null. Na przykład:
    this?.apply { println("removing $this") }

Dokumentacja języka Kotlin

Jeśli chcesz uzyskać więcej informacji na dowolny temat w tym kursie lub jeśli napotkasz trudności, najlepszym punktem wyjścia będzie strona https://kotlinlang.org.

Samouczki dotyczące języka Kotlin

Witryna https://try.kotlinlang.org zawiera rozbudowane samouczki Kotlin Koans, internetowy interpreter i pełny zestaw dokumentacji z przykładami.

Kurs Udacity

Aby obejrzeć kurs Udacity na ten temat, zobacz Kotlin Bootcamp for Programmers (w języku angielskim).

IntelliJ IDEA

Dokumentację IntelliJ IDEA znajdziesz w witrynie JetBrains.

W tej sekcji znajdziesz listę możliwych zadań domowych dla uczniów, którzy wykonują ten moduł w ramach kursu prowadzonego przez instruktora. Nauczyciel musi:

  • W razie potrzeby przypisz pracę domową.
  • Poinformuj uczniów, jak przesyłać projekty.
  • Oceń zadania domowe.

Instruktorzy mogą korzystać z tych sugestii w dowolnym zakresie i mogą zadawać inne zadania domowe, które uznają za odpowiednie.

Jeśli wykonujesz ten kurs samodzielnie, możesz użyć tych zadań domowych, aby sprawdzić swoją wiedzę.

Odpowiedz na te pytania

Pytanie 1

Która z tych funkcji zwraca kopię listy?

▢ add()

▢ remove()

▢ reversed()

▢ contains()

Pytanie 2

Która z tych funkcji rozszerzających w class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) spowoduje błąd kompilatora?

▢ fun AquariumPlant.isRed() = color == "red"

▢ fun AquariumPlant.isBig() = size > 45

▢ fun AquariumPlant.isExpensive() = cost > 10.00

▢ fun AquariumPlant.isNotLeafy() = leafy == false

Pytanie 3

Które z tych miejsc NIE jest miejscem, w którym można zdefiniować stałe za pomocą const val?

▢ na najwyższym poziomie pliku

▢ na zajęciach regularnych,

▢ w obiektach singleton

▢ w obiektach towarzyszących

Przejdź do następnej lekcji: 5.2 Typy generyczne

Omówienie kursu, w tym linki do innych ćwiczeń, znajdziesz w artykule „Kotlin Bootcamp for Programmers: Welcome to the course” (w języku angielskim).