Kurs Kotlin dla programistów 5.1: Rozszerzenia

Te ćwiczenia są częścią kursu Kotlin dla programistów. Skorzystaj z tego kursu, jeśli będziesz wykonywać kolejno kilka ćwiczeń z programowania. W zależności od Twojej wiedzy możesz mieć dostęp do niektórych sekcji. Ten kurs jest przeznaczony dla programistów, którzy znają język zorientowany na obiekty i chcą się dowiedzieć Kotlina.

Wstęp

Dzięki nim dowiesz się wielu przydatnych funkcji Kotlina, takich jak pary, kolekcje i funkcje rozszerzeń.

Lekcje nie obejmują jednej przykładowej aplikacji, ale mają one na celu pogłębianie wiedzy. Między innymi należy jednak pamiętać o zależności od siebie. W połączeniu wiele z przykładów wykorzystuje motyw akwariowy. A jeśli chcesz zobaczyć pełne informacje o oceanarium, zapoznaj się z kursem Kotlin Bootcamp for Programmers (Ukocity) na platformie Udacity.

Co musisz wiedzieć

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

Czego się nauczysz

  • Jak pracować z parami i potrójnie
  • Więcej informacji o kolekcjach
  • Definiowanie i używanie stałych
  • Funkcje rozszerzeń tekstu

Jakie zadania wykonasz:

  • Dowiedz się więcej o parach, potrójnych i mapach haszujących w programie REPL
  • Poznaj różne sposoby organizowania stałych
  • Tworzenie funkcji rozszerzenia i właściwości rozszerzenia

W tym zadaniu poznasz pary i trójki oraz zniszczysz je. Pary i potrójne to gotowe klasy danych dla dwóch lub trzech produktów ogólnych. Może to być przydatne na przykład wtedy, gdy funkcja zwraca więcej niż jedną wartość.

Załóżmy, że masz List ryby i funkcję isFreshWater(), która sprawdza, czy jest to ryba słodkowodna lub słonowodna. List.partition() zwraca dwie listy: jedną z elementami o warunku true, a drugą z elementami, 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 trzy

  1. Otwórz REPL (Narzędzia > Kotlin > Kotlin REPL).
  2. Utwórz parę, łącząc instrument z tym, do czego jest używany, i wydrukuj wartości. Można utworzyć parę, tworząc wyrażenie łączące 2 wartości, np. 2 ciągi, ze słowem kluczowym to, a następnie używając .first lub .second do odwoływania się do każdej z nich.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. Utwórz potrójny wydruk i użyj go w systemie toString(), a następnie przekonwertuj go na listę za pomocą atrybutu toList(). Tworzysz potrójnie element Triple() za pomocą 3 wartości. Użyj .first, .second i .third, aby odwołać się do każdej wartości.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

W przykładach powyżej zastosowano ten sam typ wszystkich części pary lub potrójnego układu, ale nie jest to wymagane. Częścią może być na przykład ciąg, liczba lub lista – nawet inna para lub potrójna para.

  1. Utwórz parę, w której pierwsza jej część jest 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. Zniszcz niektóre pary i potrójne elementy

Dzielenie par na pary na potrójne jest nazywane niszczeniem. Przypisz parę lub potrójnie do odpowiedniej liczby zmiennych, a Kotlin przypisze wartość każdej części w określonej kolejności.

  1. Zdeformuj 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. Zdeformuj potrójnie 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 niszczenie par i potrójnie działa tak samo jak w przypadku klas danych, które były opisane w poprzednim ćwiczeniu z programowania.

W tym zadaniu dowiesz się więcej o kolekcjach (w tym listach) oraz o nowym typie kolekcji (mapy).

Krok 1. Więcej informacji o listach

  1. Listy i listy możliwe do zmodyfikowania zostały wprowadzone we wcześniejszej lekcji. To bardzo przydatna struktura danych, więc Kotlin udostępnia wiele funkcji wbudowanych dla list. Przejrzyj tę częściową listę funkcji list. Pełne informacje znajdziesz w dokumentacji Kotlin dla List i MutableList.

