Beginners on the road, Kotlin study notes (4) --- The use of Lambda expressions in Kotlin

        A small code farmer who has not been in the industry for a few years, recently learned Kotlin and made a note record. This article is based on the process record of the book "Kotlin Combat". Some examples are taken from "Kotlin Combat". Write down your understanding, this book This article records the use of classes, objects, and interfaces in Kotlin.

        //In the last chapter  , newbies are on the road, Kotlin study notes (3) --- classes, objects, interfaces

        

        Lambda expression is a very concise way of writing. In Java1.8, we can already use Lambda. Let's see how to use Lambda expression in Kotlin.

        Since I haven't used Java 1.8 before, I don't understand Lambda expressions very well. Let's briefly record what I've learned recently.

First, the use of Lambda in the collection

        In Kotlin's collection library, there are many encapsulated methods for us to use, some of which use the Lambda method, such as the following example, to get the maximum value in the collection

/**
 * Returns the first element yielding the largest value of the given function or `null` if there are no elements.
 */
public inline fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T? {
    val iterator = iterator()
    if (!iterator.hasNext()) return null
    var maxElem = iterator.next ()
    var maxValue = selector(maxElem)
    while (iterator.hasNext()) {
        val e = iterator.next()
        val v = selector (s)
        if (maxValue < v) {
            maxElem = e
            maxValue = v
        }
    }
    return maxElem
}

        As you can see in the above source code, our parameter is selector, this parameter is (T) -> R, T is the object contained in our collection, and R can see in the front that the requirement is R : Comparable<R> , It is to require R to implement the Comparable interface, because this can be compared.

        So when we call this method, we can use it like this

open class Person(var age : Int = 0, var name : String = "", var addr : String = "") //Open can be inherited by subclasses after modification
{
    open fun showName() = println(name) //after the open modification can be overridden by subclasses
    fun showAddr() = println(addr) //The default is final and cannot be overridden by subclasses
    var user : User? = null
}
interface User
{
    val nickName : String
}

The above is the Person class we defined before. Now a new User interface has been added. There is a variable User in Person.

When we sort a collection of Person objects, call the maxBy method

   fun findOldestPerson(people : ArrayList<Person>)
    {
        people.maxBy ({person: Person -> person.age })
    }

        This usage conforms to the format of selector : (T) -> R in the source code. T is person, and R is person.age. Because age is of type Int, the Comparable interface has been implemented by default, so it can be used normally. And if we sort User, since User does not implement the Comparable interface, the editor will report an error

Type parameter bound for R in  

inline fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) → R) : T?

is not satisfied: inferred type User? is not a subtype of Comparable<User?>

        Next, we continue to return to Lambda. The use of maxBy just now seems a little uncomfortable. We can continue to improve it. After all, the purpose of Kotlin is to have a more beautiful introduction. In Kotlin, when a lambda expression is the last argument, it can be placed outside the parentheses. At the same time, if there is only a lambda as an actual parameter, you can also remove the empty pair of parentheses. As we mentioned earlier, when the type of the parameter is known, the parameter type can be omitted. In the end, the code becomes like this

people.maxBy { person -> person.age }

It looks a lot nicer now, but we can keep simplifying. When there is only one parameter Lambda and the type of this parameter can be deduced, it can be used instead of this parameter, such as the maxBy method just now, people is a collection class whose generic type is Person, so it can be deduced that the parameter in the Lambda must be of type Person, so we can replace the previous person with It, which simplifies to

people.maxBy { it.age }
        For the use of it, when using it a lot, our code looks concise, but it is not necessarily easy to understand, so it is recommended to use the previous method to explicitly write the name of the parameter, which is convenient for reading the code later. Don't use it blindly.

        The previous introduction is to use code blocks as parameters. Next, let's see how to use properties or methods as parameters. We can convert the property or method into a value, and then we can pass the method or property. The usage is as follows, represented by class name + :: (double colon) + property or method name , such as

people.maxBy(Person::age)

Here is to use the age attribute of Person directly

    val action = {person : Person, message : String -> sendEmail(person,message)}
    val nextAction = :: sendEmail //MainActivity :: sendEmail The current class is MainActivity, which can be omitted
    fun sendEmail(person: Person,message:String){}

