programowanieProgramista Kotlin

Jak jest zaimplementowane słowo kluczowe 'inline' w kontekście klas (value class/inline class) w Kotlinie? Jakie ograniczenia istnieją, jak działają takie klasy na poziomie bajtcode'u, kiedy i dlaczego warto je stosować? Podaj przykład i wyjaśnij typowe trudności.

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

W Kotlinie inline class (od wersji Kotlin 1.5 — termin "value class"), pozwalają na tworzenie opakowań dla typów z minimalnymi kosztami. Podczas kompilacji takie klasy są w tle zamieniane na swoją wewnętrzną wartość (value), aby uniknąć kosztów związanych z tworzeniem obiektów.

Ograniczenia i cechy:

  • Może mieć tylko jedną właściwość w primary constructor.
  • Nie można przechowywać referencji dla equality referencyjnej (=== nie działa jak zwykle).
  • Value class nie może być klasą dziedziczącą, ani nie może mieć stanu oprócz swojej wartości.
  • Nie wszystkie typy generyczne i API platformowe mogą działać z inline/value class bez boxing.
  • Value class nie może mieć bloku init, pól oprócz value oraz tylko minimalne funkcje.

Przykład:

@JvmInline value class UserId(val value: String) fun getUser(id: UserId) { println("Loading user with id: ${id.value}") } val id = UserId("XYZ") getUser(id) // W tle działa po prostu jako String!

Kiedy używać:

  • Zapewnienie bezpieczeństwa typów dla identyfikatorów, specjalnych wartości.
  • Poprawa wydajności podczas pracy z milionami takich opakowań (obiekty nie są tworzone).

Pytanie z podstępem

Czy można dziedziczyć po value class lub używać go w hierarchii interfejsów/abstrakcyjnych klas?

Odpowiedź: Nie, value class nie może dziedziczyć po innych klasach (z wyjątkiem interfejsów), nie może być otwarta dla dziedziczenia, nie dopuszcza bloku init i innych pól niestatycznych. Jedyną dostępną opcją jest implementacja interfejsów.

Przykład:

interface Validatable { fun isValid(): Boolean } @JvmInline value class Email(val raw: String) : Validatable { override fun isValid() = raw.contains("@") }

Przykłady rzeczywistych błędów z powodu nieznajomości szczegółów tematu


Historia

Aplikacja na Androida znacznie wydłużyła czas uruchamiania po dodaniu value class do parametrów Parcelable: okazało się, że niepoprawny @Parcelize z value class prowadził do boxing/unboxing na każdym etapie serializacji, psując zalety inline.


Historia

Mikrousługa zaczęła aktywnie używać value class dla UserId i ProductId w celu zapewnienia bezpieczeństwa typów, ale w wielu miejscach funkcje generyczne wymagały refleksji, która nie działała z "opakowaniem". Testy jednostkowe nagle zaczęły zawodzić, pojawiły się ClassCastException.


Historia

Kod migrowany z Javy zaczął zastępować wewnętrzne klasy domenowe na value class w celu optymalizacji, ale ich użycie jako pól nullable prowadziło do niespodziewanych wyjątków null pointer, ponieważ value class może być null tylko wtedy, gdy zewnętrzna wartość też jest null, co łamało stare inwarianty.