Funkcja

Cel

add(element: E)

Dodaj element do listy zmiennej.

remove(element: E)

Usuń element z listy, którą można zmienić.

reversed()

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

contains(element: E)

Zwróć wartość true, jeśli lista zawiera ten element.

subList(fromIndex: Int, toIndex: Int)

Zwróć część listy od pierwszego indeksu do drugiego, bez uwzględnienia drugiego.

  1. Pracując w REPL, utwórz listę numerów i wywołaj ją sum(). To suma wszystkich elementów.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Utwórz listę ciągów znaków i zsumuj ją.
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 dokładny List, np. ciąg znaków, możesz określić sposób jego zsumowania za pomocą funkcji .sumBy() z funkcją lambdy, na przykład aby zsumować długość każdego ciągu. Domyślna nazwa argumentu lambdy to it. W tym miejscu it odwołuje się do każdego elementu listy, ponieważ przewinięta jest lista.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. Z listami można robić znacznie więcej. Aby sprawdzić, czy funkcja jest dostępna, należy utworzyć listę w IntelliJ IDEA, dodać kropkę, a następnie sprawdzić listę autouzupełniania w etykietce. Sprawdza się to w przypadku wszystkich obiektów. Spróbuj z listą.

  1. Wybierz listIterator() z listy, a następnie przejrzyj listę przy użyciu instrukcji for i wydrukuj wszystkie elementy rozdzielone spacjami.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

Krok 2. Wypróbuj mapy hasz

Za pomocą usługi hashMapOf() w Kotlin możesz niemal zmapować wszystko, co tylko zechcesz. Mapy haszowania są jak lista par, w której pierwsza wartość jest kluczem.

  1. Utwórz mapę z haszowaniem odpowiadającą wartościom, objawom i kluczowym chorobom ryb.
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. Następnie możesz pobrać wartość choroby na podstawie objawu, używając get() lub nawet krótszych nawiasów kwadratowych [].
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. Spróbuj określić objaw, który nie występuje na mapie.
println(cures["scale loss"])
⇒ null

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

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

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

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

Zamiast zwracania prostej wartości domyślnej tworzony jest kod między nawiasami klamrowymi {}. W tym przykładzie else po prostu zwraca ciąg znaków, ale może to być równie wyrafinowane jak znalezienie strony z lekami i zwrotem.

Podobnie jak mutableListOf, możesz też zrobić mutableMapOf. Zmienna mapa umożliwia umieszczanie i usuwanie elementów. Wartość zmienne oznacza po prostu możliwość zmiany, a ciągłe to brak możliwości zmiany.

  1. Utwórz mapę zasobów reklamowych, którą możesz zmieniać, mapując ciąg znaków na sprzęt na liczbę produktów. Utwórz ją z siatką rybną, a następnie dodaj 3 zbiorniki do akwariów w usłudze put() i usuń sieć rybną z 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 zadaniu poznamy stałe w Kotlinie i poznamy różne sposoby ich organizowania.

Krok 1. Porównanie kontr. i wal.

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

Wartość jest przypisana i nie można jej zmienić, co brzmi jak deklarowanie zwykłego typu val. Jaka jest różnica między const val a val? Wartość parametru const val jest określana w momencie kompilowania, gdzie wartość val jest określana podczas wykonywania programu, co oznacza, że funkcję val można przypisać w trakcie działania.

Oznacza to, że val może mieć przypisaną wartość z funkcji, ale const val nie może tego zrobić.

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

Oprócz tego const val działa tylko na najwyższym poziomie i w klasach pojedynczego typu zadeklarowanych jako object, a nie w standardowych. Za jego pomocą możesz utworzyć plik lub pojedynczy obiekt, który zawiera tylko stałe, i zaimportować je w razie potrzeby.

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

