Kotlin基础 - 第七章lambda表达式

本文深入探讨了Kotlin中的lambda表达式,包括它们的语法、在作用域访问变量的能力、与Java函数式接口的互操作以及带接收者的lambda如with和apply的使用。通过实例展示了如何简化代码,例如在集合操作中使用filter和map,以及如何使用groupBy和flatMap处理数据。文章强调了lambda表达式在避免代码重复和提高代码可读性方面的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

kotlin中的lambda表达式



#### [kotlin官方文档 https://www.kotlincn.net/docs/reference/](https://www.kotlincn.net/docs/reference/) ####

lambda即lambda表达式,简称lambda。本质上是可以传递给其它函数的一小段代码。有了lambda,可以轻松地把通用代码结构抽取成库函数。lambda最常见的用途是和集合一起配合。kotlin甚至还拥有带接收者的lambda,这是一种特殊的lambda。

lambda的表达式和成员引用

lambda简介:作为函数参数的代码块

代码中存储和传递一小段行为是常有的任务。在老版本Java中很多需要匿名内部类来实现(java8也引入了lambda,大为改观),语法太过啰嗦。
函数式编程提供了另外一种解决方案:把函数当作值来对待。可以直接传递函数,而不需要先声明一个类再传递这个类的实例。使用lamdba表达式不仅会使代码更简洁,并且可以高效地直接传递代码快作为函数参数。

笔者注,后文我会省略大段的lambda理论文字的介绍,因为在我的认知中,接触了java8(尤其是Android)的一定对lambda有一定的认知,如果没有请自行查阅资料。

	//点击监听 java lambda
   	btn.setOnClickListener(view ->...);

  	//点击监听 kotlin lambda
    btn.setOnClickListener { ... }

kotlin的lambda相对于java8的lambda语法更简洁,事实上它们做的事情是一样的,都是替代了匿名内部类对象。

良好的编程风格主要原则之一就是避免代码中的任何重复。kotlin的lambda可以帮助避免代码重复(因为对集合执行通常遵循通用模式)。

	//建立数据类Person作为数据源
	data class Person(val name: String, val age: Int) {
	     
	}

假设需求为找到列表年龄最大的人,平时我们打代码可能是这样的。

	data class Person(val name: String, val age: Int) {
	
	}
	//未使用lambda表达式
	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 people = listOf(Person("jack", 29),
            Person("nick", 23),
            Person("jone", 26))

    findTheOldest(people)//Person(name=jack, age=29)

如果我们使用lambda表达式,我对门的代码直接可以简化成下面的样子

	class Person(var name: String, var age: Int) {
	
	}


 
    val people = listOf(
        Person("小明", 12),
        Person("小红", 12),
        Person("小花", 12),
        Person("小美", 12),
        Person("小帅", 12)
    )

   println(people.maxBy { it.age % 2 }!!.name)  //小帅

maxby函数可以在任何集合上调用,且只需要一个实参:一个函数,指定比较哪个值来找到最大元素。{it.age}就是实现了这个逻辑的lambda。它接收一个集合中元素作为实参(it引用)并且返回用来比较的值。如上述代码中,集合元素是Person对象,用来比较的是存储在其age属性中的年龄。
如果lambda刚好是函数或者属性委托,可以用成员引用替换。

	val people = listOf(
        Person("小明", 12),
        Person("小红", 14),
        Person("小花", 20),
        Person("小美", 10),
        Person("小帅", 13)
    )
	
	//注意两者的区别这种传递属性值的方式应当使用在括号里面
    println(people.maxBy(Person::age)!!.name)  //小花

主使用成员引用的方式传递闭包(代码块)的时候仅可以传递属性值

lambda表达式语法

如前所述(更多详情自行查阅资料或参 见初识lambda) lambda把一小段行为进行编码,既能当作值传递又能独立声明到一个变量中存储。

  	var sum = { x: Int, y: Int -> x + y }
    println(sum(1, 6)) //7

也可以无意义的直接调用lambda表达式

	//直接调用无意义(等同于直接执行lambda具体代码)
  	{ println(42)}()//42

正确姿势应该使用kotlin库函数run。

  	//正确姿势un调用
    run { println(42) }//42

回到people的例子,不用任何简明语法来重写。

    //未简化的标注lambda
    people.maxBy({ P:Person ->P.age} )

