Kotlin | 5.lambda 编程

2021-03-02 10:19:36 浏览数 (1)

本章内容包括:

  • Lambda 表达式和成员引用
  • 以函数式风格使用集合
  • 序列:惰性地执行集合操作
  • 在 Kotlin中使用 Java 函数式接口
  • 使用带接收者的 lambda

5.1 Lambda表达式和成员引用

代码语言:javascript复制
        /*--------------- 5.1.1 Lambda简介:作为函数参数的代码块-------------*/
        // 代码清单5.1 用匿名内部类实现监听器 java
        /* Java */
//        tv_click.setOnClickListener(new OnClickListener() {
//            @Override
//            public void onClick (View view) {
//                /*点击后执行的动作*/
//            }
//        }

        // 代码清单5.2 用lambda实现监听器 kotlin
        tv_click.setOnClickListener { }

        /*--------------- 5.1.2 Lambda和集合-------------*/
        data class Person(val name: String, val age: Int)

        // 代码清单5.3 手动在集合中搜索
        fun findTheOldest(people: List<Person>) {
            var maxAge = 0
            // 储存年龄最大的人
            var theOldest: Person? = null
            for (person in people) {
                if (person.age > maxAge) {
                    maxAge = person.age
                    theOldest = person
                }
            }
            println(theOldest)
        }

        val listOf = listOf(Person("Alice", 29), Person("Bob", 28))
        findTheOldest(listOf)// Person(name=Alice, age=29)

        // 代码清单 5.4 用lambda在集合中搜索
        val listOf2 = listOf(Person("Alice", 29), Person("Bob", 28))
        // 比较年龄找到最大的元素
        println("5.4--"   listOf2.maxBy { it.age })// 5.4--Person(name=Alice, age=29)

        // 代码清单5.5 用成员引用搜索
//        listOf2.maxBy { Person :: age }


        /*--------------- 5.1.3 Lambda表达式的语法-------------*/
        val sum = { x: Int, y: Int -> x   y }
        // -> 前面是参数 后面是函数体,且lambda始终在{}内

        // 调用保存在变量中的lambda
        println(sum(1, 1))

        // 可以直接调用lambda表达式(无意义)
        val log = { println(42) }()

        // 可以使用库函数 run 来运行传给它的lambda,运行lambda中的代码块
        kotlin.run { println(66) }

        // 回到这行代码
        val listOf3 = listOf(Person("Alice", 29), Person("Bob", 28))
        // 1、{}里是一个lambda表达式,把它作为实参传给函数。这个lambda接收一个类型为Person的参数并返回它的年龄
        listOf3.maxBy({ p: Person -> p.age })
        // 2、kotlin语法约定:如果lambda表达式是函数调用的最后一个实参,可以放在括号的外边。
        listOf3.maxBy() { p: Person -> p.age }
        // 3、当lambda时函数唯一的实参时,还可以去掉调用代码中的空括号对
        listOf3.maxBy { p: Person -> p.age }

        // 代码清单5.6 把lambda作为命名实参传递
        val listOf4 = listOf(Person("Alice", 29), Person("Bob", 30))
        val name = listOf4.joinToString(separator = " ", transform = { p: Person -> p.name })
        println(name)// Alice Bob

        // 代码清单5.7 把lambda放在括号外传递
        listOf4.joinToString(" ") { p: Person -> p.name }

        // 代码清单5.8 省略lambda参数类型
        // 显示地次写出参数类型
        listOf4.maxBy { p: Person -> p.age }
        // 推导出参数类型。和局部变量一样,如果lambda参数的类型可以被推导出来,你就不需要显示地指定它。
        listOf4.maxBy { p -> p.age }

        // 代码清单5.9 使用默认参数名称
        // 使用默认参数名称it代替命名参数,仅在实参名称没有显示地指定时这个默认的名称才会生成
        listOf4.maxBy { it.age }
        // 如果你用变量存储lambda,那么就没有可以推断出参数类型的上下文,所以你必须显示地指定参数类型
        val getAge = { p: Person -> p.age }
        listOf4.maxBy(getAge)
        // lambda可以包含更多的语句
        val sum2 = { x: Int, y: Int ->
            println("the sum of $x and $y ..")
            x   y
        }
        println(sum2(1, 1))


        /*--------------- 5.1.4 在作用域中访问变量-------------*/
        // 代码清单5.10 在lambda中使用函数参数
        fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
            // 接收lambda作为实参,指定对每个元素的操作
            messages.forEach {
                // 在lambda中访问“prefix”参数
                println("$prefix $it")
            }
        }

        val errors = listOf("403 Forbidden", "404 Not Found")
        printMessagesWithPrefix(errors, "Error:")

        // 代码清单5.11 在lambda中改变局部变量
        fun printProblemCounts(responses: Collection<String>) {
            var clientErrors = 0
            var serverErrors = 0
            responses.forEach {
                if (it.startsWith("4")) {
                    clientErrors  
                } else if (it.startsWith("5")) {
                    serverErrors  
                }
            }
            println("$clientErrors client errors, $serverErrors server errors")
        }

        val responses = listOf("200 ok", "418 I'm a teapot", "500 Internal Server Error")
        // 1 client errors, 1 server errors
        printProblemCounts(responses)
        /**
         * 和Java不一样,Kotlin允许在lambda内部访问非final变量甚至修改它们。我们称这些变量被lambda捕捉。
         * 原理:
         * 1、当你捕捉 final 变量时,它的值和使用这个值的 lambda 代码存储。
         * 2、而对非 final 变量来说,它的值被封装在一个特殊的包装器中,这样你就可以改变这个值,而对这个包装器的引用会和 lambda 代码一起存储。
         */
        // 模拟捕捉可变变量的类
        class Ref<T>(var value: T)

        val ref = Ref(0)
        // 形式上是不变量被捕捉了,但是存储在字段中的实际值是可以修改的。
        val inc = { ref.value   }

        // 上面的是这里的原理
        var counter = 0
        val inc1 = { counter   }


        /*--------------- 5.1.5 成员引用-------------*/
        // 来源:如果你想要当做参数传递的代码已经被定义成了函数,该怎么办?使用 :: 预算符将函数转换为一个值就可以传递它。
        /**  :: 之前是类; :: 之后是成员(一个方法或一个属性),成员后面不能加括号 */
        val getAge2 = Person::age

        // 同样的功能
        val getAges = { p: Person -> p.age }
        // 成员引用和调用该函数的lambda具有一样的类型,所以可以互换使用
        listOf4.maxBy(Person::age)

        // 还可以使用顶层函数(不是类的成员)
        fun salute() = println("Salute!")
        run(::salute)

        // 如果lambda要委托给一个接收多个参数的函数,提供成员引用代替它将会非常方便
        fun sendEmail(person: Person, message: String) = "$person send $message !"
        val action = { person: Person, message: String ->
            // 这个lambda委托给sendEmail函数
            sendEmail(person, message)
        }
        // 可以用成员引用代替
        val nextEmail = ::sendEmail

        action(Person("action", 1), "哈哈哈")
        nextEmail(Person("nextEmail", 1), "哈哈哈")

        // 可以用构造方法引用存储或延期执行创建类实例的动作。构造方法引用的形式是在双冒号后指定类名称:
//        data class Person(val name: String, val age: Int)
        // 创建"Person"实例的动作被保存成了值
        val createPerson = ::Person
        val p = createPerson("Alice", 29)
        println(p)// Person(name=Alice, age=29)

        // 还可以用同样的方式引用扩展函数:
        fun Person.isAdult() = age >= 21
        val kFunction1 = Person::isAdult
        // 尽管isAduth不是Person类的成员,还是可以通过引用访问它,这和访问实例的成员没什么两样:person.isAduth()

        // 绑定引用
        val p2 = Person("Dmitry", 22)
        val personsAgeFunction = Person::age
//        val personsAgeFunction0 = fun(p:Person):Int =p.age
//        val personsAgeFunction1 = fun(p:Person):Int {
//            return p.age
//        }
//        val personsAgeFunction2 = { p: Person -> p.age }
        println(personsAgeFunction(p2))// 22

        // 在kotlin1.1中可以使用绑定成员引用
        val dmitrysAgeFunction = p2::age
        println(dmitrysAgeFunction)

        /**
         * 注意:
         * personsAgeFunction 是一个单参数函数(返回给定人的年龄),
         * 而 dmitrysAgeFunction 一个零参数的函数(返回已经指定好的人的年龄)
         * 在 Kotlin 1.1 之前,你需要显式地写出 lambda { p. age },而不是使用绑定成员引用 p: age
         */

5.2 集合的函数式API

代码语言:javascript复制
        /*--------------- 5.2.1 基础:filter 和 map-------------*/
        // filter函数遍历合集并选出应用给定lambda后会返回true的那些元素:
        val list = listOf(1, 2, 3, 4)
        // 只有偶数留了下来
        println(list.filter { it % 2 == 0 })// [2,4]

        // 如果你想留下那些超过30岁的人,可以用filter
        val people = listOf(Person("jingbin", 12), Person("jinbeen", 23))
        println(people.filter { it.age > 20 })//  [Person(name=jinbeen, age=23)]

        /**
         * filter可以移除不想要的元素但不会改变这些元素。map用于元素的变换。
         * map 函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合。
         */
        // 把数字列表变换成他们平方的列表
        val list2 = listOf(1, 2, 3, 4)
        println(list2.map { it * it })// [1, 4, 9, 16]

        // 打印姓名列表,而不是人的完整信息
        val people2 = listOf(Person("jingbin", 12), Person("jinbeen", 23), Person("haha", 23))
        println(people2.map { it.name })// [jingbin, jinbeen, haha]
        // 使用 成员引用 重写
        println(people2.map(Person::name))

        // 打印出年龄超过30岁人的名字
        println(people2.filter { it.age > 20 }.map(Person::name))

        // 分组中所有年龄最大的人的名字 (集合中有100个人就会执行100遍)
        println(people2.filter { it.age == people2.maxBy(Person::age)?.age })// [Person(name=jinbeen, age=23), Person(name=haha, age=23)]
        // 只计算一次最大年龄
        val maxAge = people2.maxBy(Person::age)?.age
        println(people2.filter { it.age == maxAge })

        // 注意不要重复计算!
        val map = mapOf(0 to "zero", 1 to "one")
        println(map.mapValues { it.value.toUpperCase() })

        /**键和值分别由各自的函数来处理。filterKeys和mapKeys过滤和变换map的键,而另外的filterValues和mapValues过滤和变换对应的值*/


        /*--------------- 5.2.2 "all" "any" "count"和"find":对集合应用判断式-------------*/
        /**
         * all和any 检查集合中的所有元素是否都符合某个条件。
         * count 检查有多少元素满足判断式
         * find  返回第一个符合条件的元素
         */
        // 1.检查一个人是否还没到28岁
        val canBeInClub27 = { p: Person -> p.age <= 20 }
        // 2.如果对是否所有元素都满足判断式,应使用all函数
        val people3 = listOf(Person("jingbin", 11), Person("jinbeen", 22))
        println(people3.all(canBeInClub27))// false
        // 3.检查集合中是否至少存在一个匹配元素,那就用any
        println(people3.any(canBeInClub27))// true
        // 4.!all【不是所有】可以用any加上这个条件的取反来替换。最好使用any 条件取反
        val listOf = listOf(1, 2, 3)
        // 不全部==3
        println(!listOf.all { it == 3 })
        // 有一个!=3
        println(listOf.any { it != 3 })

        // count 有多少个元素满足了判断式
        val people4 = listOf(Person("jingbin", 11), Person("jinbeen", 22))
        println(people4.count(canBeInClub27))// 1
        // 注意正确的使用函数!count 和 size,以下会创建一个不必要的中间集合!应使用count!
        println(people4.filter(canBeInClub27).size)

        // find 找到第一个满足判断式的元素,如果没有找到就返回null
        val people5 = listOf(Person("jingbin", 11), Person("jinbeen", 15))
        println(people5.find(canBeInClub27))// Person(name=jingbin, age=11)
        println(people5.firstOrNull(canBeInClub27)) // Person(name=jingbin, age=11)


        /*--------------- 5.2.3 groupBy: 把列表转换成分组的map-------------*/
        // 将相同年龄的人放在一组
        val people6 = listOf(Person("jingbin", 11), Person("jinbeen", 15), Person("haha", 15))
        println(people6.groupBy { it.age == 15 })// {false=[Person(name=jingbin, age=11)], true=[Person(name=jinbeen, age=15), Person(name=haha, age=15)]}
        println(people6.groupBy { it.age })// {11=[Person(name=jingbin, age=11)], 15=[Person(name=jinbeen, age=15), Person(name=haha, age=15)]}

        // 使用成员引用把字符串按照首字母分组:
        val listOf1 = listOf("a", "b", "c", "cd")
        // 注意
        // 1、成员引用在()里,不在{}里
        // 2、这里first并不是String类的成员,而是一个扩展,可以把它当做成员引用访问
        println(listOf1.groupBy(String::first)) // {a=[a], b=[b], c=[c, cd]}

        /*--------------- 5.2.4 flatMap 和 flatten:处理嵌套集合中的元素-------------*/
        data class Book(val title: String, val authors: List<String>)

        val books = listOf(Book("三体", listOf("刘慈欣", "刘德华")), Book("我的世界", listOf("刘慈欣", "韩寒")))
        // 统计图书馆中的所有作者的set
        // 每本书都可能有多个作者,属性book.authors存储了每本书籍的作者集合。flatMap函数把所有书籍的作者合并成了一个扁平的列表。
        // toSet调动移除了集合中所有重复的元素
        println(books.flatMap { it.authors }.toSet())// [刘慈欣, 刘德华, 韩寒]
        // 如果不需要任何变换,只是平铺一个集合 使用flatten

        /**flatMap做了两件事:首页根根据作为实参给定的函数对集合中的每个元素做变换(或者说映射),然后把多个列表合并(或者说平铺)成一个列表。*/
        val listOf2 = listOf("abc", "def")
        println(listOf2.flatMap { it.toList() })// [a, b, c, d, e, f]

