kotlinAndroid程序员

kotlin之Lambda编程

2018-01-05  本文已影响177人  程自舟

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) {
     
}

假设需求为找到列表年龄最大的人,不用lambda实现。

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能精简代码量

 //数据源
    val people = listOf(Person("jack", 29),
            Person("nick", 23),
            Person("jone", 26))
   //使用lambda在集合中搜缩
    //people.maxBy { it.age }比较年龄找到最大
    println(people.maxBy { it.age }) //Person(name=jack, age=29)

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

//用成员引用搜索
    println(people.maxBy(Person::age)) //Person(name=jack, age=29)

lambda表达式语法

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

    //变量存储lambda
    val sum ={x:Int,y:Int -> x+y}
    println(sum(1,2))//3

也可以无意义的直接调用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("jack", 29),
            Person("nick", 23),
            Person("jone", 26))

    //把lambda作为命名实参
    val names =people.joinToString(separator = " ",transform = {p:Person -> p.name})

    println(names)//jack nick jone

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

  //把lambda作为命名实参(虽然简洁,但未显示表明lambda在哪里运用)
    val names =people.joinToString(separator = " "){P:Person -> P.name}

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

  //显示的写出参数推导类型
    people.maxBy { P:Person -> P.age }

    //推导出参数类型
    people.maxBy { p ->p.age }

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

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

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

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

  //变量存储lambda,必须显示指定参数类型
    val getAge = { p: Person -> p.age }
    people.maxBy(getAge)
    println(people.maxBy(getAge))//Person(name=jack, age=29)

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("4")){
            //在lambda中修改变量
            clientErrors++
        }else if (it.startsWith("5")){
            serverErrors++
        }

    }
    println("$clientErrors for client,$serverErrors for server")
}

printProblemCounts(errors) //2 for client,0 for server

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

//模拟捕捉可变变量类(捕捉的实现原理)
class Ref<T>(var value: T) 

val counter = Ref(0)
//并不是变量被捕捉,但是存储字段值实际值是可以修改的
val inc ={counter.value++} 

//实际代码隐藏包装器
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一样,如果把函数转换成一个值,就可以直接转换它使用::运算符转换。

  //成员引用
    val getAge = Person::age
//等价
   val getAge = {person:Person ->person.age}

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

不管引用的是函数还是属性,都不应该在成员引用的名称后面()。成员引用和调用该函数的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(val name: String, val age: Int) 

    //创建Person实例被成员引用保持成了值
    val createPerson = ::Person
    val p = createPerson("jojo", 23)
    println(p)//Person(name=jojo, age=23)

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

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

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

集合式函数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("jack", 29),
            Person("nick", 23),
            Person("jone", 26))

    //过滤掉年龄小于25的,保留年龄大于25的
    println(people.filter { it.age>25 })
    //[Person(name=jack, age=29), Person(name=jone, age=26)]

map对集合每一个元素应用给定的函数并把结果收集到一个新集合,即元素变换。

  val list = listOf(1,2,3,4,5,6)
    //map把元素换为它的平方集合
    println(list.map { it*it }) //[1, 4, 9, 16, 25, 36]


    //使用map将元素为Person类型转换为String
       println(people.map { it.name }) //[jack, nick, jone]
    //可以使用成员引用简写
    println(people.map(Person::name))   //[jack, nick, jone]

lamdba当然可以链式调用。

  //数据源
    val people = listOf(Person("jack", 29),
            Person("nick", 23),
            Person("jone", 26))

    //找出年龄小于25岁的姓名
  val selectName= people.filter { it.age<25 }.map(Person::name)
    println(selectName)//[nick]

使用filter找出年龄最大的人

    //找出年龄最大的人(此代码有不足)
    people.filter{it.age==people.maxBy(Person::age)?.age}

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

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

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