kotlin有一种语法约定,如果lambda表达式是函数调用的最后一个实参,它可以放到括号外面。

  	//lambda是函数调用的最后一个实参,可以放到()外
    people.maxBy( ){ P:Person ->P.age}

当lambda是函数唯一实参时,还可以去掉()

 	//lambda是函数唯一实参,可以省略()
    people.maxBy{ P:Person ->P.age}

三种语法语义完全一样,但是最后一种更易读。但是当lamdba有两个或多个实参时,不能把超过一个的lambda放到外面,推荐使用常规语法。

把当然,lambda也能作为命名实参传递

	val people = listOf(
        Person("小明", 12),
        Person("小红", 14),
        Person("小花", 20),
        Person("小美", 10),
        Person("小帅", 13)
    )


  var str = people.joinToString(separator = " ", transform = { people: Person -> people.name })
  println(str)  // 小明 小红 小花 小美 小帅

因为只有一个实参,所以可以放在括号外

	var str = people.joinToString(separator = " ") { people: Person -> people.name }

    println(str)	// 小明 小红 小花 小美 小帅

甚至可以根据类型推导特性而移除参数类型。

 	//显示的写出参数推导类型
    var str = people.joinToString(separator = " ") { people: Person -> people.name }

    //推导出参数类型
	var str = people.joinToString(separator = " ") { people -> people.name }

    println(str)

类型推导与局部变量一样,如果能成功被推导,就不需要显示的指定。

使用默认参数名称(注意)

 	//使用默认参数名称
    people.maxBy { it.age} //"it"是自动生成的参数名称

默认名称it只会在实参名称没有显示的指定时候才会生成。it能大大缩短简化代码,但是不应该滥用,尤其是在lambda嵌套情况下,最好显示声明lambda参数。否则很难搞清it引用的到底是哪个值,本末倒置。

如果用变量存储lambda,就没有可以推断出参数类型的上下文,必须显示的指定参数。

	//变量存储lambda,必须显示指定参数类型
 	var getAge = {person:Person -> person.age}

	//输出结果:Person(name=小花, age=20)
    println(people.maxBy(getAge)) 

lambda当然也能包含更多语句。

    val sum = { x: Int, y: Int ->
        println("this is $x and $y and sum is")
        x+y
    }
    //this is 1 and 2 and sum is
    // 3
    println(sum(1,2))

在作用域访问变量

lambda表达式有个形影不离的概念:从上下文中捕捉变量。

当在函数内部声明一个匿名内部类的时候,能够在这个匿名内部引用这个函数的参数和局部变量。lambda同样可以。

	//使用lambda进行循环的函数
	fun printMessagesWithPrefix(messages:Collection<String>,prefix:String){
	    //接收lambda作为实参,指定对每个元素的操作
	    messages.forEach{
	        //在lambda中访问函数的参数
	        println("$prefix and $it")
	    }
	}

    printMessagesWithPrefix(errors,"Error:")
    //Error: and 403 Forbidden
    // Error: and 404 not Found

这里kotlin和java的一个显著区别就是,在kotlin中不会仅局限于访问final变量,在lambda内部也可以修改这些变量。

	fun printProblemCounts(respones: Collection<String>) {
	    //声明在lambda内部访问的变量
	    var clientErrors = 0
	    var serverErrors = 0
	    respones.forEach {
	        if (it.startsWith("2")) {
	            //在lambda中修改变量
	            clientErrors++
	        }else if(it.startsWith("4")||it.startsWith("5")){
	            serverErrors++
	        }
	    }
	    println("$clientErrors for client,$serverErrors for server")
	
	}

	printProblemCounts(arrayListOf("504","201"))
	// 1 for client,1 for server

kotlin允许在lambda中内部访问非final变量甚至修改它们,我们称这些变量被lambda捕捉。
默认情况下,局部变量的生命期被限制在这个函数中。但是如果它被lambda捕捉了,使用这个变量的代码可以被存储并稍后执行。原理是当你捕捉final变量的时候,它的值和使用这个值的lambda代码一起存储。而对于非变量来说,它的值被封装在一个特殊包装器中,当改变这个值时候,对包装器的引用会和lambda一起存储。

	//模拟捕捉可变变量类(捕捉的实现原理)//注意类中的局部参数必须使用 var修饰
	class Ref<T>(var value: T)
	
	val counter = Ref(2)
	//并不是变量被捕捉,但是存储字段值实际值是可以修改的
	val inc = { counter.value++ }
	//简单理解,对象的地址不可变,但是对象的var属性是可以被访问和修改的
	
	//实际代码隐藏包装器
	var counter =0
	val inc = {counter++}

