
说句实在话我最初认识Kotlin的时候最不适应的一点就是if竟然可以当值用。写了多年Java条件判断在我脑子里一直是“语句”执行完就结束了不能像函数一样返回一个结果。Kotlin直接把这个习惯打碎if变成了表达式能赋给变量能当返回值甚至能直接作为函数体。这篇Kotlin系列第三篇想沿着“控制流与函数”这条进化线从if表达式一路聊到Lambda。适合刚接触Kotlin的人也适合想搞清楚语言设计意图的Android开发者——核心就一句话当控制流都变成了表达式函数才真正走向一等公民Lambda的出现也就成了水到渠成的事。1. 控制流的进化起点从if语句到if表达式1.1 为什么“有返回值”是质变Kotlin里if是表达式意味着它一定有返回值。这一点和Java完全不同。Java的if是纯语句你想在分支里产生一个结果只能先声明一个变量然后在两个分支里分别赋值最后再用这个变量。代码长了以后变量声明和赋值离得很远可读性全靠自觉。Kotlin的做法很直接val max if (a b) a else b这行代码里不需要临时变量不需要提前声明if本身就把结果算出来了。Kotlin也因此不需要三元运算符因为if表达式已经能覆盖“取二选一结果”这个场景并且比a b ? a : b更容易阅读。如果你嵌套多个分支这种优势更明显val level when { score 90 - A score 80 - B score 70 - C else - D }没有大括号堆叠没有多个临时变量表达式的意图完整保留在一行一值里。我第一次在项目里把这种写法替换掉原来的if-else链时最大的感受是代码里“为了赋值而写的中间变量”少了很多读起来更像在看一个公式而不是在看一段流程。还有个容易被忽略的点if作为表达式时每个分支的返回类型必须兼容。如果分支返回Int和String编译器会直接报错因为Kotlin的类型系统要求表达式在所有路径上产生一致类型。这个约束听起来是限制实际上帮了大忙——你永远不用担心某个分支忘了返回值编译器在编译期就把这种错误堵死了。1.2 表达式风格如何影响后续的函数设计if表达式的设计思路不是孤例。你会发现Kotlin在系统性做一件事让越来越多的语言构件变成表达式都能产生值。when是表达式try-catch是表达式函数体可以是表达式Lambda本身就是表达式。这也是很多Kotlin文档里说的“everything has a value”。这个理念带来的直接收益是代码开始从“步骤驱动”转向“结果驱动”。在Java里你写的是“先做什么、再做什么、最后得到什么”在Kotlin里你写的是“直接声明这个变量等于什么”。两种写法在复杂逻辑上差别不大但在简单逻辑上差距很明显。举个例子Java里常见的做法String version; if (isNew) { version v2; } else { version v1; } return version;Kotlin里可以直接写成fun betterVersion(isNew: Boolean): String if (isNew) v2 else v1函数体直接就是一个if表达式省掉了所有流程控制外衣。这种风格再往前走一步就是函数本身也可以成为“值”。当你习惯了“把if当值看”理解“把函数当值看”就变得很自然了。Lambda本质上就是函数作为表达式的体现——函数不再需要名字不再需要class去包裹直接写在需要的地方像写一个数字或字符串字面量一样。所以我在系列标题里用“进化之路”这个词就是想强调这条语言设计主线控制流先变成值然后函数变成值最后函数作为值被极简语法调用。2. 分支控制的进级when表达式是怎么做到的2.1 when的多种判断形态Kotlin的when常被说成是switch的高级替身但实际上它的能力远超switch。switch只能做等值匹配when几乎可以做任何条件判断。我整理了一下常见的分支形态使用场景写法说明精确匹配when (x) { 1 - ... 2 - ... }等价于switch但不需要break多值合并when (x) { 1, 2 - ... }多个值走同一个逻辑区间判断when (x) { in 1..10 - ... }使用范围表达式集合成员when (x) { in setOf(1, 3, 5) - ... }判断是否在集合内类型判断when (x) { is String - ... is Int - ... }配合智能转换任意条件when { score 60 - ... else - ... }不带参数纯粹的条件分支分支内嵌逻辑when (x) { y 1 - ... }分支本身可以是一个表达式其中类型判断配合智能转换特别常用尤其是在处理多态或者解析对象时。比如一个网络请求的结果可能是成功数据、错误码或者异常你不需要手动instanceof和强转when直接把类型收窄做了fun handle(result: Result) when (result) { is Success - handleData(result.data) is ApiError - handleError(result.code, result.message) is NetworkError - retry() }在这个例子里result.data能直接访问是因为Kotlin在is Success分支里已经自动把result智能转换成了Success类型。这种体验在Java里想都不敢想——你得写一堆if (result instanceof Success)再手动强转。2.2 when与函数返回值结合的场景when最舒服的用法是直接作为表达式函数体。上一段提到的fun betterVersion已经展示了if作为函数体when是它的扩展版适合多分支返回场景。我之前在处理枚举转文案时写过这样的代码fun statusText(status: OrderStatus): String when (status) { OrderStatus.CREATED - 已创建 OrderStatus.PAID - 已支付 OrderStatus.SHIPPED - 已发货 OrderStatus.COMPLETED - 已完成 OrderStatus.CANCELLED - 已取消 }注意到这里没有else因为OrderStatus是枚举编译器能确认分支已经穷尽所有可能值所以不需要兜底。这也是when作为表达式的一个硬性要求如果编译器无法证明所有分支都被覆盖就必须写else。这个约束在高强度重构时非常好用——当你给枚举新增一个状态所有没处理新状态的when表达式都会编译报错而不是等到运行时才发现漏了分支。在Android开发中这种风格尤其常见。比如处理RecyclerView的item类型override fun getItemViewType(position: Int): Int when (list[position]) { is Banner - TYPE_BANNER is Article - TYPE_ARTICLE is Video - TYPE_VIDEO }如果没有else就编译无法通过你自然会被迫思考“还有没有其他类型”很多边界bug在编译期就被消灭了。从Java switch过来的人第一次写这个可能有点不适应总担心漏掉break或者忘记default但实际写几回就回不去了。我个人现在的习惯是凡是多分支返回场景第一选择就是when表达式而不是if-else链。3. 函数的声明进化从fun到表达式函数体3.1 fun关键字与默认参数、命名参数Kotlin的函数以fun开头这个设计既简洁又统一。函数是最基础的抽象单元但Kotlin在函数声明上做了很多降低调用成本的改进最直接的就是默认参数和命名参数。Java里如果你想要一个函数支持不同参数组合只能写重载方法。方法一多类就变得臃肿调用方还得去猜哪个重载匹配自己想要的参数。Kotlin直接在一个函数上做文章fun request( url: String, method: String GET, timeout: Int 3000, retry: Boolean false )调用方式变得非常自由request(https://example.com) request(https://example.com, method POST) request(https://example.com, timeout 10000, retry true)命名参数最大的价值是调用方可以只传自己关心的参数其他参数保持默认完全不需要写一堆只为了传少数参数的重载。而且命名参数本身自带注释效果——不用去看函数定义你也能知道timeout 10000是在设置超时时间。这里要说个实操建议默认参数别往函数前面堆。如果混合位置参数和命名参数阅读代码时很容易产生误解。比如request(url, method POST, 5000)这种写法就会让人停下来想5000到底是timeout还是retry虽然Kotlin语法上允许通过位置参数混用但真实项目里最好统一风格要么全命名要么前面位置参数、后面命名参数并且在Commit规范里约定好。3.2 可变参数与局部函数把控制作用域缩小Kotlin里声明可变参数用的是vararg比Java的String...稍微灵活一点fun sum(vararg numbers: Int): Int numbers.sum() fun main() { println(sum(1, 2, 3, 4, 5)) val arr intArrayOf(5, 6, 7) println(sum(*arr)) }*arr是Kotlin的数组展开运算符用于把数组内容展开成可变参数。这个语法的操作符设计得很直觉第一次看到就会理解它的含义。需要注意的是vararg在函数参数中最好放在最后虽然语法上允许放中间或前面但那样调用时代码会很混乱尤其是和命名参数混用的时候。我实际项目里更常使用的是局部函数。函数里面再定义函数这在Java里做不了Kotlin天然支持fun processUserNames(names: ListString): ListString { fun cleanName(name: String): String { val trimmed name.trim() return if (trimmed.isBlank()) unknown else trimmed.lowercase() } return names.map(::cleanName) }局部函数有什么好处它把复用范围严格限制在当前函数体内不会污染外部命名空间也不需要额外抽一个私有方法。当你只是想在当前函数内重复一段操作时局部函数比抽private方法更合适因为代码的阅读者不需要在类的其他位置查找这个函数的定义。但要提醒一句局部函数不要嵌套太多层。我见过有人在一个函数里嵌套三层局部函数最后逻辑本身没问题但阅读时需要在脑子里维护多层闭包的作用域成本很高。我的原则是最多一层局部函数再复杂就抽出来。4. Lambda的真面目从匿名函数到高阶函数4.1 Lambda语法拆解花括号、箭头、it与下划线Lambda本质上是匿名函数的简写核心语法只有两个符号花括号和箭头。用一句话解释Lambda它把一段逻辑打包成一个对象这个对象可以赋值、传参、返回到需要的时候再执行。看一个最简单的演变过程。Java里常见的事件监听button.setOnClickListener(new OnClickListener() { Override public void onClick(View v) { doSomething(); } });Kotlin里可以直接写成button.setOnClickListener { view - doSomething() }去掉所有样板代码后剩下的就是参数和函数体。这个例子里的view - doSomething()就是Lambda的完整形态左侧是参数右侧是函数体。当只有一个参数时Kotlin允许直接用it代替参数名button.setOnClickListener { it.text clicked }it是Kotlin的隐式参数名。这个特性很爽但有个反直觉的地方it不能用在大括号的任意位置只能在单参数Lambda中省略参数声明时使用。如果你写{ a, b - ... }就必须要显式写参数名。如果某个参数用不到可以用下划线_忽略map.forEach { (key, value) - println(key) // value 没有使用 }it虽然简洁但我建议在参数语义不明确的时候别滥用。比如处理一个集合里字符串过滤时it完全没问题但如果Lambda里逻辑超过几行it反而会让代码变隐晦。我在团队里定的规矩是Lambda函数体超过三行就必须给参数起名字。4.2 常用高阶函数实操let、apply、run、filter、map高阶函数是指参数类型或者返回类型是函数类型的函数。Kotlin集合API就是高阶函数最典型的应用场景。传统写法和Lambda写法的对比非常直观传统循环实现val adults mutableListOfPerson() for (p in users) { if (p.age 18) { adults.add(p) } }Lambda链式写法val adults users.filter { it.age 18 }再配合map、take等操作可以一口气完成多个步骤val topNames users.filter { it.age 18 } .map { it.name } .take(3)不写中间变量不写循环体每个操作看起来像一个数据管道的节点读起来非常顺畅。这里有一个性能细节要注意filter和map每次都会返回一个新集合链子长了会产生多个中间集合。数据量小无所谓数据量大时建议改成Sequenceval topNames users.asSequence() .filter { it.age 18 } .map { it.name } .take(3) .toList()Sequence的原理就是惰性求值类似Java里的Stream数据处理完之前不会为每一步创建中间集合。除了集合操作Kotlin标准库还有一组作用域函数let、apply、run、also它们本质上就是高阶函数。我选它们的经验很简单函数返回什么什么时候用letLambda返回值非空判断后转换对象或需要把结果赋给变量apply接收者本身初始化对象配置比如设置View属性runLambda返回值执行一段需要返回结果的逻辑also接收者本身执行副作用保留原始对象继续链式操作举一个Android初始化弹窗的经典例子val dialog AlertDialog.Builder(this).apply { setTitle(提示) setMessage(确认删除这条记录吗) setCancelable(false) }.create()apply返回的是接收者本身所以可以在一串配置之后直接create。如果用Java写你得先创建Builder再一个个调用set方法最后create过程中多出一个中间变量。Kotlin用apply把配置和创建压到了一行链式调用里。let则常用于可空类型。比如textView?.let { it.text result it.visibility View.VISIBLE }只有textView非空时才执行这段逻辑而且it已经被安全收窄为非空类型不会再触发空指针警告。这里是真的省心——Java里你要单独加一个判空Kotlin用let加安全调用就解决了。5. 常见问题与排查技巧5.1 Lambda的return与外层函数的诡异关系Kotlin的Lambda里直接写return默认不是从Lambda返回而是从外层函数返回。这个行为官方叫“非局部返回”。新手写代码时最容易在这里踩坑。fun findFirstValue(list: ListInt) { list.forEach { if (it 0) return // 这行会直接退出 findFirstValue println(processing $it) } println(done) }本来你可能只想跳过当前这个元素结果整个函数直接退出了。解决办法是使用隐式标签或显式标签list.forEach { if (it 0) returnforEach // 只跳过当前次循环 println(processing $it) }returnforEach这种写法返回值指向Lambda所属的函数名。还可以给Lambda自定义标签但实际项目中return函数名已经足够清晰不需要额外标签。如果你是Java转过来的要特别改变一下习惯Java里return永远作用在最近的函数上Kotlin里则是作用在最近的“非Lambda函数”上这个差异真的坑。5.2 闭包捕获与线程安全Lambda可以捕获作用域内的变量包括var可变变量这让Lambda用起来很方便但也埋了一个隐患。看这段代码var counter 0 val increment { counter }单线程环境没问题但如果在多线程里执行这个Lambdacounter的读写不是原子的可能丢失更新。Kotlin不像Java要求局部变量被匿名内部类捕获时必须是finalvar和val都能捕获所以编译器不会给你任何警告。实践中我的建议是Lambda捕获的变量尽量用val如果一定要用var并且涉及并发就用原子类型比如AtomicInteger或者干脆把状态封装到一个线程安全的容器里。还有一点容易被忽略Lambda捕获可变变量后变量的生命周期会被延长。如果那个变量属于一个大型对象Lambda被传出去之后没有及时释放可能引起内存泄漏尤其在Android组件销毁场景里。写回调时如果持有外部Activity实例务必使用弱引用或者及时解绑。5.3 内联函数与crossinline的两难Kotlin提供了inline关键字可以把Lambda在调用处展开避免创建Lambda对象减少运行时开销。这个优化很有价值但有个反效果如果Lambda函数体特别大内联会让字节码膨胀反而不利于性能。一般标准库里的函数都加了inline你自己写高阶函数时除非是高频调用点否则不建议滥用。inline和Lambda搭配时还会出现一个限制。非局部返回让Kotlin的高阶函数有两种返回机制inline函数里的Lambda可以直接return外层函数但如果Lambda被标记了crossinline就不允许非局部返回了。inline fun runWithLog(block: () - Unit) { println(before) block() println(after) }如果block里被另一个函数间接执行比如inline fun runAsync(crossinline block: () - Unit) { Runnable { block() } }不加crossinline会编译错误因为Lambda在Runnable内部执行已经不在调用方的控制栈里了非局部返回没有意义。这种编译报错信息一开始看很劝退但理解了“内联和跨作用域执行”的冲突后就明白编译器其实是在帮你排除危险代码。遇到这个编译错误时第一反应不是想办法绕过而是检查你的高阶函数设计是否合理。6. 我的一点实操经验聊了这么多最后说说个人在实际项目里的感受。Kotlin这套控制流与函数的设计最舒服的一点是它能让代码在保持可读的前提下把大量样板逻辑压缩掉。但“压缩”不等于“炫技”。我在项目里见过一些同学为了展示Lambda功底把代码写成好几层链式调用加十几个高阶函数可读性严重下降。代码最重要的是让人看懂而不是让编译器满意。我个人现在的习惯是控制流模块尽量用when表达式收敛分支函数模块能表达式函数体就表达式函数体集合操作优先用filter/map的链式写法但一旦链式逻辑超过三四步我会拆成带名字的中间变量或局部函数。Lambda和命名参数确实好用但它们是工具不是表演道具。这样写代码半年以后再回头看你会发现维护成本低很多尤其是换人接手时这种写法的优势会特别明显。