"Kotlin Core Programming" Notes: Collections, Sequences and Inline Functions

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}

mapThe 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. The filterNot method has the opposite effect to the filter method. When the conditions passed in are the same, opposite results will be obtained.
  • filterNotNull is used to filter out elements whose value is null.
  • 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 to sumBy. 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 thatfoldThe method needs to receive two parameters, the first parameterinitialis 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.

reduceThe 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 }

reduceThe 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]

flatMapReceived a function whose return value is a list, a list consisting of student names.

通过flattenmap也可以实现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 afterwardflatMapReal:

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.

flatMapSource 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
}

transformThe 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

flatMapToThe 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

Insert image description here

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 in List can also be repeated.
  • Set: A non-repeatable set. There are two commonly used specific implementations of Set, namely HashSet and TreeSet. HashSet uses Hash hashing to store data, which cannot guarantee the ordering of elements; while the underlying structure of TreeSet is a binary tree, which can guarantee the ordering of elements. sex. Without specifying the specific implementation of Set, we generally say that Set 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 implement Iterable or Collection. 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. listlist[0]=0set

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: listfoo

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 mapThe 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 mapPerform 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. .

SequenceThe operation process diagram is as follows:

Insert image description here

ListThe operation process diagram is as follows:

Insert image description here

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 fixedinvokedynamic 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,foothe 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, filterare 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 asinternal.

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. block2noinline

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 mainreturning()

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>()

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/135027490