5.3 惰性集合操作:序列

代码语言:javascript复制
        // 很多时候使用函数会创建一个中间集合,有损性能。序列给了一个选择,避免创建中间对象。
        val people = listOf(Person("jingbin", 12), Person("jinbeen", 23))
        // 下面的链式调用会创建两个列表,一个保存filter 一个保存map
        println(people.map(Person::name).filter { it.startsWith("j") })// [jingbin, jinbeen]

        // 为提高效率使用序列,而不是直接使用集合:
        val list = people.asSequence()// 把初始集合转换成序列
                .map(Person::name)// 序列支持和集合一样的api
                .filter { it.startsWith("j") }
                .toList()// 把结果序列转换回列表
        println(list)// [jingbin, jinbeen]

        /**
         * Kotlin惰性集合操作的入口就是Sequence接口。
         * 这个接口表示的就是一个可以逐个列举元素的元素序列。Sequence值提供一个方法,iterator,用来从序列中获取值。
         * 通常,需要对一个大型集合执行链式操作时要使用序列。
         */

        /*--------------- 5.3.1 执行序列操作:中间和末端操作-------------*/
        // 一次中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素。
        // 一次末端操作返回的是一个结果,这个结果可能是集合,元素、数字,或者其他从初始集合的变换序列中获取的任意对象。
        // 中间操作:map{}、filter{}
        // 末端操作:toList()

        // 中间操作始终都是惰性的! 一行的话使用 ;
        listOf(1, 2, 3, 4).asSequence().map { println("map($it) ");it * it }
                .filter {
                    println("filter($it) ")
                    it % 2 == 0
                }
        // 以上不会被调用,只有在获取结果的时候会被调用
        listOf(1, 2, 3, 4).asSequence().map { println("map($it) ");it * it }
                .filter {
                    println("filter($it) ")
                    it % 2 == 0
                }.toList()
        // 处理完第一个元素,然后完成第二个元素的处理。这样意味着有时候部分元素不需要变换,就可以拿到拿到结果。
        // 结果:  map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)

        // 找到平方后的第一个大于3的数。只会执行到2 (有时候部分元素不需要变换,就可以拿到拿到结果。)
        println(listOf(1, 2, 3, 4).asSequence().map { it * it }.find { it > 3 })//4

        // 在集合上执行操作的顺序也会影响性能。
        // 假设有一个人的集合,想要打印集合中那些长度小于某个限制的人名。
        val people2 = listOf(Person("jingbin", 12), Person("jin", 23), Person("jinbeen", 23), Person("Bob", 21))
        // 先map再filter  先把所有名字找出再筛选
        println(people2.asSequence().map(Person::name).filter { it.length < 4 }.toList())
        // 先filter再map  先筛选再找出名字,显然这个好
        println(people2.asSequence().filter { it.name.length < 4 }.map(Person::name).toList())


        /*--------------- 5.3.2 创建序列-------------*/
        // generateSequence 函数,给定序列中的前一个元素,这个函数会计算出下一个元素。
        // 代码清单5.12 生成并使用自然数序列
        val generateSequence = generateSequence(0) { it   1 }
        val numTo100 = generateSequence.takeWhile { it <= 100 }
        // 当获取结果 sum 时,所有被推迟的操作都被执行
        println(numTo100.sum())// 5050

        // 代码清单5.13 创建并使用父目录的序列。  查询文件是否放在隐藏目录中
        // any 至少有一个元素匹配给定谓词
        // find 找到第一个符合条件的情况
        fun File.isInsideHiddenDirectory() =
