新手上路,Kotlin学习笔记(四)---Lambda表达式在Kotlin中的使用

        入行没几年的小码农,近期学习Kotlin,做一份笔记记录,此文依据《Kotlin实战》这本书的流程记录,部分示例内容均摘自《Kotlin实战》,记下自己的理解,本篇记录Kotlin中对类,对象,接口的使用。

        //上一章 新手上路,Kotlin学习笔记(三)---类、对象、接口

        

        Lambda表达式是非常简洁的书写方式,在Java1.8中,我们已经可以使用Lambda了,下面我们来看在Kotlin中如何使用Lambda表达式。

        由于之前没有用过Java1.8,对于Lambda表达式不是很理解,先简单记录一下最近的所学吧。

一、Lambda在集合中的使用

        在Kotlin的集合库中,有很多封装好的方法供我们使用,其中一些就使用了Lambda的方式,如下面的例子,获取集合中的最大值

/**
 * 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(e)
        if (maxValue < v) {
            maxElem = e
            maxValue = v
        }
    }
    return maxElem
}

        上述源码中可以看到,我们的传参是selector,这个参数是(T) -> R的,T就是我们集合所包含的对象,而R在前面可以看到要求是R : Comparable<R> ,就是要求R实现Comparable接口,因为这样才能进行比较。

        所以我们在调用该方法的时候,可以这样使用

open class Person(var age : Int = 0, var name : String = "", var addr : String = "") //open 修饰之后可以被子类继承
{
    open fun showName() = println(name) //open修饰之后的可以被子类重写
    fun showAddr() = println(addr) //默认为final的,不能被子类重写
    var user : User? = null
}
interface User
{
    val nickName : String
}

上面是我们之前定义的Person类,现在新加了一个User接口,Person中有变量User

当我们对于一个Person对象的集合进行排序时,调用maxBy方法

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

        这种使用方式符合源码中selector : (T) -> R的格式,T就是person,而R是person.age,因为age是Int类型的,默认已经实现了Comparable接口,所以完全可以正常使用。而如果我们对User进行排序的话,由于User没有实现Comparable接口,所以编辑会报错

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?>

        接下来我们继续回到Lambda中,刚才的maxBy的使用看起来还是有点不舒服,我们可以继续改进,毕竟Kotlin的宗旨就是更加美观简介。在Kotlin中,当Lambda表达式是最后一个实参的时候,可以放置在括号的外面,同时,如果只有Lambda这个实参的时候,还可以去掉空的括号对。前面我们也提到过,参数的类型已知的情况下,是可以省略参数类型的,最后,代码就变成了这样

 people.maxBy { person -> person.age }

现在看起来美观了不少,不过,我们还可以继续简化。当是只有一个参数Lambda并且这个参数的类型可以被推导出来时,此时可以用it代替这个参数,如刚才的maxBy方法,people是泛型为Person的集合类,所以可以推导出来,参数中的Lambda一定是Person类型的,所以我们可以用It代替之前的person,这样就简化成了

people.maxBy { it.age }
        对于it的使用,在大量使用It的时候,我们的代码看起来简洁了,但不一定易于理解,所以还是建议使用之前的方式,显式的写出参数的名称,便于后期会代码的阅读,不要盲目的使用it。

        前面介绍的都是将代码块当做参数使用的,接下来我们看怎么讲属性或者方法当做参数使用。我们可以将属性或者方法转换成一个值,然后我们就可以传递方法或者属性了,使用方式如下,用类名 + ::(双冒号) + 属性或方法名表示,如

 people.maxBy(Person::age)

此处就是将Person的age属性直接使用

    val action = {person : Person, message : String -> sendEmail(person,message)}
    val nextAction = :: sendEmail //MainActivity :: sendEmail  当前类为MainActivity,可以省略
    fun sendEmail(person: Person,message:String){}

这里是将方法转换为一个值的方式,action和nextAction是同样的效果,nextAction看起来更加简洁。


二、.集合相关的API

        在Lambda中,有很多函数式的API,熟练掌握这些API,对我们的开发过程会有明显的提升,下面我们一起学习一下。

(1)基础的filter、map、all、any、count、find

        就和方法名一样,filter的作用就是过滤,将符合过滤条件的数据返回。而map是转换的作用,将集合中的每个元素按照一定的规则进行转换,然后返回。

/**
 * 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
}

        通过源码,我们可以看到filter的实现方式,就是遍历集合,将符合要求的元素组成新的集合,值得注意的是,该方法的参数是一个Lambda表达式,结合之前我们学习的Lambda表达式,对于集合过滤的处理,就可以用短短一两行代码完成了。

/**
 * 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
}

        map的实现同filter差不多,也是进行某些操作后,将操作后的数据,组成新的集合返回。

/**
 * 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
}

        通过源码,我们也可以看到all和any的作用,all是判断是否所有元素都满足我们传入的Lambda表达式。any则是判断是否有元素满足我们传入的Lambda表达式,两者是互相对应的关系。

/**
 * 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则是查询满足Lambda表达式的所有元素的个数。

/**
 * 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
}

        find方法的效果是,找到满足Lambda表达式的第一个元素,如果没有满足要求的元素,则返回null。

        上面这些是一些基本的集合操作,在Kotlin源码中还有很多相关的函数供我们使用,比如groupBy可以让我们对集合重组,根据规则分成多个子集合,这要比我们自己来书写这部分业务逻辑简单地多,具体可以参考Collections.kt文件。

(2)序列

        上面的集合操作很便捷,每次都会返回新的集合对象,然后我们可以继续调用新的操作,这样就形成了一个链式的写法,看起来很清楚,但是通过源码我们可以看到,在操作的时候,每次都创建了新的集合,代价就是创建了非常多的中间对象,这些对象我们实际是不需要的,那么可不可以不创建这些临时的对象呢?答案是可以的,接下来让我们了解一下序列。

        序列拥有集合这些链式操作的API,集合调用asSequence()方法即可转换成序列,序列变换后使用toList()等方法可以再转换回集合。让我们以filter为例,看一下序列是如何实现的

/**
 * 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() }
}

        可以看到,asSequence方法返回的是一个Sequence对象,然后调用Sequence的filter方法

/**
 * 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
        }
    }
}

        通过源码可以看出,我们在调用filter方法的时候并没有去创建新的集合接收,而是更改了Iterator的方法,在下一个查询的时候,过滤掉了我们不需要的元素,只在最后toList的时候,才去创建新的集合,这样就省略了很多中间不需要的变量,对我们来说是一个好事。

        tips:通过上面分析,我们得知序列调用的链式方法,只有在最后转换成集合的时候才会生效,所以如果我们没有toList并进行接收,其实是没有做任何操作的,比如people.asSequence().maxBy { it.age }是没有意义的,

        另外,还可以通过generateSequence(seed: T?, nextFunction: (T) -> T?)这个方法创建序列,第一个参数是序列的第一个元素,后一个参数是序列的下一个元素的计算方式。


三、Lambda中更简洁的使用方式: with 和 apply

        让我们先看一个示例,当我们想要拼接a123456789b这样的字符串时,用StringBuilder实现的代码可能是这样的

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

        这个实现是我们通常的写法,也是没有什么问题的,但是我们在里面不停的书写stringBuilder.append,其实也比较麻烦,代码编写的终极目标是就消除所有的重复代码,那么我们可不可以连stringBuilder也省略掉呢?答案当然是可以的,with就可以帮我们完成这件事,看下面示例

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

        看上面代码,我们将stringBuilder对象放在了with后面的括号中,然后在代码块中调用append方法的时候就不用再次书写stringBuilder对象了,同if那样,在最后一个表达式stringBuilder的结果就是with方法的返回值。

        接下来我们看另一种实现方式,用apply实现这个逻辑

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

        apply和with的使用方式差不多,调用apply的那个对象,就是代码块中的默认对象,可以省略那个对象名,直接书写调用的方法即可,和with的区别在于,apply方法是在原有的对象上进行更改,不需要返回值,所以我们修改完成之后,直接调用toString()方法即可。


        今天的文档到这里就结束了,下一章我们将记录学习Kotlin中的类型,对于null怎么办,还有数据类型之间的转换,特殊的数据类型等。

        //下一章  新手上路,Kotlin学习笔记(五)---Kotlin中的类型系统


 

猜你喜欢

转载自blog.csdn.net/WonderfulMTF/article/details/79945621