当捕捉变量var时候,它的值会如上述代码一样作为Ref类的一个实例被存储下来,Ref是final的当然能轻易捕捉,而实际值又是存储在其字段里,所以能在lambda里修改。

着重强调注意的是,如果lambda被用作事件处理器或者在其它异步执行的情况,对局部变量修改只会在lambda执行时候发生。

    //错误代码
    fun tryToCountButtonClicks(button: View): Int {
        var clicks = 0
        button.setOnClickListener{clicks++}
        return clicks
    }

函数返回值始终是0,因为onClick是在函数返回后调用(java的接口回调),即此函数执行完后才会执行onClick,正确姿势是不应该把clicks存储在函数的局部变量中。

成员引用
kotlin和java8一样,如果把函数转换成一个值,就可以直接转换它使用::运算符转换。

	data class Person(var name: String, var age: Int) {
	    //成员引用
	    val getName = Person::name
	    等价
	    val getName = { person: Person -> person.name }
	}

成员引用提供简明语法,来创建一个调用单个方法或者访问单个属性的函数值。::把类名称和引用成员(一个方法或属性)隔开。

不管引用的是函数还是属性,都不应该在成员引用的名称后面加()。成员引用和调用该函数的lambda具有一样的类型。

	//成员引用
	people.maxBy(Person::age)

成员引用还可以引用顶层函数

	//顶层函数
	fun salute()= println("Salute")
	//引用顶层函数,::salute当作实参传递给了lambda
	run(::salute)
	println(run(::salute))//Salute

如果lambda要委托一个或者多个参数的函数,提供成员引用代替非常方便。

	//将lambda委托给sendEmail函数
    val action={person:Person,msg:String ->sendEmail(person,msg)}
 	//成员引用代替,上面的代码和下面是等价的
    val nextAction = ::sendEmail

也可以使用构造方法引用存储或者延期执行创建类的实例。

	data class Person(var name: String, var age: Int) {
	    //成员引用
	    val getName = Person::name
	    等价
	    val getName2 = { person: Person -> person.name }
	}

	//调用委托createPerson为Person的构造函数
 	val createPerson = ::Person
    val p = createPerson("小红", 27)

	//Person(name=小红, age=27)
    println(p)  

其实可以理解为将方法的调用代码存储在变量 createPerson 中

也可以用同样的方式来引用扩展函数。

 	//Person的扩展函数
	fun Person.isAdult()=age>=23

    //成员引用,等价person.isAdult
    val isHave =Person::isAdult

对应代码

	data class Person(var name: String, var age: Int) {
	    //成员引用
	    val getName = Person::name
	    等价
	    val getName2 = { person: Person -> person.name }
	
		//Person的扩展函数
	    fun isAdult() = age >= 25
	}
	
	//成员引用,等价person.isAdult
	val isHave = Person::isAdult
	val p = createPerson("小红", 27)
	
	//true
	println(isHave(p))

集合式函数API

笔者注:集合的lambda函数式是存在于所有支持的语言中的,所以笔者会简略带过这些函数的效果,着重于这些函数在kotlin里的运用。如果有心了解更多,请自行查阅资料(java推荐lambda函数式编程或了解rxjava操作符)

