Przeładowanie operatorów w Kotlinie
W Kotlinie możemy dodać element do listy za pomocą operatora +
. W ten sam sposób dodajemy do siebie dwa ciągi znaków. Sprawdzamy, czy kolekcja zawiera element, używając operatora in
. Możemy również dodawać, odejmować lub mnożyć elementy typu BigDecimal
, czyli klasy JVM używanej do reprezentowania potencjalnie ogromnych liczb o nieograniczonej precyzji. To wszystko to użycia operatorów.
Stosowanie operatorów jest możliwe dzięki funkcjonalności Kotlina o nazwie przeładowanie operatorów, która pozwala na zdefiniowanie specjalnych metod, które mogą być używane jako operatory. Zobaczmy to na przykładzie własnej klasy.
Przykład przeładowania operatorów
Załóżmy, że musisz reprezentować liczby zespolone w swojej aplikacji. Jest to specjalny typ liczb w matematyce, reprezentowany przez dwie części: rzeczywistą i urojoną. Liczby zespolone są użyteczne w różnego rodzaju obliczeniach w fizyce i inżynierii.
W matematyce istnieje szereg operacji, które możemy wykonać na liczbach zespolonych. Na przykład możemy dodać dwie liczby zespolone lub odjąć liczbę zespoloną od innej liczby zespolonej. Robi się to za pomocą operatorów +
i -
. W związku z tym rozsądne jest, abyśmy obsługiwali te operatory dla naszej klasy Complex
. Aby obsłużyć operator +
, musimy zdefiniować metodę mającą modyfikator operator
o nazwie plus
i pojedynczy parametr. Aby obsłużyć operator -
, musimy zdefiniować metodę mającą modyfikator operator
o nazwie minus
i pojedynczy parametr.
Użycie operatorów +
i -
jest równoznaczne z wywołaniem funkcji plus
i minus
. Obie opcje można stosować zamiennie.
Kotlin definiuje konkretny zestaw operatorów, dla każdego z nich określona jest nazwa i liczba obsługiwanych argumentów. Dodatkowo wszystkie operatory muszą być metodami oraz mieć modyfikator operator
.
Dobrze używane operatory mogą pomóc nam poprawić czytelność kodu tak samo jak źle używane operatory mogą jej zaszkodzić1. Przedyskutujmy wszystkie operatory w Kotlinie.
Operatory arytmetyczne
Zacznijmy od operatorów arytmetycznych, takich jak plus czy razy. Poniższa tabela prezentuje, jak przekształcane jest użycie konkretnych operatorów przez kompilator Kotlina.
Wyrażenie | Przekształca się na |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b) |
a..b | a.rangeTo(b) |
a..<b | a.rangeUntil(b) |
Zauważ, że %
przekształca się na rem
, co jest skrótem od "remainder", czyli "reszta". Ten operator zwraca resztę pozostałą po podzieleniu jednego operandu5 przez drugi operand, więc jest podobny do operacji modulo0.
Także ..
oraz ..<
są operatorami, odpowiadającymi metodom rangeTo
i rangeUntil
. Służą do zdefiniowania zakresu. Jeśli użyjemy ich z liczbami całkowitymi, rezultatem jest IntRange
, po którym można iterować przy użyciu pętli for. Możemy ich także użyć pomiędzy dowolnymi wartościami implementującymi interfejs Comparable
, by zdefiniować zakres poprzez wartości skrajne.
Operator in
Jednym z moich ulubionych operatorów jest in
. Wyrażenie a in b
przekształca się na b.contains(a)
. Istnieje także !in
, które przekształca się na negację.
Wyrażenie | Przekształca się na |
---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
Jest kilka sposobów użycia tego operatora. Po pierwsze, w przypadku kolekcji można sprawdzić, czy element znajduje się na liście, zamiast sprawdzać, czy lista zawiera element.
Dlaczego mielibyśmy tak robić? Głównie dla poprawy czytelności kodu. Zapytałbyś "Czy lodówka zawiera piwo?", czy raczej "Czy w lodówce jest piwo?"? Wsparcie dla operatora in
daje nam możliwość wyboru.
Często używamy również operatora in
razem z zakresami. Wyrażenie 1..10
generuje obiekt typu IntRange
, który ma metodę contains
. Dlatego można użyć in
i zakresu, aby sprawdzić, czy liczba znajduje się w tym zakresie.
Zakres tworzymy z dowolnych obiektów, które są porównywalne, a wynikowy ClosedRange
również ma metodę contains
. Dlatego można użyć sprawdzania zakresu dla dowolnych obiektów, które są porównywalne, takich jak duże liczby czy obiekty reprezentujące czas.
Operator iterator
Można użyć pętli for do iterowania po dowolnym obiekcie, który ma metodę operatora iterator
. Każdy obiekt implementujący interfejs Iterable
musi obsługiwać metodę iterator
.
Zwróć uwagę, że istnieją obiekty iterowane, które nie implementują interfejsu Iterable
. Map
jest świetnym tego przykładem. Nie implementuje interfejsu Iterable
, jednak można po nim iterować, używając pętli for. Jak to możliwe? Dzięki operatorowi iterator
, który jest zdefiniowany jako funkcja rozszerzająca w bibliotece standardowej Kotlin.
Aby lepiej zrozumieć, jak działa pętla for, rozważ poniższy kod.
Pod maską pętla for jest kompilowana do bajtkodu, który używa pętli while do iteracji po iteratorze obiektu, jak pokazano na poniższym fragmencie kodu.
Operatory równości i nierówności
W Kotlinie występują dwa rodzaje równości:
Równość strukturalna: sprawdzana za pomocą metody
equals
lub operatora==
(i jego negowanej wersji!=
).a == b
przekłada się naa.equals(b)
, gdya
nie jest nullowalne, w przeciwnym razie przekłada się naa?.equals(b) ?: (b === null)
. Równość strukturalna jest zwykle preferowana nad równością referencyjną. Metodęequals
można nadpisać w niestandardowej klasie.Równość referencyjna: sprawdzana za pomocą operatora
===
(i jego negowanej wersji!==
); zwracatrue
, gdy obie strony wskazują na ten sam obiekt.===
i!==
(sprawdzenia tożsamości) nie są przeciążalne, a więc zawsze sprawdzają, czy dwa obiekty mają ten sam adres w pamięci.
Ponieważ equals
jest zaimplementowane w Any
, które jest nadklasą każdej klasy, możemy sprawdzić równość dowolnych dwóch obiektów.
Wyrażenie | Przekłada się na |
---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
Operatory porównania
Niektóre klasy mają naturalny porządek, który jest używany domyślnie, gdy porównujemy dwie instancje danej klasy. Dobrym przykładem są liczby: 10 jest mniejszą liczbą niż 100. Istnieje popularna konwencja w Javie, że klasy o naturalnym porządku powinny implementować interfejs Comparable
, który wymaga metody compareTo
, która służy do porównywania dwóch obiektów.
W rezultacie istnieje konwencja, że powinniśmy porównywać dwa obiekty za pomocą metody compareTo
. Jednak bezpośrednie użycie metody compareTo
nie jest zbyt intuicyjne. Powiedzmy, że widzisz a.compareTo(b) > 0
w kodzie. Co to oznacza? Kotlin upraszcza to, czyniąc z compareTo
operator, który może być zastąpiony intuicyjnymi matematycznymi operatorami porównania: >
, <
, >=
i <=
.
Wyrażenie | Tłumaczy się na |
---|---|
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
Często używam operatorów porównania do porównywania wartości przechowywanych w obiektach typu BigDecimal
lub BigInteger
.
Lubię również porównywać odniesienia czasowe w podobny sposób (choć taka praktyka może być uważana za kontrowersyjną).
Operator indeksowania
W programowaniu istnieją dwie popularne konwencje pozwalające na pobieranie lub ustawianie elementów w kolekcjach. Pierwsza z nich używa nawiasów kwadratowych, a druga metod get
i set
. W Javie pierwszą konwencję stosujemy dla tablic, a drugą dla innych rodzajów kolekcji. W Kotlinie obie konwencje można stosować wymiennie, ponieważ metody get
i set
są operatorami, które można używać z nawiasami kwadratowymi.
Wyrażenie | Tłumaczy się na |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |
Nawiasy kwadratowe są tłumaczone do wywołań get
i set
z odpowiednią liczbą argumentów. Warianty funkcji get
i set
z większą liczbą argumentów mogą być używane przez biblioteki przetwarzania danych. Na przykład możemy mieć obiekt reprezentujący tabelę i używać nawiasów kwadratowych z dwoma argumentami: współrzędnymi x
i y
.
Przypisania z operatorem arytmetycznym
Gdy ustawiamy nową wartość dla zmiennej, często opiera się ona na poprzedniej wartości. Załóżmy, że chcemy dodać wartość do poprzedniej. W tym celu wprowadzono przypisanie z operatorem arytmetycznym, określane w języku angielskim jako augmented assignment3. Na przykład a += b
to krótsza forma a = a + b
. Istnieją podobne przypisania dla innych operacji arytmetycznych.
Wyrażenie | Tłumaczy się na |
---|---|
a += b | a = a + b |
a -= b | a = a - b |
a *= b | a = a * b |
a /= b | a = a / b |
a %= b | a = a % b |
Zauważ, że przypisania z operatorem arytmetycznym można używać dla wszystkich typów, które obsługują odpowiednią operację arytmetyczną, w tym dla list czy stringów. Takie przypisania wymagają, aby zmienna była var
, a wynik operacji matematycznej musi mieć właściwy typ (aby przetłumaczyć a += b
na a = a + b
, zmienna a
musi być var
, a a + b
musi być podtypem typu a
).
Przypisania z operatorem arytmetycznym można stosować jeszcze w inny sposób: do modyfikowania obiektów zmiennych. Na przykład możemy użyć +=
do dodania elementu do zmiennej listy. W takim przypadku a += b
tłumaczy się na a.plusAssign(b)
.
Wyrażenie | Tłumaczy się na |
---|---|
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b) |
Jeśli oba rodzaje rozszerzonego przypisania mogą być zastosowane, Kotlin domyślnie wybiera modyfikację obiektu modyfikowalnego.
Jednoargumentowe operatory przedrostkowe
Plus, minus lub negacja przed pojedynczą wartością to także operator. Operatory używane tylko z jedną wartością nazywane są operatorami jednoargumentowymi4. Kotlin obsługuje przeciążanie operatorów dla następujących operatorów jednoargumentowych:
Wyrażenie | Tłumaczenie na |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
Oto przykład przeciążania operatora unaryMinus
.
Operator unaryPlus
jest często używany w Kotlinowych DSL-ach, co opisuję szczegółowo w następnej książce tej serii – Funkcyjny Kotlin.
Inkrementacja i dekrementacja
W ramach wielu algorytmów używanych w starszych językach często musieliśmy dodawać lub odejmować wartość 1
od zmiennej, dlatego wynaleziono inkrementację i dekrementację. Operator ++
służy do dodawania 1
do zmiennej; więc jeśli a
jest liczbą całkowitą, to a++
przekłada się na a = a + 1
. Operator --
służy do odejmowania 1
od zmiennej; więc jeśli a
jest liczbą całkowitą, to a--
przekłada się na a = a - 1
.
Zarówno inkrementacja, jak i dekrementacja mogą być używane przed lub po zmiennej, a to determinuje wartość zwracaną przez tę operację.
- Jeśli użyjesz
++
przed zmienną, jest to preinkrementacja; inkrementuje zmienną, a następnie zwraca wynik tej operacji. - Jeśli użyjesz
++
po zmiennej, jest to postinkrementacja; inkrementuje zmienną, ale zwraca wartość przed operacją. - Jeśli użyjesz
--
przed zmienną, jest to predekrementacja; dekrementuje zmienną, a następnie zwraca wynik tej operacji. - Jeśli użyjesz
--
po zmiennej, jest to postdekrementacja; dekrementuje zmienną, ale zwraca wartość przed operacją.
Na podstawie metod inc
i dec
Kotlin obsługuje przeciążanie inkrementacji i dekrementacji, które powinny inkrementować lub dekrementować niestandardowy obiekt. Nigdy nie widziałem przeciążenia tych operacji w praktyce, więc myślę, że wystarczy jedynie wiedzieć, że jest to możliwe.
Wyrażenie | Tłumaczenie na (uproszczone) |
---|---|
++a | a.inc(); a |
a++ | val tmp = a; a.inc(); tmp |
--a | a.dec(); a |
a-- | val tmp = a; a.dec(); tmp |
Operator invoke
Obiekty z operatorem invoke
można wywoływać jak funkcje, czyli z nawiasami bezpośrednio po zmiennej reprezentującej ten obiekt. Wywołanie obiektu przekłada się na wywołanie metody invoke
z takimi samymi argumentami.
Wyrażenie | Tłumaczenie na |
---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
Operator invoke
jest używany dla obiektów reprezentujących funkcje, takich jak wyrażenia lambda2 lub obiekty UseCases z Clean Architecture.
Kolejność wywołania operatorów
Jaki jest wynik wyrażenia 1 + 2 * 3
? Odpowiedź brzmi 7
, a nie 9
, ponieważ w matematyce mnożenie wykonuje się przed dodawaniem. Mówimy, że mnożenie ma wyższy priorytet niż dodawanie.
Priorytet jest również niezwykle ważny w programowaniu, ponieważ gdy kompilator ewaluuje wyrażenie takie jak 1 + 2 == 3
, musi wiedzieć, czy powinien najpierw dodać 1
do 2
, czy porównać 2
i 3
. Poniższa tabela porównuje priorytety wszystkich operatorów, w tym tych, które można przeciążyć, i tych, których nie można.
Priorytet | Tytuł | Symbole |
---|---|---|
Najwyższy | Postfiksowy | ++, --, ., ?. |
Prefiksowy | -, +, ++, --, ! | |
Rzutowanie typów | as, as? | |
Mnożenie | *, /, % | |
Dodawanie | +, - | |
Zakres | .. | |
Funkcja infiksowa | simpleIdentifier | |
Elvis | ?: | |
Sprawdzenia nazwane | in, !in, is, !is | |
Porównanie | <, >, <=, >= | |
Równość | ==, !=, ===, !== | |
Koniunkcja | && | |
Alternatywa | || | |
Operator rozprzestrzeniania | * | |
Najniższy | Przypisanie | =, +=, -=, *=, /=, %= |
Czy na podstawie tej tabeli potrafisz przewidzieć, co wydrukuje poniższy kod?
To popularna zagadka Kotlin. Odpowiedź brzmi -2
, a nie 0
, ponieważ pojedynczy minus przed funkcją jest operatorem, którego priorytet jest niższy niż wywołanie metody plus
. Najpierw wywołujemy metodę, a następnie wywołujemy unaryMinus
na wyniku, więc zmieniamy z 2
na -2
. Aby użyć dosłownie -1
, umieść go w nawiasach.
Podsumowanie
W Kotlinie używamy operatorów, z których wiele można przeciążyć i wykorzystać do poprawy czytelności naszego kodu. Z poznawczego punktu widzenia używanie intuicyjnego operatora może być ogromnym ułatwieniem w porównaniu z wszechobecnym stosowaniem metod. Dlatego warto wiedzieć, jakie opcje są dostępne i być otwartym na używanie operatorów zdefiniowanych przez bibliotekę standardową Kotlina. Możemy też definiować własne operatory. Bądź jednak ostrożny, bo nieintuicyjne operatory mogą znacznie utrudniać czytanie kodu.
W następnym rozdziale pomówimy wreszcie o systemie typów stosowanych w Kotlinie i dowiemy się czym jest Nothing
.
Ten operator wcześniej nazywał się mod
, co pochodzi od "modulo", ale teraz ta nazwa jest deprecated. W matematyce operacje reszty z dzielenia i modulo działają tak samo dla liczb dodatnich, ale różnica ujawnia się dla liczb ujemnych. Wynik reszty z dzielenia -5 przez 4 to -1, ponieważ -5 = 4 * (-1) + (-1). Wynik modulo -5 przez 4 to 3, ponieważ -5 = 4 * (-2) + 3. Operator %
w Kotlinie implementuje zachowanie reszty z dzielenia, dlatego jego nazwa musiała zostać zmieniona z mod
na rem
.
Więcej na ten temat można znaleźć w Efektywny Kotlin, Temat 12: Znaczenie operatora powinno być zgodne z nazwą funkcji i Temat 13: Używaj operatorów, aby zwiększyć czytelność.
Więcej o wyrażeniach lambda będzie w kolejnej książce serii – Funkcyjny Kotlin.
Nie jestem pewien, który język wprowadził pierwszy tę konwencję, ale te operatory są obsługiwane nawet przez tak stare języki jak C.
Operatory unarne to te używane z tylko jedną wartością (operandem). Operatory używane z dwiema wartościami nazywane są operatorami binarnymi. Operatory używane z trzema wartościami nazywane są operatorami trójargumentowymi, czyli po angielsku "ternary operators". Ponieważ w głównych językach programowania istnieje tylko jeden operator trójargumentowy, mianowicie operator warunkowy, często określany jest pojęciem ternary operator. W Kotlinie jednak on nie funkcjonuje i zamiast niego używamy if
i else
.
Operandem nazywamy wartość stojącą po jednej ze stron operatora.