Here is the way to convert a method to a value, action and nextAction have the same effect, nextAction looks more concise.


2. Collection related API

        In Lambda, there are many functional APIs. Mastering these APIs will significantly improve our development process. Let's learn together.

(1) Basic filter, map, all, any, count, find

        Just like the method name, the function of filter is to filter and return the data that meets the filter conditions. The map is the function of conversion, which converts each element in the collection according to certain rules, and then returns.

/**
 * Returns a list containing only elements matching the given [predicate].
 */
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}
/**
 * Appends all elements matching the given [predicate] to the given [destination].
 */
public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}

        Through the source code, we can see that the implementation of filter is to traverse the collection and form a new collection of elements that meet the requirements. It is worth noting that the parameter of this method is a Lambda expression, combined with the Lambda expression we learned before, For collection filtering, it can be done with just one or two lines of code.

/**
 * Returns a list containing the results of applying the given [transform] function
 * to each element in the original collection.
 */
public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
/**
 * Applies the given [transform] function to each element of the original collection
 * and appends the results to the given [destination].
 */
public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
    for (item in this)
        destination.add(transform(item))
    return destination
}

        The implementation of map is similar to that of filter. After some operations are performed, the operated data is formed into a new set and returned.

/**
 * Returns `true` if all elements match the given [predicate].
 */
public inline fun <T> Iterable<T>.all(predicate: (T) -> Boolean): Boolean {
    if (this is Collection && isEmpty()) return true
    for (element in this) if (!predicate(element)) return false
    return true
}

/**
 * Returns `true` if collection has at least one element.
 */
public fun <T> Iterable<T>.any(): Boolean {
    if (this is Collection) return !isEmpty()
    return iterator().hasNext()
}

/**
 * Returns `true` if at least one element matches the given [predicate].
 */
public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean {
    if (this is Collection && isEmpty()) return false
    for (element in this) if (predicate(element)) return true
    return false
}

        Through the source code, we can also see the role of all and any, all is to determine whether all elements satisfy the Lambda expression we passed in. any is to determine whether there is an element that satisfies the Lambda expression we passed in, and the two correspond to each other.

/**
 * Returns the number of elements matching the given [predicate].
 */
public inline fun <T> Iterable<T>.count(predicate: (T) -> Boolean): Int {
    if (this is Collection && isEmpty()) return 0
    var count = 0
    for (element in this) if (predicate(element)) count++
    return count
}

        count is the number of all elements that satisfy the lambda expression.

/**
 * Returns the first element matching the given [predicate], or `null` if no such element was found.
 */
@kotlin.internal.InlineOnly
public inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
    return firstOrNull(predicate)
}
/**
 * Returns the first element matching the given [predicate], or `null` if element was not found.
 */
public inline fun <T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean): T? {
    for (element in this) if (predicate(element)) return element
    return null
}

        The effect of the find method is to find the first element that satisfies the lambda expression, and return null if there is no element that satisfies the requirement.

        The above are some basic set operations. There are many related functions in the Kotlin source code for us to use. For example, groupBy allows us to reorganize the set and divide it into multiple sub-sets according to the rules, which is better than writing this part of the business logic by ourselves. Much simpler, you can refer to the Collections.kt file for details.

(2) Sequence

        The above collection operation is very convenient, a new collection object will be returned each time, and then we can continue to call new operations, which forms a chained writing method, which looks very clear, but we can see from the source code that in the operation Every time a new collection is created, the cost is to create a lot of intermediate objects, which we don't actually need, so can we not create these temporary objects? The answer is yes, let's take a look at sequences.

        Sequences have APIs for chained operations such as collections. A collection can be converted into a sequence by calling the asSequence() method. After the sequence is transformed, it can be converted back to a collection using methods such as toList(). Let's take filter as an example and see how the sequence is implemented

/**
 * Creates a [Sequence] instance that wraps the original collection returning its elements when being iterated.
 *
 * @sample samples.collections.Sequences.Building.sequenceFromCollection
 */