filter和map
  • filter即过滤,它会遍历集合并选出应用给定lambda后返回未true的元素。使用它可以移除不满足条件的元素(数据源并不会改变)

      val list = listOf(1, 2, 3, 4, 5, 6)
      
      println(list.filter { it % 2 == 0 })
      //[2, 4, 6]
    
      val people = listOf(
              Person("小明", 22),
              Person("小红", 24),
              Person("小花", 29),
              Person("小美", 30),
              Person("小帅", 19)
          )
      
      println(people.filter { it.age > 24 })
      //[Person(name=小花, age=29), Person(name=小美, age=30)]
    
  • map对集合每一个元素应用给定的函数并把结果收集到一个新集合,即元素变换。

      val list = listOf(1, 2, 3, 4, 5, 6)
      println(list.map { it * 2 })
      //[2, 4, 6, 8, 10, 12]
    
    
      val people = listOf(
              Person("小明", 22),
              Person("小红", 24),
              Person("小花", 29),
              Person("小美", 30),
              Person("小帅", 19)
          )
      
     
      println(people.map {
          if (it.age > 24) {
              it.age = it.age + 4
          }
      	it
      })
      //[Person(name=小明, age=22), Person(name=小红, age=24), Person(name=小花, age=33), Person(name=小美, age=34), Person(name=小帅, age=19)]
    
  • lamdba当然可以链式调用。

      val people = listOf(
      	        Person("小明", 22),
      	        Person("小红", 24),
      	        Person("小花", 29),
      	        Person("小美", 30),
      	        Person("小帅", 19)
      )
    
      //  找出年所有成年人,把他们的年龄加4
    	println(people.filter { it.isAdult() }.map {
          if (it.age > 24) {
              it.age = it.age + 4
          }
          it
      })
      //[Person(name=小花, age=33), Person(name=小美, age=34)]
    
  • 使用filter找出年龄最大的人

      //找出年龄最大的人(此代码有不足,每次遍历都会查找最大年龄,性能损耗大,同一个事只做一次)
    	println(people.filter { it.age == people.maxBy(Person::age)?.age })
      //[Person(name=小美, age=30)]
    

此代码的问题在于,它会对每个人都会重复寻找最大年龄的过程,如果集合里有百个,千个人(元素),简直要原地爆炸。

  • 改进方案

      //先找出集合中年龄最大的人的年龄
      val maxAge = people.maxBy(Person::age)?.age
      //然后再用filter去过滤
      println(people.filter { it.age == maxAge})
      //[Person(name=小美, age=30)]
    

如果没有必要当然就不需要重复计算。lambda表达式隐藏了底层操作的复杂性,必须牢记自己的代码在干什么。

  • 也能对map进行过滤和变换。

filterKeys(过滤map的键),mapKeys(变换map的键),filterValues(过滤map的值),mapValues(变换对应的map值)

	val numbers = mapOf(0 to "ZERO", 1 to "ONE", 2 to "TWO")

    //获取map里小于2的key
    println(numbers.filterKeys { it < 2 })//{0=ZERO, 1=ONE}
     
    把key变换为value
    println(numbers.mapKeys { it.value })//{ZERO=ZERO, ONE=ONE, TWO=TWO}
     
    //过滤map的value
    println(numbers.filterValues { it.startsWith("O") })//{1=ONE}
    
    //转换map的value为小写
    println(numbers.mapValues { it.value.toLowerCase() })//{0=zero, 1=one, 2=two}
  • all,any,count,find:对集合的判断应用

检查集合中所有的元素是否都符合某个条件(又或是是否存在符合的元素)。它们是通过all和any函数表达。count为检查有多少个元素满足判断式,find函数返回第一个符合条件的元素。

	val people = listOf(
			        Person("小明", 22),
			        Person("小红", 24),
			        Person("小花", 29),
			        Person("小美", 30),
			        Person("小帅", 19)
	)


  	//年龄是否满足23
    val isAge23 = { person: Person -> person.age < 23 }
    //检查集合看是否所有元素满足(all)
    println(people.all(isAge23))//false

    //检查集合中是否至少存在一个匹配的元素(any)
    println(people.any(isAge23))//true

    //检查有多少个元素满足判断式(count)
    println(people.count(isAge23))//true

    //找到第一个满足判断式的元素(find)
    //如有多个匹配返回其中第一个元素,没有返回null。同义函数firstOrNull。
    println(people.find(isAge23))//Person(name=小明, age=22)


    //找到倒数第一个满足判断式的元素(find)
    //如有多个匹配返回其中第一个元素,没有返回null。同义函数lastOrNull。
    println(people.findLast(isAge23))//Person(name=小帅, age=19)
  • 值得一提的是!all(不是所有)加上某个条件,应该用any取反

      val list = listOf(1, 2, 3)
      此种方式不推荐
      println(!list.all { it < 2 })//false
    
      //推荐此种方式定义(lambda参数中条件取反)
      println(list.any { it >= 2 })//true
    