Krok 2. Utwórz obiekt towarzyszący

Kotlin nie ma pojęcia stałych stałych na poziomie klasy.

Aby zdefiniować stałe w klasie, musisz owinąć je w obiekty towarzyszące zadeklarowane za pomocą słowa kluczowego companion. Obiekt towarzyszący to w zasadzie obiekt pojedynczego tonu.

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

Podstawowa różnica między obiektami towarzyszącymi a zwykłymi obiektami to:

  • Obiekty towarzyszące są inicjowane na podstawie statycznego konstruktora klasy zawierającej, co oznacza, że są tworzone podczas tworzenia obiektu.
  • Obiekty zwykłe są leniwie inicjowane przy pierwszym dostępie do danego obiektu, czyli przy pierwszym użyciu.

To jeszcze nie wszystko, ale teraz musisz tylko pamiętać o umieszczeniu stałych w klasach obiektów towarzyszących.

W tym zadaniu nauczysz się rozszerzać zachowanie zajęć. Zapisywanie funkcji użytkowych w celu rozszerzenia zakresu klasy jest bardzo powszechne. Kotlin udostępnia wygodną składnię tych funkcji:

Funkcje rozszerzeń umożliwiają dodawanie funkcji do istniejących klas bez konieczności otwierania kodu źródłowego. Możesz na przykład zadeklarować je w pliku Extensions.kt, który jest częścią Twojego pakietu. Nie powoduje to zmodyfikowania zajęć, ale pozwala wywołać notację w postaci kropek w obiektach tej klasy.

Krok 1. Napisz funkcję rozszerzenia

  1. Nadal w REPL zapisuj prostą funkcję rozszerzenia hasSpaces(), aby sprawdzić, czy ciąg zawiera spacje. Nazwa funkcji jest poprzedzona klasą, w której działa. W ramach funkcji this odnosi się do obiektu, który jest wywoływany, a it 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(). this nie jest jawnie potrzebny, a funkcję można zmniejszyć do jednego wyrażenia i zwrócić, więc nawias klamrowy {} wokół niego nie jest potrzebny.
fun String.hasSpaces() = find { it == ' ' } != null

Krok 2. Poznaj ograniczenia rozszerzeń

Funkcje rozszerzeń mają dostęp tylko do publicznego interfejsu API klasy, której wersję można rozszerzyć. Nie można uzyskać dostępu do zmiennych private.

  1. Spróbuj dodać funkcje rozszerzeń do usługi oznaczonej etykietą 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. Przejrzyj kod i zastanów się, 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() odbitka GreenLeafyPlant. aquariumPlant.print() może też wydrukować plik GreenLeafyPlant, ponieważ ma przypisaną wartość plant. Jednak typ jest usuwany w momencie kompilacji, więc AquariumPlant zostaje wydrukowany.

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

Oprócz funkcji rozszerzenia Kotlin umożliwia też dodawanie właściwości rozszerzenia. Podobnie jak w przypadku funkcji rozszerzenia, klasy, których wartość rozszerzasz, podajesz kropkę, po której następuje nazwa właściwości.

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

Dostęp do właściwości isGreen można uzyskać tak samo jak standardową usługę. Po uzyskaniu dostępu do metody pobierany jest element pobierający isGreen, który pobiera wartość.

  1. Wydrukuj właściwość isGreen zmiennej aquariumPlant i zaobserwuj wynik.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

Krok 4. Poznaj adresatów o wartości null

