Programowalny ketchup – operatory
Jak można nazwać języka jak ketchup? Ano można, a dziś pierwszy wpis na temat Kotlina, czyli języka od JetBrains działającego na JVM. Przez pewien czas zastanawiałem się jak podejść do tematu i co na początek pokazać, żeby było ciekawie. Z jednej strony są data class, które są fajne. Z drugiej specyficzne modyfikatory dostępu, które w świecie JVM mają sens. Jednak koniec końców stwierdziłem, że zacznę od czegoś co w Javie nie występuje, ale było by bardzo przydatne, czyli od przeciążania operatorów.
Problem wersjonowania
Załóżmy, że mamy sobie klasę Report, która trzyma sobie wersję, która to wersja będzie podnoszona i tylko podnoszona. W javie realizacja tego zadania polega najprościej na wprowadzeniu klasy Version (bo gołe longi są chujowe), która będzie niosła w sobie odpowiednie informacje.
Kotlin pozwala na zrobienie tego trochę inaczej. Na początek zdefiniujmy sobie data class (taki semantyczny value object) Version, która będzie miała tylko jedno pole:
Listing 1. Klasa Version w Kotlin
data class Version(val version: Long) {
init {
if (version
<p>Teraz chciałbym móc w jakiś łatwy sposób podbijać numer wersji. Można dodać metodę tak jak w Javie, ale po co skoro można przeciążyć operator? Mechanizm przeciążania operatorów w Kotlinie jest dość prosty. Jest lista zdefiniowanych funkcji, które należy dopisać do klasy w odpowiedni sposób i już mamy możliwość użycia go w naszym kodzie. Dorzućmy wiec do naszej klasy wsparcie dla operatora <samp>+a</samp>:</p>
<p class="listing">Listing 2. Klasa <samp>Version</samp> z operatorem</p>kotlin
data class Version(val version: Long) {
init {
if (version
<p>Metoda <samp>unaryPlus</samp> pozwala na użycie operatora <samp>+</samp> w stosunku do naszego obiektu. Przykładowy test sprawdzający nasz pomysł:</p>
<p class="listing">Listing 3. Test działania operatora <samp>+</samp> w klasie <samp>Version</samp></p>kotlin
class VersionTest() {
@Test
fun shouldUnaryPlusIncreaseVersionByOne(){
val v0:Version = Version(0);
val v1:Version = +v0;
Assertions.assertThat(v1.version).isEqualTo(1L);
Assertions.assertThat(v1).isNotSameAs(v0);
}
}
<p>Wszystko ładnie, ale to nie jest tak oczywisty zapis jak użycie dobrze znanego <samp>++</samp>. W tym celu dodajmy do naszej klasy kolejną metodę - <samp>inc</samp>:</p>
<p class="listing">Listing 4. Definiowanie operatora <samp>++</samp> w klasie <samp>Version</samp></p>kotlin
data class Version(val version: Long) {
//...
operator fun inc():Version{
return copy(version + 1);
}
}
<p>Tyle tylko, że ten operator działa inaczej niż <samp>+</samp> co spowoduje mały problem.</p>
<p class="listing">Listing 5. Test działania operatora <samp>++</samp> w klasie <samp>Version</samp></p>kotlin
class VersionTest() {
@Test
fun shouldIncIncreaseVersionByOne(){
val v0:Version = Version(0);
val v1:Version = v0++;
Assertions.assertThat(v1.version).isEqualTo(1L);
Assertions.assertThat(v1).isNotSameAs(v0);
}
}
<p>Ten kod się nie skompiluje ponieważ operator <samp>++</samp> wartość zmiennej zatem zmieńmy <samp>val</samp> na <samp>var</samp></p>
<p class="listing">Listing 6. Test działania operatora <samp>++</samp> w klasie <samp>Version</samp></p>kotlin
class VersionTest() {
@Test
fun shouldIncIncreaseVersionByOne(){
var v0:Version = Version(0);
val v1:Version = v0++;
Assertions.assertThat(v1.version).isEqualTo(1L);
Assertions.assertThat(v1).isNotSameAs(v0);
}
}
<p> i zobaczmy co się stanie:</p>
<p class="listing">Listing 7. Wyniki testu</p>bash
org.junit.ComparisonFailure:
Expected :1L
Actual :0L
<p>I jest to rozsądne ponieważ najpierw przypisaliśmy wartość do <samp>v1</samp> równą <samp>v0</samp>, a potem podbiliśmy <samp>v0</samp>. Czyli wiemy już jak działa postinkrementacja w kotlinie (jak działa w <a href="http://www.zielonasowa.pl/przygody-tappiego-z-szepczacego-lasu-cz-1-7135.html">Szepczącym Lesie</a>? oto jest pytanie). Zmieńmy nasz kod na preinkrementację:</p>
<p class="listing">Listing 8. Preinkrementacja</p>kotlin
class VersionTest() {
@Test
fun shouldIncIncreaseVersionByOne(){
var v0:Version = Version(0);
val v1:Version = ++v0;
Assertions.assertThat(v1.version).isEqualTo(1L);
Assertions.assertThat(v1).isNotSameAs(v0);
}
}
<p>Odpalamy i otrzymujemy</p>
<p class="listing">Listing 9. Wyniki testu preinkrementacji</p>bash
java.lang.AssertionError: expected not same:<Version(version=1)>
<p>I to też jest zrozumiałe ponieważ najpierw podbiliśmy <samp>v0</samp>, a następnie przypisaliśmy wynik do <samp>v1</samp>. Czyli preinkrementacja działa też w rozsądny sposób. Zatem nasz kod testu w wersji finalnej będzie wyglądał tak</p>
<p class="listing">Listing 10. Działajkacy test</p>kotlin
class VersionTest() {
@Test
fun shouldIncIncreaseVersionByOne(){
var v0:Version = Version(0);
val v1:Version = v0;
v1++
Assertions.assertThat(v0.version).isEqualTo(0L);
Assertions.assertThat(v1.version).isEqualTo(1L);
Assertions.assertThat(v1).isNotSameAs(v0);
}
}
<p>Możemy tu zaobserwować ważną rzecz. Operator <samp>++</samp> (oraz <samp>--</samp>) dotyka tylko zmiennej na rzecz której został wywołany. Obiekt reprezentowany przez tą zmienną nie podlega podmianie chyba, że funkcja <samp>inc</samp> (albo <samp>dec</samp>) explicite zmienia coś w obiekcie. Oznacza to też, że funkcje te muszą zwracać obiekt o co najmniej tym samym typie co typ w którym zostały zdefiniowane, mogą oczywiście zwrócić podtyp. </p>
<p>W przypadku operatora <samp>+a</samp> (i innych) możemy zwrócić cokolwiek. Reszta w <a href="http://kotlinlang.org/docs/reference/operator-overloading.html">dokumentacji</a>.</p>
<h4>Podsumowanie</h4>
<p>Kotlin pozwala na przeciążanie istniejących operatorów. To duży plus ponieważ pozwala to na uproszczenie zapisu pewnych fragmentów kodu. Jednocześnie nie możemy definiować własnych operatorów np. próba zdefiniowania <samp>::</samp> albo <samp>→</samp> nie przejdzie. Jest to zachowanie pośrednie pomiędzy czystą Javą gdzie nie można było przeciążać operatorów, a Scalą gdzie można przeciążać operatory jak i definiować nowe (technicznie są to "dziwne" nazwy metod). </p>