注意:all 、any 对立条件一般采用对 lambda 表达式取反来达到取反的目的

  • 再就是count和.size。count方法只是跟踪匹配元素的数量,不关心元素本身,所以更高效。.size需要配合filter过滤从而中间会创建新集合用来存储。

      //.size的方式(具体使用情况看实际,只关心数量不推荐此方式)
      println(people.filter (isAge23).size)
    

groupBy:把列表转换成分组的map

如果需要把不同的元素划分到不同的分组,使用groupaBy事半功倍。

	val people = listOf(
        Person("小明", 22),
        Person("小红", 22),
        Person("小花", 29),
        Person("小美", 30),
        Person("小帅", 29)
    )

	//使用group按年龄分组,返回结果是map
    println(people.groupBy{it.age})
	//输出结果: {22=[Person(name=小明, age=22), Person(name=小红, age=22)], 29=[Person(name=小花, age=29), Person(name=小帅, age=29)], 30=[Person(name=小美, age=30)]}

每一个分组都存储在一个列表中,这里结果类型实质是Map<Int,List<Person>>mapKeysmapValues 函数也能作用于它。

再来看个例子,按首字母区分

 	val list = listOf("a", "ab", "abc", "b")
    //值得注意的是,这里的first不是String的成员,而是一个扩展(可成员引用)
    println(list.groupBy(String::first))//{a=[a, ab, abc], b=[b]}

flatMapflatten:处理嵌套集合元素

相信如果了解Rxjava的话,那么一定会对flatMap不会陌生。flatMap会根据作为实参给定的函数对集合中的每个元素做变换后,然后把多个列表合并成一个新的列表。

 	val list2 = listOf("ab", "cd", "ef")
    flatMap变换成新集合
    println(list2.flatMap { it.toList() })
    [a, b, c, d, e, f]

再举个例子,有一堆藏书,每本书可能有一个作者或者多个作者。

	//title表示书名,authors表示作者的集合
	class Book(val title: String, val authors: List<String>)


 	val books = listOf(Book("三国演义", listOf("罗贯中")),
        Book("水浒传", listOf("施耐庵","罗贯中")),
        Book("红楼梦", listOf("曹雪芹","程伟元","高鹗")))

    //flatMap变换合并,toSet移除重复元素
    println(books.flatMap { it.authors }.toSet())

这里需要注意的是,如果集合元素不需要变换,而只是想合并成一个新的集合的时候,不必使用 flatMap ,应该使用 flatten 函数。

惰性集合操作:序列

如果对java8的lambda熟悉,一定会知道stream流的存在。上面大部分的lambda函数会及早的创建中间集合(每一步中间结果都被存储在一个临时列表)。因此如果数据过多的话链式调用就会特别低效。而序列恰好能避免创建这些临时中间对象,从而解决这一问题。

  val people = listOf(
        Person("小明", 24),
        Person("小红", 22),
        Person("小花", 28),
        Person("小美", 30),
        Person("小帅", 29)
    )

  var list2 = people.asSequence()  		//把初始集合转成序列
        .sortedBy(Person::age)	   		//排序
        .map { it.name }		   		//序列支持和集合一样的api
        .filter { it.startsWith("小") }	//过滤
        .toList()     			       	//把结果序列转换未序列表

    println(list2)	//[小红, 小明, 小花, 小帅, 小美]

惰性集合操作入口就是Sequence接口,这个接口就是表示可以逐个列举的元素序列。Sequence只提供一个方法,iterator(用来从序列里获取值)。
Sequence接口强大在于操作实现的形式。序列中元素求值是惰性的。值得注意的是asSequence()是扩展函数。

执行序列操作:中间和末端操作

序列操作分为两类:中间的和末端。一次中间操作返回值是另一个序列(知道如何变换原始序列中的元素)。而一次末端操作返回的是一个结果,这个结果可能是集合,元素,数字或者其它从初始集合的变换序列中获取的任意对象。

	people.asSequence()         
       .map (Person::name)  //中间操作
       .filter { it.startsWith("j") } //中间操作
       .toList()         //末端操作

中间操作始终都是惰性的。

	//不会输出任何内容,lambdaa变换被延期,只有获取结果时才会被调用(末端操作)
	listOf(1,2,3,4).asSequence()
        .map { print("map($it) ");it*it }
        .filter { print("filter($it)");it%2==0 }

只要函数的的返回值不是 Sequence 整个函数就会被执行