也能对map进行过滤和变换。
filterKeys(过滤map的键),mapKeys(变换map的键),filterValues(过滤map的值),mapValues(变换对应的map值)

   val numbers = mapOf(0 to "ZERO",1 to "ONE")
    //移除map里小于1的key
    println(numbers.filterKeys { it<1 })//{0=ZERO}
    //把key变换为value
    println(numbers.mapKeys {it.value})//{ZERO=ZERO, ONE=ONE}
    //过滤map的value
    println(numbers.filterValues { it.startsWith("Z") })//{0=ZERO}
    //转换map的value为小写
    println(numbers.mapValues { it.value.toLowerCase() })//{0=zero, 1=one}

all,any,count,find:对集合的判断应用

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

    //数据源
    val people = listOf(Person("jack", 29),
            Person("nick", 23),
            Person("jone", 26))

    //年龄是否满足23
    val isAge23={p:Person->p.age<=23}

    //检查集合看是否所有元素满足(all)
    println(people.all(isAge23)) //false

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

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

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

值得一提的是!all(不是所有)加上某个条件,应该用any取反

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

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

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

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

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

    //数据源
    val people = listOf(Person("jack", 29),
            Person("nick", 23),
            Person("jone", 23))

    //使用group按年龄分组,返回结果是map
    println(people.groupBy{it.age})
   // {29=[Person(name=jack, age=29)], 23=[Person(name=nick, age=23), Person(name=jone, age=23)]}

每一个分组都存储在一个列表中,这里结果类型实质是Map<Int,List<Person>>,mapKeys或mapValues函数也能作用于它。
再来看个例子,按首字母区分

    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 list = listOf("ab","cd","ef")
    //flatMap变换成新集合
    println(list.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函数会及早的创建中间集合(每一步中间结果都被存储在一个临时列表)。因此如果数据过多的话链式调用就会特别低效。而序列恰好能避免创建这些临时中间对象,从而解决这一问题。

   people.asSequence()         //把初始集合转成序列
           .map (Person::name)  //序列支持和集合一样的api
           .filter { it.startsWith("j") }
           .toList()            //把结果序列转换未序列表

惰性集合操作入口就是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 }

加上末端操作

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 { it*it }.find { it>3 })

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

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

  //数据源
    val people = listOf(Person("jk", 29),
            Person("nec", 23),
            Person("jojo", 23))
    //先map再filter
    println(people.asSequence().map (Person::name ).filter { it.length>2 }.toList())//[nec, jojo]

    //先filter再map
    println(people.asSequence().filter { it.name.length>2 }.map(Person::name).toList())//[nec, jojo]

结果当然是一样的,不同的是如果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

另一个常用的场景是父序列,如果元素的父元素和它的类型相同,我们可能会对它所有祖先组成的序列的特质感兴趣。举个查询文件是否在隐藏目录中的例子。

//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实例。

  postponeComputation(1000){
        println(42)
    }

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

/把对象表达式作为函数式接口实现传递
    postponeComputation(1000, object :Runnable{ /
        override fun run() {
            println(42)
        }
        
    })

不同的是,当显示声明对象时,每次调用都会创建一个新的实例。而使用lambdar如果没有访问任何来自定义它函数的变量,相应的匿名类实例可以在多次调用后重用(即只会创建一个Runnable的实例)。
因此完全等价的实现应该显示obeject声明,在Rumnnable实例里存储变量,每次调用使用此变量。

    //全局变量,程序中只有一个实例
    val runnable = Runnable { println(42) }

    fun handleComputation(){
        //函数调用时候会作用同一个对象
        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同时也被成为带接收者的lambda。kotlin标准库中的with函数和apply函数就是带接收者的lambda,它们用途广泛而又方便。

with

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

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

}
  println(alphabet())//ABCDEFGHIJKLMNOPQRSTUVWXYZOk!

使用with重构。

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

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

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

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

fun alphabet() = with(StringBuilder()) {

    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("Ok!") 
    toString() 
apply

with返回值是执行lambda的代码结果,该结果是lambda最后一个表达式。但是如果想返回的是接收者对象而不是执行lambda的结果,就该使用apply了。
apply函数几乎和with函数一样,唯一区别就是apply会始终返回作为实参传递给它的对象(接收者对象),依旧拿上面的with函数例子举例apply的用法。

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

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

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

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

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

上一篇下一篇

猜你喜欢

热点阅读