//                generateSequence(this) { it.parentFile }.any { it.isHidden }
                generateSequence(this) { it.parentFile }.find { it.isHidden }

        val file = File("/users/svtk/.hiddenDir/a.txt")
        println(file.isInsideHiddenDirectory())

5.4 使用Java函数式调用接口

代码语言:javascript复制
        /*--------------- 5.4.1 把lambda当做参数传递给Java方法-------------*/
        // 可以把lambda传给任何期望函数式接口的地方。

        // 下面的方法有一个Runnable类型的参数
        /*java*/
//        void postponeComputation(int delay,Runnable computation)
        // 在Kotlin中可以调用它并把一个lambda作为实参传给它。编译器会自动把它转换为一个Runnable实例
        JavaCode().postponeComputation(1000) { println(42) }
        JavaCode().postponeComputation(1000, { println(42) })
        // 通过显示的创建一个实现了Runnable的匿名对象也能达到同样的效果:
        JavaCode().postponeComputation(1000, object : Runnable {
            override fun run() {
                println(42)
            }
        })
        /**
         * 当显示的声明对象时,每次调用都会创建一个新的实例。
         * 使用lambda的情况不同:如果lambda没有访问任何来自自定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用
         */
        // 整个程序里只会创建一个Runnable的示例
        JavaCode().postponeComputation(1000) { println(42) }

        // 等价于这种实现
        val runnable = Runnable { println(42) }
        JavaCode().postponeComputation(1000, runnable)

        // 下面实例每次调用都会使用一个新的Runnable实例,把id值存储在它的字段中:
        // lambda会捕捉id这个变量
        fun handleComputation(id: String) {
            // 每次handleComputation调用时都创建一个Runnable的新实例
            JavaCode().postponeComputation(1000) { println(42) }
        }

        /**
         * 每个lambda表达式都会被编译成一个匿名类,除非它是一个内联的lambda。
         * 如果lambda捕捉到了变量,每次被捕捉的变量会在匿名类中有对应的字段,而且每次(对lambda的)调用都会创建一个这个类的匿名类的实例。
         * 编译器给每个被捕捉的变量生成了一个字段和一个构造方法参数。
         */
//        class HandleComputation$1(val id:String):Runnable{
//            override fun run(){
//                println(id)
//            }
//        }
//        fun handleComputation(id:String){
//            JavaCode().postponeComputation(1000,HandleComputation$1(id))
//        }


        /*--------------- 5.4.2 SAM构造方法:显示地把lambda转换成函数式接口-------------*/
        // SAM构造方法是编译器生成的函数,让你执行从lambda到函数接口实例的显示转换。

        // 代码清单5.14 使用SAM构造方法来返回值
        fun createAllDoneRunnable(): Runnable {
            return Runnable { println("All done!") }
        }
        createAllDoneRunnable().run()

        // 代码清单5.15 使用SAM构造方法来重用listener实例
        val listener = View.OnClickListener { view ->
            val text = when (view.id) {
                R.id.tv_click -> "first"
                else -> "unkown"
            }
            toast(text)
        }
        tv_click.setOnClickListener(listener)

5.5 带接收者的lambda: “with”与“apply”