加上末端操作

	listOf(1,2,3,4).asSequence()
        .map { print("map($it) ");it*it }
        .filter { print("filter($it)");it%2==0 }
        .toList() 			// 末端操作触发执行所有延期计算
    //map(1) filter(1)map(2) filter(4)map(3) filter(9)map(4) filter(16)

着重注意的是计算顺序,序列的操作是按顺序应用在每一个元素上:处理第一个元素后,再完成第二个元素,以此类推。
这也意味着有部分元素根本不会发生任何变换,举个map和find的例子。先把一个数字映射成它的平方,然后找到第一个比3大的条目。

 	 println(listOf(1, 2, 3, 4).asSequence()
    .map {
        println("执行了 $it" )
        it * it
    }
    .find { it > 3 })

执行结果

	执行了 1
	执行了 2
	4

可见满足条件后中间的函数就不再执行了,大大的提高了执行效率

如果同样的操作被应用在集合上,那么map结果会先被求出来,然后会把中间集合中满足的判断式的元素找出来。而对于序列来说,惰性方法意味着可以跳过处理部分元素。这也是及早求值(用集合)和惰性求值(用序列)的区别。

集合上执行操作的顺序是会影响性能的。再举个例子,用不同的操作顺序找出上述数据源person集合中长度小于某个限制的人名。

先map再filter

	val people = listOf(
	        Person("小明", 24),
	        Person("小红", 22),
	        Person("小花", 28),
	        Person("小美", 30),
	        Person("小帅", 29)
    )

	//先map再filter
  	println(people.asSequence()
        .map {
            print("map 执行了 $it  ,")
            it.name
        }
        .filter {
            println("filter 执行了 $it  ,")
            it=="小花"
        }
        .toList())

输出结果

map 执行了 Person(name=小明, age=24)  ,filter 执行了 小明  ,
map 执行了 Person(name=小红, age=22)  ,filter 执行了 小红  ,
map 执行了 Person(name=小花, age=28)  ,filter 执行了 小花  ,
map 执行了 Person(name=小美, age=30)  ,filter 执行了 小美  ,
map 执行了 Person(name=小帅, age=29)  ,filter 执行了 小帅  ,
[小花]

先filter再map

val people = listOf(
		        Person("小明", 24),
		        Person("小红", 22),
		        Person("小花", 28),
		        Person("小美", 30),
		        Person("小帅", 29)
	    )
//先filter再map
println(people.asSequence()
        .filter {
            println("filter 执行了 $it  ,")
            it.name == "小花"
        }
        .map {
            print("map 执行了 $it  ,")
            it.name
        }
        .toList())

结果当然是一样的,不同的是如果map在前,那么是每个元素都进行变换后在去过滤,而filter在前,则是先过滤在变换(被过滤掉的不会进行变换)。在实际中根据需求来执行,一定要时刻的记着自己的代码在干什么。

kotlin的序列实际上就是java8里stream(流的概念)的翻版,遗憾的是序列尚且未实现流的重要特性:并行执行流操作。

创建序列

  • asSequence()是用在集合上创建序列,而创建序列还有另一个函数generateSequence。给定此函数创建的序列,这个函数会计算出下一个元素。

整数举例

		//100以内所有自然数和
	    val naturalNumbers = generateSequence(0) { it+1 }
	    val naturalNumbersTo100 = naturalNumbers.takeWhile { it<=100 }
	    //sum触发延时操作计算结果
	    println(naturalNumbersTo100.sum()) //5050

字符串举例

	  val naturalNumbers = generateSequence("a") { "$it ${it.length}" }
	    val naturalNumbersTo100 = naturalNumbers.takeWhile {
	        println(it)
	        it.length <= 20
	    }
	    println("  ")
	    println(naturalNumbersTo100.toList())
  • 另一个常用的场景是父序列,如果元素的父元素和它的类型相同,我们可能会对它所有祖先组成的序列的特质感兴趣。举个查询文件是否在隐藏目录中的例子。

列举所有父文件

	fun File.listParentFile() = generateSequence(this) { it.parentFile }

    val file = File("C:\\Users\\m1362\\Documents\\Tencent Files\\1107472286\\FileRecv")
    println(file.listParentFile()
        .toList()
        .forEach {
            println(it)
        })

	//运行结果 : 
	C:\Users\m1362\Documents\Tencent Files\1107472286\FileRecv
	C:\Users\m1362\Documents\Tencent Files\1107472286
	C:\Users\m1362\Documents\Tencent Files
	C:\Users\m1362\Documents
	C:\Users\m1362
	C:\Users
	C:\

