Przeciążanie operatorów. W Kotlinie i nie tylko

Zalety, wady i praktyczne zastosowania przeciążania operatorów.

Przeciążanie operatorów jest dość kontrowersyjnym tematem. Istnieje spore grono osób, które uważają je za coś złego. Ja osobiście uważam, że w określonych przypadkach jest bardzo przydatne, wręcz niezastąpione. Poniżej przedstawiam kilka argumentów „za” i „przeciw”, i uzupełniam przykładami. Na końcu pokażę jak wygląda przeciążanie operatorów w Kotlinie. I tutaj spoiler – oczywiście bardzo prosto 😉

Jeśli interesuje Cię tylko strona techniczna – jak przeciążać operatory w Kotlinie – przewiń od razu na koniec tego wpisu.

Zaczynamy!

Argumenty „przeciw” przeciążaniu operatorów

Zacznijmy od negatywów – problemów których może przysporzyć przeciążanie operatorów.

Nieumiejętne przeciążanie

Pierwsza rzecz, jaka przychodzi mi do głowy, to nieumiejętne przeciążanie operatorów. Np. gdy ktoś pod operatorem + tak naprawdę odejmuje, albo pod operatorem * robi sumę.

Ale czy to w ogóle jest argument? Tak samo niebezpieczne jest dziedziczenie, czy rzucanie wyjątków. Właściwie każda konstrukcja językowa może być niepoprawnie użyta, ale czy to znaczy, że mamy ją usuwać? Po to mamy Code Review, żeby takie rzeczy wyłapywać i tępić.

Debugowanie +, -, *, / jest nieintuicyjne

Rozumiem w pewnym stopniu argument, iż po prostu przyzwyczailiśmy się, że operatory typu +, -, *, / są jakby integralną, niezmienną częścią języka. W tym sensie, że patrząc na fragment kodu c=a+b, nigdy by nam nie przyszło do głowy, że ten + może być tak naprawdę przeciążoną funkcją. I dlatego popatrzenie w jej źródło będzie ostatnią rzeczą, o jakiej pomyślimy podczas debugowania.

Rozumiem, ale tak samo mówiono kiedyś o GC w Javie, dziedziczeniu wielobazowym, rozszerzeniach, czy choćby słówku var w C#. Zawsze znajdą się malkontenci snujący fatalistyczne wizje, że „teraz to ten język zejdzie na psy…” A reszta po prostu robi swoje, i „z głową” korzysta z nowych „ficzerów” 😉

Tworzenie nowych, magicznych operatorów

Tu oczywiście puszczamy oko w stronę Scali, w której dowolny ciąg znaków możemy uczynić operatorem 😉 Podpisuję się obiema rękami, że taki kod czyta się beznadziejnie. Nikomu nie polecam.

Na szczęście, w Kotlinie nie możemy tworzyć nowych operatorów. Mamy do dyspozycji zestaw istniejących, i tylko je możemy przeciążać.

Argumenty „za” przeciążaniem operatorów

Tych będzie trochę więcej, gdyż możemy omówić konkretne przykłady.

Algebra na typach niestandardowych

Przykład z życia: Unity3D, w którym sporo swego czasu kodowałem, wykorzystuje przeciążone operatory bardzo intensywnie. Możemy mnożyć i dzielić wektory (Vector3) przez skalary (int, float etc.), wektory przez wektory, mnożyć wektory przez kąty (Quaternion), dodawać i odejmować wektory itd. Wyobraź sobie, jak by wyglądał poniższy kod, gdyby C# nie miał przeciążania operatorów?

// C#
// Script for simulating magnetic force

float magneticForce = 1f;

foreach (var coin in allCoins) {
	Vector3 magnetPosition = transform.position;
	Vector3 coinPosition = coin.transform.position;

	Vector3 direction = magnetPosition - coinPosition;
	float magnitude = direction.magnitude;
	Vector3 dirNormal = direction / magnitude;

	float forceDelta = magneticForce * Time.deltaTime / magnitude;

	coinPosition += dirNormal * forceDelta;
}

BigDecimal

Tu chyba nie ma co dyskutować. Chyba każdy przy zdrowych zmysłach wolałby pisać:

x = a/b + c/d

zamiast:

x = a.divide(b).add(c.divide(d))

Od razu pomyślałem, że to świetny przykład, na którym mógłbym zrobić tutorial o przeciążaniu operatorów! Jednak szybko okazało się, że Kotlin posiada takie rozszerzenie out-of-the-box, wiec nawet nie musimy go pisać 😉

Algebra na kolekcjach

Kolejna rzecz, która po prostu bardzo zwiększa czytelność kodu. I również jest  zawarta w bibliotece standardowej Kotlina. Także poniższy kod jest zupełnie poprawny:

val l1 = listOf(1, 2, 3)
val l2 = listOf(4, 5, 6)
val even = listOf(2, 4, 6)
val x = 10

val l3 = l1 + l2        // 1, 2, 3, 4, 5, 6
val l4 = l3 - even      // 1, 3, 5
val l5 = l4 + x         // 1, 3, 5, 10

 

Przeciążanie operatorów w Kotlinie

Zacznijmy od kilku ważnych założeń:

  1. W Kotlinie nie możemy dodawać nowych operatorów, a jedynie dostarczać własne implementacje dla z góry ustalonej listy istniejących operatorów (np. +,-,*,/)
  2. Możemy dostarczać implementacje zarówno dla naszych własnych typów, jak i typów już istniejących (np. BigDecimal)

Jak to robimy? Bardzo prosto. Tworzymy jedynie funkcję (lub rozszerzenie) dla danego typu, o konkretnej, z góry ustalonej nazwie (np. plus() dla operatora +). Dodatkowo taka funkcja (rozszerzenie) musi być poprzedzona słówkiem operator.

Rozpatrzmy to na przykładzie hipotetycznej klasy Vector. Poniżej zamieściłem jej definicję, zawierającą operatory + i -, oraz ich użycie:

class Vector(val x: Double, val y: Double, val z: Double) {

    operator fun plus(other: Vector): Vector {
        return Vector(
                x + other.x,
                y + other.y,
                z + other.z)
    }

    operator fun minus(other: Vector): Vector {
        return Vector(
                x - other.x,
                y - other.y,
                z - other.z)
    }

}

//Użycie:

val v1 = Vector(1.0, 2.0, 3.0)
val v2 = Vector(4.0, 5.0, 6.0)

val sum = v1 + v2
val diff = v1 - v2

Jeżeli chcielibyśmy np. umożliwić mnożenie wektora przez skalar, to tworzymy funkcję podobną do poniższej:

operator fun times(other: Double): Vector {
    return Vector(
            x * other,
            y * other,
            z * other)
}

// Użycie:
val triple = Vector(1.0, 2.0, 3.0) * 3.0

Przeciążanie operatorów dla typów wbudowanych

W przypadku, gdy chcemy przeciążyć operator na typie, do którego kodu nie mamy dostępu, możemy to zrobić za pomocą rozszerzenia. Przykładowo, implementacja operatora + dla typu BigDecimal mogłaby wyglądać tak:

operator fun BigDecimal.plus(other: BigDecimal) : BigDecimal {
    return this.add(other)
}

To tyle na dziś.

Podsumowując, tak jak pisałem wcześniej: przeciążanie operatorów – jak każde inne narzędzie – używane odpowiednio jest bardzo przydatne, zwiększa czytelność kodu, i naszą produktywność.

Do następnego wpisu!

Nie przegap kolejnych wpisów - subskrybuj!

Dodaj komentarz