public fun <T> Iterable<T>.asSequence(): Sequence<T> {
    return Sequence { this.iterator() }
}

        As you can see, the asSequence method returns a Sequence object, and then calls the filter method of Sequence

/**
 * Returns a sequence containing only elements matching the given [predicate].
 *
 * The operation is _intermediate_ and _stateless_.
 */
public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
    return FilteringSequence(this, true, predicate)
}
/**
 * A sequence that returns the values from the underlying [sequence] that either match or do not match
 * the specified [predicate].
 *
 * @param sendWhen If `true`, values for which the predicate returns `true` are returned. Otherwise,
* values for which the predicate returns `false` are returned
 */
internal class FilteringSequence<T>(private val sequence: Sequence<T>,
                                  private val sendWhen: Boolean = true,
                                  private val predicate: (T) -> Boolean
                                 ) : Sequence<T> {

    override fun iterator(): Iterator<T> = object : Iterator<T> {
        val iterator = sequence.iterator()
        var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
        var nextItem: T? = null

        private fun calcNext() {
            while (iterator.hasNext()) {
                val item = iterator.next()
                if (predicate(item) == sendWhen) {
                    nextItem = item
                    nextState = 1
                    return
                }
            }
            nextState = 0
        }

        override fun next(): T {
            if (nextState == -1)
                calcNext()
            if (nextState == 0)
                throw NoSuchElementException()
            val result = nextItem
            nextItem = null
            nextState = -1
            @Suppress("UNCHECKED_CAST")
            return result as T
        }

        override fun hasNext(): Boolean {
            if (nextState == -1)
                calcNext()
            return nextState == 1
        }
    }
}

        It can be seen from the source code that when we called the filter method, we did not create a new collection to receive, but changed the Iterator method. In the next query, we filtered out the elements we did not need, and only in the last toList When a new set is created, it omits many unnecessary variables in the middle, which is a good thing for us.

        Tips: Through the above analysis, we know that the chain method called by the sequence will only take effect when it is finally converted into a collection, so if we don't have toList and receive it, we actually don't do anything, such as people.asSequence( ).maxBy { it.age } is meaningless,

        In addition, you can also create a sequence through the method generateSequence(seed: T?, nextFunction: (T) -> T?), the first parameter is the first element of the sequence, and the latter parameter is the calculation of the next element of the sequence Way.


3. More concise usage in Lambda: with and apply

        Let's look at an example first, when we want to concatenate strings like a123456789b, the code implemented with StringBuilder might look like this

    fun getString():String{
        val stringBuilder = StringBuilder()
        stringBuilder.append("a")
        for(i in 1..9)
        {
            stringBuilder.append(i)
        }
        stringBuilder.append("b")
        return stringBuilder.toString()
    }

        This implementation is our usual way of writing, and there is no problem, but we keep writing stringBuilder.append in it, which is actually quite troublesome. The ultimate goal of code writing is to eliminate all duplicate codes, so can we connect stringBuilder. Also omitted? The answer is of course yes, with can help us do this, see the following example

   fun getStringByWith():String{
        val stringBuilder = StringBuilder()
        return with(stringBuilder)
        {
            append("a")
            for(i in 1..9)
            {
                append(i)
            }
            append("b")
            stringBuilder.toString()
        }
    }

        Looking at the above code, we put the stringBuilder object in the parentheses after with, and then we don't need to write the stringBuilder object again when calling the append method in the code block. Like if, the result of the last expression stringBuilder is the with method. The return value.

        Next, let's look at another implementation, using apply to implement this logic

   fun getStringByApply() = StringBuilder().apply{
        append("a")
        for(i in 1..9)
        {
            append(i)
        }
        append("b")
    }.toString()

        Apply and with are used in similar ways. The object that calls apply is the default object in the code block. You can omit the object name and write the calling method directly. The difference from with is that the apply method is on the original object. To make changes, there is no need to return a value, so after we complete the modification, we can directly call the toString() method.


        Today's document ends here. In the next chapter, we will record learning about types in Kotlin, what to do with null, conversion between data types, special data types, etc.

        //The next chapter   is new to the road, Kotlin study notes (5) --- the type system in Kotlin


 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325200476&siteId=291194637