查找隐藏文件

	//File的扩展函数,generateSequence函数创建了文件的父序列
	fun File.isInsideHiddenDirectory()=
	        generateSequence(this){it.parentFile}.any{it.isHidden}
	
	 val file = File("/Users/svtk/.HiddenDir/a.txt")
    println(file.isInsideHiddenDirectory())//true

一样是通过提供第一个元素和获取每个后续的元素来提供实现。

使用Java函数式接口

kotlin的lambda能无缝和JavaAPI互操作。
举个例子,常见的在android中点击事件的监听(此处省略常规写法和java8的lambda写法,只举kotlin和javaAPI互操作的写法)。

	btn.setOnClickListener { view ->Log.e("cb","点击了") }

这种接口被称为函数式接口,又被称SAM接口,SAM代表单抽象方法。kotlin允许调用接收函数式接口作为参数方法使用lambda,来保证代码的整洁和简洁。

lambda当作参数传递Java
  • lambda可以传给任何期望函数式接口的方法。

      //Java
     	void postponeComputation(int dealay,Runnable computation);
    
  • 在kotlin中,可以调用它并把一个lambda作为实参传给它。编译器会自动把它转换成一个Runnable实例。

      //底层优化,没有外部变量的引用复用 runnable对象 ,有引用创建新的对象
      postponeComputation(1000){
          println(42)
      }
    

注意的是,“一个Runnable的实例"时,是指一个"一个实现了Runnable接口的匿名类的实例”。编译器会帮忙创建它。
也可以通过显示的创建实现Runnable的匿名对象达到同样效果。

	//把对象表达式作为函数式接口实现传递,强制每次调用创建新的对象
	Demo().postponeComputation(1000,object :Runnable{
        override fun run() {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
            println(42)
        	}
	})

不同的是,当显示声明对象时,每次调用都会创建一个新的实例。而使用lambda如果没有访问任何来自定义函数的变量,相应的匿名类实例可以在多次调用后重用(即只会创建一个Runnable的实例)。

  • 因此完全等价的实现应该显示object声明,在Runnable实例里存储变量,每次调用使用此变量。

      public class Demo {
    
          //Java
          public void postponeComputation(int dealay, Runnable computation) {
              computation.run();
          }
      
      }
    
    
      //全局变量,程序中只有一个实例
      val runnable = Runnable { println(42) }
    
      fun handleComputation(){
          //函数调用时候会作用同一个对象,强制使用同一个对象
           Demo().postponeComputation(1000, runnable)
      }
    

如果lambad从包围它的作用域捕捉到变量,那么就不可能每次都用通一个对象了,而是会使用新的实例,每次调用都会生成新的实例(否则就成了单例)。本质上这样的行为底层不是一个lambda,而是一个特殊类实例,类名是函数名称加上后缀衍生($调用次数)。

为lambda创建一个匿类及该类实例方式只对期望函数式的java方法有效。

SAM构造方法

SAM构造方法是编译器生成的函数,可以让执行从lambda到函数式接口实例的显式转换。可以在编译器不会自动应用转换的上下文中使用它。比如我们有一个方法返回的是一个函数式接口的实例,不能直接返回一个lambda,要用SAM构造方法把它包装起来。

	//使用SAM构造方法来返回值
    fun createAllDoneRunnable(): Runnable {
        return Runnable { println("all down") }
    }
    createAllDoneRunnable().run()  //all down

SAM构造方法的名称和底层函数式接口的名称一样。SAM构造方法只接收一个参数——一个被用作函数式接口单抽象方法体的lambda——并返回实现了这个接口类的一个实例。

除返回值以外,SAM构造方法还可以用在需要把lambda生成的函数式接口实例存储在一个变量中的情况。如在多个view上重用同一个监听器,这里仍以android点击监听做例。

	val listener = View.OnClickListener { view ->
        val text = when (view.id){
            R.id.btn -> "first button"
            R.id.btn1 ->"Second button"
            else ->"UnClick"
        }
     println(text)
    }

    btn.setOnClickListener(listener)
    btn1.setOnClickListener(listener)