代码语言:javascript复制
        /*--------------- 5.5.1 “with”函数-------------*/
        // 对同一对象执行多次操作,而不需要反复把对象的名称写出来。
        // 代码清单5.16 构建字母表
        fun alphabet(): String {
            val result = StringBuilder()
            for (letter in 'A'..'Z') {
                result.append(letter)
            }
            result.append("nNow I know the alphabet!")
            return result.toString()
        }
        println(alphabet())

        // 代码清单5.17 使用with构建字母表
        fun alphabet2(): String {
            val stringBuilder = StringBuilder()
            // 指定接收者的值,你会调用它的方法
            return with(stringBuilder) {
                for (letter in 'A'..'Z') {
                    this.append(letter)
                }
                // 通过显示的 this 或者省掉this 都可以调用方法
                append("nNow I know the alphabet!2")
                // 从lambda返回值
                this.toString()
            }
        }
        println(alphabet2())

        /*
         * with 结构看起来是一种特殊的语法结构,但它实际上是一个接收两个参数的函数:这个例子分别是stringBuilder和一个lambda。lambda放在了括号外。
         * lambda是一种类似普通函数的定义行为的方式。而带接收者的lambda是类似扩展函数的定义行为的方式。
         */

        // 代码清单5.18 使用with和一个表达式函数体来构建字母表
        fun alphabet3() = with(StringBuilder()) {
            for (letter in 'A'..'Z') {
                append(letter)
            }
            append("nNow I know the alphabet!3")
            toString()
        }

        // with 返回的值是执行lambda代码的结果,该结果就是lambda中的最后一个表达式的值。
        // apply 返回的是接收者对象


        /*--------------- 5.5.1 “apply”函数-------------*/
        // apply 函数几乎和 with 函数一模一样, 唯一的区别是 apply 始终会返回作为实参传递给它的对象(换句话说,接收者对象)。
        // 代码清单5.19 使用apply构建字母表
        fun alphabet4() = StringBuilder().apply {
            for (letter in 'A'..'Z') {
                append(letter)
            }
            append("nNow I know the alphabet!4")
        }.toString()

        // 代码清单5.20 使用apply初始化一个TextView
        fun createViewCustomAttributes(context: Context) = TextView(context).apply {
            text = "Sample Text"
            textSize = 20f
            setPadding(10, 0, 0, 0)
        }

        /**
         * 新的 TextView 实例创建后立即被传给了 apply 。在传给 apply lambda 中, TextView 实例变成了(lambda的)接收者,你就可以调用它的方法并设置它的属性。
         * Lambda 执行之后, apply回己经初始化过的接收者实例 它变成了 createViewWithCustomAttributes函数的结果。
         */

        // 代码清单5.21 使用buildString创建字母表
        fun alphabet5() = buildString {
            for (letter in 'A'..'Z') {
                append(letter)
            }
            append("nNow I know the alphabet!5")
        }
        /*
         * buildString 会负责创建 StringBuilder 调用 toString,buildString 的实参是一个带接收者的 lambda ,接收者就是StringBuilder。
         * buildString 函数优雅地完成了借助StringBuilder创建String的任务。
         */

总结

  • Lambda 允许你把代码块当作参数传递给函数。
  • Kotlin 可以把 lambda 放在括号外传递给函数,而且可以用 it 引用单个的lambda 参数。
  • lambda 中的代码可以访问和修改包含这个 lambda 调用的函数中的变量。
  • 通过在函数名称前加上前缀 :: ,可以创建方法、构造方法及属性的引用,并用这些引用代替 lambda 传递给函数。
  • 使用像 filter map all any 等函数,大多数公共的集合操作不需要手动迭代元素就可以完成。
  • 序列允许你合并一个集合上的多次操作,而不需要创建新的集合来保存中间结果。
  • 可以把 lambda 作为实参传给接 Java 函数式接口(带单抽象方法的接口,也叫作 SAM 接口)作为形参的方法。
  • 带接收者的 lambda 种特殊的 lambda ,可以在这种 lambda 中直接访问一个特殊接收者对象的方法。
  • with 标准库函数允许你调用同一个对象的多个方法,而不需要反复写出这个对象的引用 apply 函数让你使用构建者风格的 API 创建和初始化任何对象

0 人点赞