Rozszerzona klasa jest nazywana odbiornikiem i możesz ją przypisać do wartości null. W takim przypadku zmienna this użyta w treści może mieć wartość null, więc sprawdź, czy jest odpowiednia. Użyj odbiornika o wartości null, jeśli oczekujesz, że rozmówcy będą wywoływać metodę rozszerzenia w przypadku zmiennych null lub jeśli chcesz zapewnić domyślne działanie, gdy funkcja jest stosowana do funkcji null.

  1. Nadal w środowisku REPL określ metodę pull(), która przyjmuje odbiornik o wartości null. Wskazuje to znak zapytania ? po typie przed kropką. Aby sprawdzić, czy element this nie jest null, użyj ?.apply.
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. W tym przypadku dane wyjściowe nie będą widoczne po uruchomieniu programu. plant to null, więc wewnętrzny element println() nie jest wywoływany.

Funkcje rozszerzeń są bardzo zaawansowane, a większość standardowej biblioteki Kotlin jest zaimplementowana jako funkcje rozszerzeń.

Podczas tej lekcji dowiedzieliśmy się więcej o kolekcjach i stałych oraz zdołaliśmy przedstawić potęgę i funkcje rozszerzeń.

  • Pary i trójki mogą służyć do zwracania więcej niż jednej wartości z funkcji. Przykład:
    val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin ma wiele przydatnych funkcji usługi List, takich jak reversed(), contains() i subList().
  • HashMap może służyć do mapowania kluczy na wartości. Przykład:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • Zadeklaruj stałe czasy kompilacji za pomocą słowa kluczowego const. Możesz umieścić je na najwyższym poziomie, umieścić w pojedynczym obiekcie lub umieścić w elemencie towarzyszącym.
  • Obiekt towarzyszący to pojedynczy obiekt w definicji klasy zdefiniowany za pomocą słowa kluczowego companion.
  • Funkcje i właściwości rozszerzeń mogą dodawać funkcje do klasy. Przykład:
    fun String.hasSpaces() = find { it == ' ' } != null
  • Odbiorca null umożliwia tworzenie rozszerzeń, które mogą mieć null. Operator ?. można sparować z systemem apply, aby sprawdzać kod null przed wykonaniem kodu. Przykład:
    this?.apply { println("removing $this") }

Dokumentacja Kotlin

Jeśli chcesz dowiedzieć się więcej na dowolny temat dotyczący tego kursu lub nie wiesz, co zrobić, wejdź na https://kotlinlang.org.

Samouczki Kotlin

Na stronie https://try.kotlinlang.org znajdziesz szczegółowe samouczki o nazwie Kotlin Koans, internetowe narzędzie do tłumaczenia, a także kompletny zestaw dokumentacji referencyjnej z przykładami.

Kurs Udacity

Kurs Udacity na ten temat znajdziesz na kursie Kotlin dla programistów.

IntelLIJ IDEA

Dokumentację IntelliJ IDEA znajdziesz na stronie JetBrains.

Ta sekcja zawiera listę możliwych zadań domowych dla uczniów, którzy pracują w ramach tego ćwiczenia w ramach kursu prowadzonego przez nauczyciela. To nauczyciel może wykonać te czynności:

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

Nauczyciele mogą wykorzystać te sugestie tak długo, jak chcą lub chcą, i mogą przypisać dowolne zadanie domowe.

Jeśli samodzielnie wykonujesz te ćwiczenia z programowania, możesz sprawdzić swoją wiedzę w tych zadaniach domowych.

Odpowiedz na te pytania

Pytanie 1

Która z poniższych odpowiedzi zwraca kopię listy?

add()

remove()

reversed()

contains()

Pytanie 2

Która z tych funkcji rozszerzenia 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óra z poniższych pozycji nie jest miejscem, w którym można definiować stałe wartości za pomocą atrybutu const val?

▢ na najwyższym poziomie pliku

▢ w zwykłych zajęciach

▢ w obiektach Singleton

▢ w obiektach towarzyszących

Przejdź do następnej lekcji: 5.2 Informacje ogólne

Omówienie kursu, w tym linki do innych ćwiczeń z programowania, znajdziesz w artykule "Kotlin Bootcamp for Programmers: Witamy na kursie."