需要注意的是,lambda 内部没有匿名对象那样的 this ,所以没办法引用到lambda转换成的匿名类实例。从编译器角度看,lambda 是一个代码块,不是一个对象,也不能当作一个对象的引用。lambda 中的 this 引用的是指向包围它的类。所以如果事件监听器在处理事件时还需要取消它自己,不能使用 lambda 这样做。这种情况使用实现了接口的匿名对象。在匿名对象内,this 关键字指向该对象实例,可以把它传给移除监听器的 API

带接收者的lambda:with与apply

kotlin 中的 lambda 独特功能在于:lambda 函数体内可以调用一个不同对象的方法,而且无需借助任何额外限定符。这样的 lambda 同时也被成为带接收者的 lambdakotlin 标准库中的 with 函数和 apply 函数就是带接收者的 lambda,它们用途广泛而又方便。

with
  • with函数用来对同一个对象执行多次操作,而不需要反复把对象名称写出来。举个例子,先不用with去构建字母表。

传统代码

	//不使用with函数来构建字母表
	fun alphabet(): String {
        val result = StringBuilder()
        for (latter in 'A'..'Z') {
            result.append(latter)
        }
        result.append("Ok!")
        return result.toString()
    }


    println(alphabet()) //ABCDEFGHIJKLMNOPQRSTUVWXYZOk!
  • 使用with重构。

      //使用with函数来构建字母表
      fun alphabet(): String {
          val result = StringBuilder()
    
          return with(result) {
      		//指定接收者的值,会调用它的方法
              for (latter in 'A'..'Z') {
                  this.append(latter)  //通过显示的"this"来调用接收者方法
              }
              append("Ok!")  //也可以省略this
              toString()  //从lambda返回值
          }
      }
    
    
      println(alphabet()) //ABCDEFGHIJKLMNOPQRSTUVWXYZOk!
    

with函数把它 的第一个参数转换成作为第二个参数传给它的lambda的接收者,既可以显示通过 this 引用访问也可以省略(不使用任何限定符)。

值得一提的是,lambda 是一种类似普通函数的定义方式,而带接收者的 lambda 是类似扩展函数定义的方式。

  • 进一步重构成终极版,使用with和一个表达式函数体。

      fun alphabet() =
          with(StringBuilder()) {
              for (latter in 'A'..'Z') {
                  append(latter)
              }
              append("Ok!")
              toString()
          }
    
    
    
      println(alphabet()) //ABCDEFGHIJKLMNOPQRSTUVWXYZOk!
    
apply

with 返回值是执行 lambda 的代码结果,该结果是 lambda 最后一个表达式。但是如果想返回的是接收者对象而不是执行 lambda 的结果,就该使用 apply 了。

apply 函数几乎和 with 函数一样,唯一区别就是 apply 会始终返回作为实参传递给它的对象(接收者对象),依旧拿上面的 with 函数例子举例 apply 的用法。

	fun alphabet() =
        StringBuilder().apply() {
            for (latter in 'A'..'Z') {
                append(latter)
            }
            append("Ok!")
        }.toString()



    println(alphabet()) //ABCDEFGHIJKLMNOPQRSTUVWXYZOk!

apply 被声明成扩展函数。它的接收者变成了作为实参的 lambda 的接收者。执行 apply 的结果就是 StringBuilder
许多情况下 apply 都极其有效,其中一种就是在创建一个对象实例并需要用正确的方式初始化它的一些属性的时候。在 kotlin 中,可以在任意对象上使用 apply

	//使用apply创建Textview
    fun createrTextView(contetx:Context){
        TextView(contetx).apply { 
            text="this is apply create"
            textSize= 20.0F
            setPadding(10,0,0,0)       
        }
    }

函数的返回值是 TextView

with 函数和 apply 函数是最基本最通用的使用带接收者的 lambda 例子。更多具体例的函数也可以使用这种模式。比如标准库函数 buildString

	//使用buildString创建字母表
	fun alphabet() = buildString { 
	    for (letter in 'A'..'Z'){
	        append(letter)
	    }
	    append("Ok!")
	}

buildString 函数优雅的完成了借助 StringBuilder 创建 String 的任务。我们可以使用带接收者的 lambda 构建 DSL,以及如何定义自己的函数来调用接收者 lambda。后续见分晓。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值