High-order function API for collections
map operation
val list = listOf(1, 2, 3, 4, 5, 6)
val newList = list.map {
it * 2 }
Of course, in Java 8, you can now manipulate collections like Kotlin.
The above method is actually ahigher-order function, and the parameter it receives is actually aFunction, maybe the above writing method is not very clear. We can modify the above expression as follows:
val newList = list.map {
el -> el * 2}
map
The expression after Lambda
is actually an anonymous function with one parameter. We can also call a function like this in the map
method:
fun foo(bar: Int) = bar * 2
val newList = list.map {
foo(it)}
After using themap
method, a new collection will be produced, and the size of the collection will be the same as the original collection. By using the map
method, we eliminate the for
statement and no longer need to define some intermediate variables.
Filter the collection: filter, count
val mStudents = students.filter {
it.sex == "m"}
This method is similar to map
and also receives a function, except that the return value type of the function must be Boolean
. The function of this function is to determine whether each item in the set meets a certain condition. If it does, the filter
method will insert the item into a new list, and finally get an item that satisfies the given A new list of conditions. The new list produced after calling filter
is a subset of the original list.
Other methods with filtering capabilities include the following:
filterNot
, used to filter out elements that meet the conditions. ThefilterNot
method has the opposite effect to thefilter
method. When the conditions passed in are the same, opposite results will be obtained.filterNotNull
is used to filter out elements whose value isnull
.count
, counts the number of elements that meet the conditions.
val countMStudent = students.count {
it.sex == "m"}
val countMStudent = students.filter {
it.sex == "m"}.size
Sum: sumBy, sum, fold, reduce
sum
val scoreTotal = students.sumBy {
it.score}
sum
: Sum the list of numeric typessum
is similar tosumBy
. It is also a common summation API, but it can only sum some Lists of numeric types are summed.
val a = listOf(1, 2, 3, 4, 5)
val b = listOf(1.1, 2.5, 3.0, 4.5)
val aTotal = a.sum()
val bTotal = b.sum()
also possiblesumBy
:
val aTotal = a.sumBy {
it}
val bTotal = b.sumBy {
it}
fold
Its source code:
public inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) ->
var accumulator = initial
for (element in this) accumulator = operation(accumulator, element)
return accumulator
}
You can see thatfold
The method needs to receive two parameters, the first parameterinitial
is usually called the initial value, the second parameter a>operation
is a function. Each element in the collection is traversed through the for
statement, and the operation
function is called each time, and the function has two parameters, one is the last time the function was called The result of the function (the initial value is initial
), and the other is the currently traversed set element. To put it simply: call the operation
function for each traversal, and then provide the result as a parameter to the next traversal.
val scoreTotal = students.fold(0) {
accumulator, student -> accumulator + student.score }
We can also get the total scores of all students through the above method. In the above code, the fold
method receives an initial value 0
, and then receives a function, which is the subsequent Lambda expression.
{
accumulator, student -> accumulator + student.score }
The above function has two parameters. The first parameter is the return result after each execution of the function, and the second parameter is an element in the student list. We implement the summation operation by adding the result after the previous execution to the score of the currently traversed student. In fact, it is an accumulation operation.
Similarly, we can also perform cumulative multiplication operations:
val list = listOf(1,2,3,4,5)
list.fold(1) {
mul, item -> mul * item }
>>> 120
fold
makes good use of the idea of recursion.
reduce
The method is very similar to the fold
method. The only difference is that the reduce
method has no initial value. Let's also take a look at the source code of the reduce
method:
public inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S {
val iterator = this.iterator()
if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
var accumulator: S = iterator.next()
while (iterator.hasNext()) {
accumulator = operation(accumulator, iterator.next())
}
return accumulator
}
You can find that reduce
method only receives one parameter, which is a function. The specific implementation is similar to fold
, except that when the collection to be traversed is empty, an exception will be thrown. Because there is no initial value, the default initial value is the first element in the collection. The above summation operation can also be achieved using the reduce
method:
val scoreTotal = students.reduce {
accumulator, student -> accumulator + student.score }
reduce
The method is similar to the fold
method. When we do not need the initial value, we can use the reduce
method.
Grouping: groupBy
Kotlin provides us with agroupBy
method. So if we want to group the elements in the student list according to gender, we can do this:
students.groupBy {
it.sex }
The type of the result data structure returned isMap<String, List<Student>>
, which has two groups, one is the group corresponding to male gender, and the other is the group corresponding to female gender.
Flattening - dealing with nested collections: flatMap, flatten
Sometimes we hope that each element in the nested collection can be taken out and then formed into a collection with only these elements, like this:
val list = listOf(listOf(jilen, shaw, lisa), listOf(yison, pan), listOf(jack))
val newList = listOf(jilen, shaw, lisa, yison, pan, jack)
This can be achieved via flatten:
list.flatten()
Suppose we don't want to directly get a flattened set, but want to "process" the elements in the sub-collection and then return a "processed" set.
For example, if we want to get a list of names, how should we do it?
Kotlin also provides us with a method-flatMap
, which can be used to achieve this requirement:
list.flatMap {
it.map{
it.name}}
>>> [Jilen, Shaw, Lisa, Yison, Pan, Jack]
flatMap
Received a function whose return value is a list, a list consisting of student names.
通过flatten
和map
也可以实现flatMap
's ability:
list.flatten().map {
it.name}
Through this example, you will find thatflatMap
seems to be to perform the flatten
operation on the list first and then perform the map
operation , then if you now need to take out the students' hobbies from the student list, and then combine these hobbies into a list. Let’s first look at how to do it using flatten
and map
:
students.map {
it.hobbies}.flatten()
Used afterwardflatMap
Real:
students.flatMap {
it.hobbies}
Through this example, we also found that flatMap
first performed the map
operation on the list and then performed the flatten
operation , and using flatMap
in this example is more concise.
flatMap
Source code:
public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
return flatMapTo(ArrayList<R>(), transform)
}
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
for (element in this) {
val list = transform(element)
destination.addAll(list)
}
return destination
}
transform
The function receives a parameter (usually a sublist in a nested list) and returns a list.
For example, when we used flatMap
to get the list of hobbies:
{
it.hobbies}
// 上面的表达式等价于:
{
it -> it.hobbies}
calls a method inflatMap
, which receives two parameters. One parameter is a list, and the list is an empty list. The other parameter is a function, and the return value of the function is a sequence. flatMapTo
flatMapTo
The implementation of is very simple. First iterate over the elements in the collection, then pass each element into the function transform
to get a list, and then add all the elements in this list to the empty In the list destination
, you will finally get a flattened list processed by the transform
function.
flatMap
can actually be regarded as a combination of flatten
and map
. The combination method is determined according to the specific situation. When we only need to flatten a collection, use flatten
; if we need to perform some "processing" on the elements, then we can consider using < a i=4>. flatMap
Collection inheritance relationship
Iterable
is the top-level interface of the Kotlin collection library. We can find that each set is divided into two types, one with Mutable
prefix, and the other without. For example, our common lists are divided into MutableList
and List
, List
implements the Collection
interface, MutableList
implements MutableCollection
and List
(MutableList
represents variable List
, and List
means read-only List
).
In fact, Kotlin's collections are built based on Java's collection library, but Kotlin enhances it through extension functions.
List
: An ordered and repeatable linear list. The elements inList
can also be repeated.Set
: A non-repeatable set. There are two commonly used specific implementations ofSet
, namelyHashSet
andTreeSet
.HashSet
uses Hash hashing to store data, which cannot guarantee the ordering of elements; while the underlying structure ofTreeSet
is a binary tree, which can guarantee the ordering of elements. sex. Without specifying the specific implementation ofSet
, we generally say thatSet
is unordered.
listOf(1, 2, 3, 4, 4, 5, 5)
>>> [1, 2, 3, 4, 4, 5, 5]
setOf(1, 2, 3, 4, 4, 5, 5)
>>> [1, 2, 3, 4, 5]
Map
:Map
in Kotlin is a bit different from other collections in that it does not implementIterable
orCollection
.Map
is used to represent a collection of key-value pairs, such as:
mapOf(1 to 1, 2 to 2, 3 to 3)
>>> {
1=1, 2=2, 3=3}
In the key-value pair inMap
, the key cannot be repeated.
Mutable collections and read-only collections
Although Kotlin's collections are built based on Java, Kotlin has chosen to take a different approach at this point. Kotlin divides the collection into mutable collectionsWithread-only collection. For example, our common collection lists are divided into MutableList
and List
. There is no immutable collection in Kotlin's collection yet, we can only call it a read-only collection.
Variable collection: Mutable collections will have a modified prefix "Mutable
", such as MutableList
. Change here refers to changing the elements in the collection.
val list = mutableListOf(1, 2, 3, 4, 5)
We modify the first element in the collection to 0:
val list = mutableListOf(1, 2, 3, 4, 5)
list[0] = 0
>>> [0, 2, 3, 4, 5]
Read-only collection: The elements in the read-only collection cannot be modified under normal circumstances, such as those created by listOf()
< a i=3>To modify an error will be reported. Because this actually calls the method, but there is no such method in Kotlin's read-only collection. list
list[0]=0
set
The difference between Kotlin's mutable collections and read-only collections is actually that after Kotlin removes the modification, addition, deletion, etc. methods in the mutable collection, the original The mutated collection becomes a read-only collection.
In other words, there are only some methods that can be used to "read" in read-only collections, such as getting the size of the collection, traversing the collection, etc.
The advantage of this is that it makes the code easier to understand and, to some extent, more secure.
For example, we implement a method that adds elements in the a
list to the b
list:
fun merge(a: List<Int>, b: MutableList<Int>) {
for (item in a) {
b.add(item)
}
}
You can find that the a
list is just traversed, but what really changes is the b
list. The advantage of this is that we can easily know that the function mergeList
will not modify a
because a
is read-only, and The function will most likely modify the listb
.
However, we cannot say that read-only lists cannot be changed. In Kotlin, there is a reason why we call List
a read-only list instead of a mutable list, because in some cases read-only lists can indeed be changed, such as:
val writeList: MutableList<Int> = mutableListOf(1,2,3,4)
val readList: List<Int> = writeList
>>> readList [1,2,3,4]
In the above code, we first define a mutable listwriteList
, and then we define a read-only listreadList
, which Points to the same collection object as writeList
, because MutableList
is a subclass of List
, so we can do this. We now modify this collection:
writeList[0] = 0
>>> readList [0,2,3,4]
You can find that the read-only listreadList
has changed, which means that in this case we can modify the read-only collection. So we can only say that read-only lists are safe in some cases, but it is not always safe.
In addition, since kotlin and Java are compatible and can call each other, but Java does not distinguish between read-only collections and mutable collections, it is easy to modify read-only collections.
For example, when we call the followingbar
method in Kotlin:
fun bar(list: List<Int>) {
foo(list)
}
The method called herefoo
is defined in Java code:
public static List<Int> foo(List<Int> list) = {
for (int i = 0; i < list.size(); i++) {
list[i] = list[i] * 2;
}
return list;
}
So the passed into the bar
method will be changed by the method: list
foo
val list = listOf(1, 2, 3, 4)
bar(list)
println(list)
>>> [2, 4, 6, 8]
Therefore, we must consider this situation when we interoperate with Java.
Sequence collection Sequence
list.asSequence().filter{
it > 2 }.map{
it * 2 }.toList()
Here we first convert a list into a sequence through the asSequence()
method, then perform corresponding operations on this sequence, and finally convert the sequence through toList()
distribution for a list.
In Kotlin, the evaluation of elements in a sequence islazy, which means that the sequence is used for chain evaluation. At this time, there is no need to generate a new collection to store intermediate data every time an evaluation operation is performed like operating a normal collection.
SoinertWhat does it mean? Let’s take a look at its definition first:
- In programming language theory,Lazy Evaluation (LazyEvaluation) represents a calculation method that evaluates only when needed. When using lazy evaluation,the expression will not be evaluated immediately after it is bound to the variable, but will be evaluated when the value is retrieved< a i=4>. In this way, not only can performance be improved, but the most important benefit is that it can construct an infinite data type.
Through the above definition, we can simply summarize the two benefits of lazy evaluation. One isoptimizing performance, and the other is the ability to construct Outunlimited data types.
How sequences work
Sequence operations in Kotlin are divided into two categories, one is intermediate operations, and the other is < a i=3>Terminal operation.
In the above code, filter {it > 2}.map {it * 2}
this type of operation is called intermediate operation, < This type of operation a i=4> converts the sequence into . We call this type of operation end operation. toList()
List
Intermediate operations: When performing chain operations on ordinary collections, some operations will produce intermediate collections. When such operations are used to evaluate sequences, they It is called an intermediate operation, such as filter
and map
above. Each intermediate operation returns a sequence, and the new sequence generated internally knows how to transform the elements in the original sequence. Intermediate operations are all evaluated lazily .
for example:
list.asSequence()
.filter {
println("filter($it)")
it > 2
}
.map {
println("map($it)")
it * 2
}
After running the above code, you will find that the println
method is not executed at all, which means that the filter
method and map
The execution of the method is delayed, which is the embodiment of lazy evaluation.
Lazy evaluation is also called delayed evaluation. Lazy evaluation will only actually evaluate the value when it is needed. So how to trigger this "being needed" state? This requires another operation - terminal operation.
End operation: When operating on a collection, in most cases, we only care about the result, not the intermediate process. The terminal operation is an operation that returns a result. Its return value cannot be a sequence, but must be a clear result, such as a list, number, object, etc. The terminal operation is generally placed at the end of the chain operation. When the terminal operation is executed, the delayed calculation of the intermediate operation will be triggered, that is, the "required" state is turned on.
Let’s add the terminal operator to the above example:
list.asSequence()
.filter {
println("filter($it)")
it > 2
}
.map {
println("map($it)")
it * 2
}
.toList()
result:
filter(1)
filter(2)
filter(3)
map(3)
filter(4)
map(4)
filter(5)
map(5)
[6,8,10]
As you can see, all intermediate operations have been executed. Taking a closer look at the results above, we can find something interesting. For comparison, let's first take a look at the difference if the above operation is implemented using a list instead of a sequence:
list.filter {
println("filter($it)")
it > 2
}
.map {
println("map($it)")
it * 2
}
result:
filter(1)
filter(2)
filter(3)
filter(4)
filter(5)
map(3)
map(4)
map(5)
[6,8,10]
We can find that when performing chain operations, ordinary collections will first call list
on filter
, and then generate a result list, and then map
Perform operations on this result list.
The sequence is different. When the sequence performs chain operations, will apply all operations to one element. That is to say, after the first element has performed all operations, the second element will perform all operations again, and so on.
is reflected in our example, that is, the first element is executedfilter
and then executedmap
, and then the second element is also the same. .
Sequence
The operation process diagram is as follows:
List
The operation process diagram is as follows:
Through the return results of the above sequence, we can also find that because elements 1 and 2 in the list do not meet the condition that filter
is greater than 2
in the operation, So the nextmap
operation will not be executed. So when we use sequences, if the positions of filter
and map
are interchangeable, filter
should be used first, so It will reduce some expenses.
Create infinite sequences
Kotlin also provides us with such a methodgenerateSequence
to create an infinite sequence:
val naturalNumList = generateSequence(0) {
it + 1}
The above creates a sequence of natural numbers, the next number of which is always the result of adding 1 to the previous number.
We know that sequences are evaluated lazily, so the sequence created above will not enumerate all natural numbers. Only when we call a terminal operation, we will enumerate the list we need. For example, we want to take the first 10 natural numbers from this list of natural numbers:
naturalNumList.takeWhile {
it <= 9}.toList()
>>> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Regarding the infinite sequence, we cannot present an infinite data structure in an exhaustive way, but only achieve a state that represents infinity, so that we feel that it is infinite when we use it.
Sequence vs. Java 8 Stream
Sequences look similar to Streams in Java 8:
students.stream().filter (it -> it.sex == "m").collect(toList());
But compared to Kotlin, Java's operation method is still a bit cumbersome, because if you want to use this API for a collection, you must first convert the collection to stream
, and the operation is completed After that, you need to convert stream
to List
. This operation is somewhat similar to Kotlin's sequence. This is because the Java 8 stream, like the sequence in Kotlin, is also lazily evaluated, which means that the Java 8 stream also has intermediate operations and terminal operations (which is indeed the case), so it must go through the above series Just convert.
Streams are disposable: Unlike Kotlin’s sequences, streams in Java 8 are disposable. That is, if we create a Stream
, we can only traverse this Stream
once. This is very similar to an iterator. After you complete the traversal, the stream is equivalent to being consumed, and you must create a new Stream
to traverse again.
Stream<Student> studentsStream = students.stream();
studentsStream.filter (it -> it.sex == "m").collect(toList());
studentsStream.filter (it -> it.sex == "f").collect(toList());
Stream can process data in parallel: Stream
in Java 8 can process streams in parallel on multi-core architectures. Just replace stream
with paralleStream
.
students.paralleStream().filter (it -> it.sex == "m").collect(toList());
Parallel processing of data is a feature that Kotlin's sequences have not yet implemented. If we need to use multi-threaded collections, we still need to rely on Java.
inline function
Optimize Lambda overhead
Kotlin makes extensive use of Lambda in the collection API, which makes our operations on collections much more elegant. But the price of this approach is that using lambda expressions in Kotlin will bring some additional overhead.
Every time a Lambda expression is declared in Kotlin, an anonymous class is generated in the bytecode. This anonymous class contains a invoke
method, which is used as the calling method of Lambda. Every time it is called, a new anonymous class object will be created. inline functions. As you can imagine, although Lambda syntax is simple, it also adds a lot of additional overhead. To introduce Lambda syntax in Android, Kotlin must adopt some method to optimize the additional overhead caused by Lambda, that is,
invokedynamic in Java
The inline function in Kotlin is a bit embarrassing, because it was designed mainly to optimize the overhead caused by Kotlin's support for Lambda expressions. However, we don't seem to need to pay special attention to this issue in Java, because after Java 7, the JVM introduced a method called >invokedynamic
technology, it will automatically help us do Lambda optimization.
Different from Kotlin, which generates Lambda conversion classes through hard coding at compile time, Java uses the invokedynamic
technology after SE 7 to generate the corresponding translation code at runtime. . When invokedynamic
is called for the first time, an anonymous class will be triggered to replace the intermediate code. Subsequent calls will directly use the code of this anonymous class a>. The main benefits of this approach are:
- Since the specific conversion implementation is generated at runtime, only one fixed
invokedynamic
can be seen in the bytecode, so the number of statically generated classes and Bytecode size is significantly reduced; - is different from the strategy that is hard-coded in the bytecode at compile time. Using
invokedynamic
can hide the actual translation strategy in the implementation of the JDK library, which greatly improves flexibility. While ensuring backward compatibility, the translation strategy can be continuously optimized and upgraded later; - The JVM naturally supports the translation and optimization of Lambda expressions in this way, which also means that developers can write Lambda expressions without having to worry about this issue at all, which greatly improves the development experience.
invokedynamic
is nice, but there seems to be a good reason why Kotlin doesn't support it. We have enough reason to believe that the biggest reason is that Kotlin needs to be compatible with Android's most mainstream Java version SE 6 from the beginning, which makes it unable to solve the Lambda overhead problem of the Android platform through invokedynamic
.
Therefore, as another mainstream solution, Kotlin embracesinline functions, which are also supported in languages such as C++ and C#. This characteristic.
To put it simply, we can use the inline
keyword to modify functions, and these functions become inline functions. Their function bodies are embedded in each called place at compile time to reduce the number of additional anonymous classes generated and the time overhead of function execution.
So if you want to get the best possible performance support and control the number of anonymous classes generated when developing Android with Kotlin, it is necessary to learn the relevant syntax of inline functions.
fun main() {
foo {
println("dive into Kotlin...")
}
}
fun foo(block: () -> Unit) {
println("before block")
block()
println("end block")
}
A higher-order function is declared herefoo
, which can receive a Lambda of type () -> Unit
, and then main
Call it in the function.
Here is the relevant Java code decompiled from bytecode:
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
foo((Function0)null.INSTANCE);
}
public static final void foo(@NotNull Function0 block) {
Intrinsics.checkNotNullParameter(block, "block");
String var1 = "before block";
System.out.println(var1);
block.invoke(); // 调用 invoke() 方法执行 Lambda
var1 = "end block";
System.out.println(var1);
}
As we know, callingfoo
will generate a Function0
typeblock
class, and then pass a>invoke
method to execute, which will add additional generation class and calling overhead.
Now, we add the modifier to the foo
function, as follows:inline
inline fun foo(block: () -> Unit) {
println("before block")
block()
println("end block")
}
Let’s take a look at the corresponding Java code:
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String var1 = "before block";
System.out.println(var1);
// block函数体在这里开始粘贴
String var2 = "dive into Kotlin...";
System.out.println(var2);
// block函数体在这里结束粘贴
var1 = "end block";
System.out.println(var1);
}
public static final void foo(@NotNull Function0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var2 = "before block";
System.out.println(var2);
block.invoke();
var2 = "end block";
System.out.println(var2);
}
Sure enough,foo
the function body code and the called Lambda code are pasted to the corresponding calling location. Just imagine, if this is a public method in the project, or is nested in a logical body called in a loop, this method will inevitably be called many times. Through the syntax of inline
, we can completely eliminate this extra call, thus saving overhead.
A typical application scenario of inline functions is Kotlin'scollection class. If you have read Kotlin’s collection class API documentation or source code implementation, you will find that collection functional API, such as map
, filter
are defined as inline functions, such as:
inline fun <T, R> Array<out T>.map(transform: (T) -> R): List<R> {
val destination = ArrayList<R>(size)
for (element in this) {
destination.add(transform(element))
}
return destination
}
inline fun <T> Array<out T>.filter(predicate: (T) -> Boolean): List<T> {
val destination = ArrayList<T>()
for (element in this) {
if (predicate(element)) {
destination.add(element)
}
}
return destination
}
This is easy to understand. Since these methods all receive Lambda as a parameter and need to traverse the collection elements, it is undoubtedly very suitable to inline the corresponding implementation.
Inline functions are not a panacea
We should avoid using inline functions in the following situations:
- Since the JVM can already intelligently determine whether to perform inline optimization for ordinary functions based on the actual situation, we do not need to actually use Kotlin's
inline
syntax, which will only make Bytecode becomes more complex; - Try to avoid inlining functions with large function bodies, which can result in excessive bytecode count;
- Once a function is defined as an inline function, you cannot access the private members of the closure class unless you declare them as
internal
.
noinline: prevent parameters from being inlined
If you add the inline
modifier at the beginning of a function, its function body and Lambda parameters will be inlined. However, the situation in reality is more complicated. One possibility is that the function needs to receive multiple parameters, but we only want to inline some of the Lambda parameters, and not others. How to deal with this?
Solving this problem is also very simple. When Kotlin introduced inline
, it also added the noinline
keyword. We can add it where we don’t want it. At the beginning of the parameter to be inlined, the parameter will not have the effect of being inlined. Let's modify the above example again, and then applynoinline
:
fun main() {
foo({
println("I am inlined...")
}, {
printIn("I am not inlined...")
})
}
inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) {
println("before block")
block1()
block2()
println("end block")
}
Using the same method, let’s look at the decompiled Java version:
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
Function0 block2$iv = (Function0)null.INSTANCE;
String var2 = "before block";
System.out.println(var2);
// block1被内联了
String var3 = "I am inlined...";
System.out.println(var3);
// block2还是原样
block2$iv.invoke();
var2 = "end block";
System.out.println(var2);
}
public static final void foo(@NotNull Function0 block1, @NotNull Function0 block2) {
Intrinsics.checkParameterIsNotNull(block1, "block1");
Intrinsics.checkParameterIsNotNull(block2, "block2");
String var3 = "before block";
System.out.println(var3);
block1.invoke();
block2.invoke();
var3 = "end block";
System.out.println(var3);
}
It can be seen that after the parameter in the foo
function is brought , the decompiled Java The code does not replace the function body code at the call site. block2
noinline
non-local return
Inline functions in Kotlin not only optimize Lambda overhead, but also bring other special effects, typically non-local returns and materialized parameter types. Let's first take a look at how Kotlin supports non-local returns.
The following are our common examples of partial returns:
fun main() {
foo()
}
fun localReturn() {
return
}
fun foo() {
println("before local return")
localReturn()
println("after local return")
return
}
operation result:
before local return
after local return
As we all know, after localReturn
is executed, the return
in its function body will only take effect locally in the function, so Functions after are still valid. localReturn()
println
Let’s replace this function with a Lambda expression version:
fun main() {
foo {
return }
}
fun foo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
operation result:
Error:(2, 11) Kotlin: 'return' is not allowed here
The compiler will report an error directly, that is to say, in Kotlin,Normally Lambda expressions are not allowed to existreturn
keywords . At this time, inline functions can come in handy again. Let's try it again after inlining foo
:
fun main() {
foo {
return }
}
inline fun foo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
operation result:
before local return
has been compiled, and the result is in line with our expectations. Because the function body and parameter Lambda of the inline functionfoo
will directly replace the specific call, so in the actual generated code, < /span>. non-local return will naturally not be executed. This is the so-called function, so the code after return
is equivalent to being directly exposed to the main
returning()
Use @ tag to implement Lambda non-local return
Another equivalent way is to use the @
symbol through tags to implement Lambda non-local return.
Similar to the above example, we can do this to achieve the same effect without declaring the inline
modifier:
fun main() {
foo {
return@foo }
}
fun foo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
operation result:
before local return
non-local return is particularly useful in loop control, such as Kotlin's forEach
interface, which receives a Lambda parameter. Since it is also an inline function, we can Execute directly in the Lambda it callsreturn
to exit the program at the upper level.
fun hasZeros(list: List<Int>): Boolean {
list.forEach {
if (it == 0) return true // 直接返回 hasZeros 函数结果
}
return false
}
Just imagine, if the inline
function does not support non-local returns, then the above code will not guarantee normal logic.
crossinline
Although non-local returns are useful in some situations, they can also be dangerous. Because sometimes, the Lambda parameters received by our inline functions often come from elsewhere in the context. In order to prevent the Lambda parameter with return
from damaging the main calling process, we can also use the crossinline
keyword to modify the parameter to eliminate such risks. Like this:
fun main() {
foo {
return }
}
inline fun foo(crossinline returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
operation result:
Error:(2, 11) Kotlin: 'return' is not allowed here
reified materialized parameter type
In addition to non-local returns, inline functions help Kotlin implement reified parameter types.
Kotlin is the same as Java. Due to runtime type erasure, we cannot directly obtain the type of a parameter. However, since the inline function will directly generate the corresponding function body implementation in the bytecode, in this case we can get the specific type of the parameter. We can use the reified
modifier to achieve this effect.
fun main() {
getType<Int>()
}
inline fun <reified T> getType() {
print(T::class)
}
operation result:
class kotlin.Int
This feature is also particularly useful in Android development. For example, in Java, when we want to call startActivity
, we usually need to pass the specific target Activity
class as a parameter. In Kotlin, we can use reified
to simplify:
inline fun <reified T : Activity> Activity.startActivity() {
startActivity(Intent(this, T::class.java))
}
It will be very convenient for us to jumpActivity
:
startActivity<DetailActivity>()