1. Общ преглед
В Kotlin функциите са първокласни граждани, така че можем да предаваме функции или да ги връщаме точно както другите нормални типове. Представянето на тези функции по време на изпълнение обаче понякога може да причини няколко ограничения или усложнения при изпълнението.
В този урок първо ще изброим два на пръв поглед несвързани въпроса за ламбдите и генеричните лекарства, а след това, след въвеждането на вградени функции , ще видим как те могат да отговорят и на тези проблеми, така че нека започнем!
2. Проблеми в рая
2.1. Над главата на агнетата в Котлин
Една от предимствата на функциите на първокласните граждани в Котлин е, че можем да предадем част от поведението на други функции. Предаването на функции като ламбда ни позволява да изразим намеренията си по по-кратък и елегантен начин, но това е само една част от историята.
За да изследваме тъмната страна на ламбда, нека преоткрием колелото, като декларираме функция за разширение за филтриране на колекции:
fun Collection.filter(predicate: (T) -> Boolean): Collection = // Omitted
Сега нека видим как горната функция се компилира в Java. Съсредоточете се върху предикатната функция, която се предава като параметър:
public static final Collection filter(Collection, kotlin.jvm.functions.Function1);
Забележете как се обработва предикатът с помощта на интерфейса Function1 ?
Сега, ако наречем това в Kotlin:
sampleCollection.filter { it == 1 }
Ще бъде създадено нещо подобно на следното, за да обгърне ламбда кода:
filter(sampleCollection, new Function1() { @Override public Boolean invoke(Integer param) { return param == 1; } });
Всеки път, когато декларираме функция от по-висок ред, ще бъде създаден поне един екземпляр от тези специални типове Функция * .
Защо Kotlin прави това, вместо, да речем, използва invokedynamic като това, което Java 8 прави с ламбда? Просто казано, Kotlin се придържа към съвместимостта на Java 6 и invokedynamic не е наличен до Java 7.
Но това не е краят му. Както може да се досетим, само създаването на екземпляр от тип не е достатъчно.
За да изпълни действително операцията, капсулирана в ламбда на Kotlin, функцията от по-висок ред - филтър в този случай - ще трябва да извика специалния метод с име invoke на новия екземпляр. Резултатът е по-висок, поради допълнителното обаждане.
Така че, за да обобщим, когато предаваме ламбда на функция, под капака се случва следното:
- Поне един екземпляр от специален тип се създава и съхранява в купчината
- Винаги се случва допълнително извикване на метод
Още едно разпределение на екземпляр и още едно повикване на виртуален метод не изглежда толкова лошо, нали?
2.2. Затваряния
Както видяхме по-рано, когато предаваме ламбда на функция, ще бъде създаден екземпляр от тип функция, подобно на анонимните вътрешни класове в Java.
Точно както при последния, ламбда израз може да получи достъп до затварянето си , т.е. променливи, декларирани във външния обхват. Когато ламбда улавя променлива от нейното затваряне, Kotlin съхранява променливата заедно с улавящия ламбда код.
Допълнителните разпределения на паметта се влошават още повече, когато ламбда улавя променлива: JVM създава екземпляр от тип функция при всяко извикване . За неприхващащи ламбди ще има само един екземпляр, единичен , от тези типове функции.
Как сме толкова сигурни в това? Нека преоткрием друго колело, като декларираме функция, която да приложи функция към всеки елемент на колекция:
fun Collection.each(block: (T) -> Unit) { for (e in this) block(e) }
Колкото и глупаво да звучи, тук ще умножим всеки елемент на колекция по произволно число:
fun main() { val numbers = listOf(1, 2, 3, 4, 5) val random = random() numbers.each { println(random * it) } // capturing the random variable }
И ако надникнем в байт кода, използвайки javap :
>> javap -c MainKt public final class MainKt { public static final void main(); Code: // Omitted 51: new #29 // class MainKt$main$1 54: dup 55: fload_1 56: invokespecial #33 // Method MainKt$main$1."":(F)V 59: checkcast #35 // class kotlin/jvm/functions/Function1 62: invokestatic #41 // Method CollectionsKt.each:(Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)V 65: return
Тогава можем да забележим от индекс 51, че JVM създава нов екземпляр на MainKt $ main $ 1 вътрешен клас за всяко извикване. Също така, индекс 56 показва как Kotlin улавя случайната променлива. Това означава, че всяка уловена променлива ще бъде предадена като аргументи на конструктора, като по този начин ще се генерира допълнителна памет.
2.3. Тип Изтриване
Що се отнася до генеричните продукти на JVM, за начало никога не е било рай! Както и да е, Kotlin изтрива общата информация за типа по време на изпълнение. Тоест, екземпляр на общ клас не запазва параметрите на типа си по време на изпълнение .
Например, когато декларираме няколко колекции като List или List, всичко, което имаме по време на изпълнение, са само сурови списъци . Това изглежда не е свързано с предишните проблеми, както беше обещано, но ще видим как вградените функции са общото решение и за двата проблема.
3. Вградени функции
3.1. Премахване на горната част на Lambdas
Когато използвате ламбда, допълнителното разпределение на паметта и допълнителното виртуално извикване на метод въвеждат някои режийни разходи. Така че, ако изпълнявахме същия код директно, вместо да използваме ламбда, нашето изпълнение би било по-ефективно.
Трябва ли да избираме между абстракция и ефективност?
As is turns out, with inline functions in Kotlin we can have both! We can write our nice and elegant lambdas, and the compiler generates the inlined and direct code for us. All we have to do is to put an inline on it:
inline fun Collection.each(block: (T) -> Unit) { for (e in this) block(e) }
When using inline functions, the compiler inlines the function body. That is, it substitutes the body directly into places where the function gets called. By default, the compiler inlines the code for both the function itself and the lambdas passed to it.
For example, The compiler translates:
val numbers = listOf(1, 2, 3, 4, 5) numbers.each { println(it) }
To something like:
val numbers = listOf(1, 2, 3, 4, 5) for (number in numbers) println(number)
When using inline functions, there is no extra object allocation and no extra virtual method calls.
However, we should not overuse the inline functions, especially for long functions since the inlining may cause the generated code to grow quite a bit.
3.2. No Inline
By default, all lambdas passed to an inline function would be inlined, too. However, we can mark some of the lambdas with the noinline keyword to exclude them from inlining:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }
3.3. Inline Reification
As we saw earlier, Kotlin erases the generic type information at runtime, but for inline functions, we can avoid this limitation. That is, the compiler can reify generic type information for inline functions.
All we have to do is to mark the type parameter with the reified keyword:
inline fun Any.isA(): Boolean = this is T
Without inline and reified, the isA function wouldn't compile, as we thoroughly explain in our Kotlin Generics article.
3.4. Non-Local Returns
In Kotlin, we can use the return expression (also known as unqualified return) only to exit from a named function or an anonymous one:
fun namedFunction(): Int { return 42 } fun anonymous(): () -> Int { // anonymous function return fun(): Int { return 42 } }
In both examples, the return expression is valid because the functions are either named or anonymous.
However, we can't use unqualified return expressions to exit from a lambda expression. To better understand this, let's reinvent yet another wheel:
fun List.eachIndexed(f: (Int, T) -> Unit) { for (i in indices) { f(i, this[i]) } }
This function performs the given block of code (function f) on each element, providing the sequential index with the element. Let's use this function to write another function:
fun List.indexOf(x: T): Int { eachIndexed { index, value -> if (value == x) { return index } } return -1 }
This function is supposed to search the given element on the receiving list and return the index of the found element or -1. However, since we can't exit from a lambda with unqualified return expressions, the function won't even compile:
Kotlin: 'return' is not allowed here
As a workaround for this limitation, we can inline the eachIndexed function:
inline fun List.eachIndexed(f: (Int, T) -> Unit) { for (i in indices) { f(i, this[i]) } }
Then we can actually use the indexOf function:
val found = numbers.indexOf(5)
Inline functions are merely artifacts of the source code and don't manifest themselves at runtime. Therefore, returning from an inlined lambda is equivalent to returning from the enclosing function.
4. Limitations
Generally, we can inline functions with lambda parameters only if the lambda is either called directly or passed to another inline function. Otherwise, the compiler prevents inlining with a compiler error.
For example, let's take a look at the replace function in Kotlin standard library:
inline fun CharSequence.replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence): String = regex.replace(this, transform) // passing to a normal function
The snippet above passes the lambda, transform, to a normal function, replace, hence the noinline.
5. Conclusion
В тази статия разгледахме проблемите с ламбда производителността и изтриването на типа в Kotlin. След това, след въвеждане на вградени функции, видяхме как те могат да се справят и с двата проблема.
Трябва обаче да се опитаме да не прекаляваме с тези видове функции, особено когато тялото на функцията е твърде голямо, тъй като размерът на генерирания байт код може да нарасне и може да загубим няколко JVM оптимизации по пътя.
Както обикновено, всички примери са достъпни в GitHub.