W języku Kotlin standardowe funkcje wyższego rzędu apply, also, let i run powstały w celu uproszczenia konfiguracji obiektów w stylu chainable oraz lokalnych zmian przy minimalnej ilości kodu szablonowego. Funkcje te ułatwiają pracę z obiektami mutowalnymi i niemutowalnymi, pozwalają na zwięzłe wyrażanie ciągów przekształceń oraz rozwiązują część problemów związanych z czasowym zakresem widoczności zmiennych.
Funkcje te zaczerpnięto z wzorca buildera i podejść do fluent interfaces. Ich pojawienie się jest odpowiedzią na dążenie do stworzenia czystszego kodu i pozbycia się zbędnych deklaracji zmiennych pomocniczych.
Tradycyjne podejście wymaga wielokrotnego odwoływania się do obiektu lub wyodrębniania zmiennych tymczasowych. To obniża czytelność i zwiększa ryzyko błędów:
val user = User() user.name = "Alex" user.age = 26 user.isActive = true
Zastosowanie funkcji apply, also, let, run zwiększa wyrazistość:
val user = User().apply { name = "Alex" age = 26 isActive = true }
Krótki opis:
data class User(var name: String = "", var age: Int = 0, var isActive: Boolean = false) val configuredUser = User().apply { name = "Alice" age = 30 isActive = true } debugUser(configuredUser.also { println("User is configured: $it") }) val emailLength = configuredUser.email?.let { it.length } ?: 0 val description = configuredUser.run { "$name ($age)" }
Kluczowe cechy:
Czy funkcja let może zmienić obiekt, z którym pracuje?
Funkcja let nie jest przeznaczona do zmiany obiektu. Raczej służy do zastosowania przekształcenia do wartości i zwraca wynik. Należy pamiętać, że wewnątrz let zamiast this dostępne jest it.
val upperName = user.name.let { it.uppercase() } // let nie zmienia usera
Jaka jest różnica między apply a run?
Obie działają z kontekstem this wewnątrz lambdy, różnica tkwi w zwracanej wartości:
// apply val building = StringBuilder().apply { append("start-") append("end") } // building to StringBuilder // run val result = StringBuilder().run { append("start-") append("end") toString() } // result to String
Czy można zagnieżdżać zastosowanie apply i let? Jeśli tak, to kiedy to jest uzasadnione?
Tak, zagnieżdżenie jest łagodnie zalecane dla agregacji lub stopniowej konfiguracji obiektów, szczególnie w przypadku pracy z nullable:
val userInfo = user?.apply { isActive = true }?.let { "${it.name} is active: ${it.isActive}" }
W całym kodzie używa się let do modyfikacji obiektu, mieszają się apply i let — w efekcie obiekt nie zmienia się tam, gdzie oczekiwano, a jego wartość ginie w łańcuchach.
Plusy:
Minusy:
Zastosowanie apply i also do konfiguracji i logowania, let tylko do pracy z nullable i uzyskiwania wyniku, run — do ciągów przekształceń wymagających obliczenia nowej wartości.
Plusy:
Minusy: