文章目录第 1 章 从嵌套判空到表达式链为什么需要作用域函数原理补充踩坑动手练第 2 章 五函数一张表it、this 与返回值决定选型踩坑动手练第 3 章 let空安全映射与局部作用域原理补充踩坑动手练第 4 章 apply 与 lazy对象配置与延迟初始化踩坑动手练第 5 章 also、run、with 与 runCatching 组合链踩坑动手练第 6 章 inline 边界与反模式何时不该再套一层动手练面试速查 · 追问链追问链 #1let 与 apply 怎么选 追问链 #2also 和 let 都能用 it为什么两个 追问链 #3五个函数哪个不是扩展函数 ⭐追问链 #4inline 作用域函数有什么代价 ⭐追问链 #5嵌套 let 有什么问题 完整链路一句通相关推荐第 1 章 从嵌套判空到表达式链为什么需要作用域函数读完空安全篇后你已经会用?.和?:处理「可能没有」的值。但在 Android 工程里三类写法仍会拖慢阅读和 Code Review守卫逻辑与业务缠在一起多层if (x ! null)把映射、校验、副作用挤在同一段。配置样板OkHttpClient.Builder()、ImageRequest.Builder()等多行 setter只为初始化一个局部对象。临时变量污染外层val name dto.name; if (name ! null) log(name)把一次性中间量留在作用域里。作用域函数scope functions不是「少写几行」的语法糖而是用返回值语义表达意图映射出新类型、配置原对象、打日志后继续传递、对已有引用批量操作。五个函数都是高阶函数其中四个是扩展函数with是顶层普通函数——这一点面试常考。原理补充标准库签名简化把设计拆成两个维度// 返回 lambda 结果 —— 映射 / 计算inlinefunT,RT.let(block:(T)-R):Rblock(this)inlinefunT,RT.run(block:T.()-R):Rblock()// 返回接收者本身 —— 配置 / 副作用后继续传inlinefunTT.apply(block:T.()-Unit):T{block();returnthis}inlinefunTT.also(block:(T)-Unit):T{block(this);returnthis}// 非扩展对已有引用批量操作inlinefunT,Rwith(receiver:T,block:T.()-R):Rblock(receiver)inline在调用点展开通常避免为单次 lambda 再分配 function 对象失效条件见第 6 章。踩坑五个函数「随机套」未先问「要不要新类型」「要不要原对象」。在let里堆副作用却不用also在apply里做映射却指望得到User而不是UserDto。动手练A用一句话写出let与apply在返回值上的根本区别。D在所在工程搜索?.let找 2 处并判断若改成if (x ! null)会多多少行、可读性如何。第 2 章 五函数一张表it、this 与返回值决定选型背「let 用 it、apply 用 this」不够——返回值类型决定能否接入 Repository 映射链。apply返回UserDtolet返回User混用是工程最高频 bug。函数对象引用返回值典型意图letitlambda 结果空安全链、映射、局部作用域runthislambda 结果在对象上下文中配置并算出一个值withthislambda 结果非扩展对已有引用批量操作applythis接收者本身Builder / 初始化链式配置alsoit接收者本身副作用、调试、放入集合选型决策按顺序问三个问题1. 左侧可能为 null且 null 应整条跳过 → 先 ?. 再 let / also / apply / run 2. 需要「新类型」作为链的结果 → let / run / with返回 lambda 最后一行 需要「原对象」继续往下传 → apply / also 3. 接收者已存在、只是批量读写字段 → with(receiver) { ... } 或 receiver.run { ... }记忆锚点要「新类型」→ let / run / with 要「原对象」→ apply / alsorunvslet都返回 lambda 结果run用this适合「在对象上下文中算出一个值」。alsovslet都用italso返回原对象let返回映射结果。踩坑with不是扩展函数user.with { }非法应with(user) { }。apply链末尾.build()才得到最终类型——Builder().apply { ... }返回的是 Builder不是OkHttpClient。动手练ARepository需要UserDto? → User?应选let还是apply写出 3 行骨架。B起点下面代码类型错误指出返回值并改正funmapUser(dto:UserDto?):User?dto?.apply{mapper.toDomain(this)}// 期望 User?实际得到什么第 3 章 let空安全映射与局部作用域let是 Android 工程里出现频率最高的作用域函数左侧为 null 时整段跳过非空时 lambda 参数it已是非空类型适合 DTO→Domain、解析导航参数、收窄平台类型。典型 Repository 映射interfaceUserMapper{funtoDomain(dto:UserDto):User}classUserRepository(privatevalapi:UserApi,privatevalmapper:UserMapper,){suspendfunloadUser(id:String):User?api.fetch(id)?.let{dto-mapper.toDomain(dto)}}带错误/默认分支时在let外接?:suspendfunloadUserOrThrow(id:String):Userapi.fetch(id)?.let(mapper::toDomain)?:throwIllegalStateException(user$idnot found)ViewModel 从SavedStateHandle解析可选枚举是「可空边界 安全解析 失败降级」的中级模式private val category: Category? savedStateHandle.getString(category)?.let { name - runCatching { Category.valueOf(name) }.getOrNull() }链路解读getString返回String??.let有值才进入runCatching包住非法枚举名.getOrNull()降级为null而非崩溃。这比!!或裸valueOf更符合生产习惯。UI 层收集StateFlow时可空字段只在非空时绑定state.province?.let { bindProvince(it) }bindProvince参数为Province非空避免在函数体内反复处理province?。原理补充?.let编译为左侧 null 则结果为 null否则调用let。lambda 内对it的智能转换由let的泛型签名保证不依赖外层var的可变性。踩坑三层以上let嵌套每层缩进 同名it阴影Review 时难以一眼看出数据流。在let里写return非 inline 的 lambda 中是局部 return标准库let是inlinereturn会从外层函数返回非局部返回极易误用。动手练A把if (dto ! null) mapper.toDomain(dto) else null改写成单表达式mapUser(dto: UserDto?): User?。C在let链外再接?: User.guest()写出loadUser的完整签名与实现5 行以内。第 4 章 apply 与 lazy对象配置与延迟初始化apply返回接收者本身适合「先配置、再当同类型继续用」Builder、Fragment事务参数、测试夹具初始化。不要用它做 DTO→Domain 映射上一章已说明。网络层常把超时、拦截器写在 Builder 上。配置应来自命名常量或注入而非魔法数字objectNetworkDefaults{constvalCONNECT_TIMEOUT_SEC10constvalREAD_TIMEOUT_SEC10}privatevalokHttpClient:OkHttpClientbylazy{OkHttpClient.Builder().apply{connectTimeout(NetworkDefaults.CONNECT_TIMEOUT_SEC,TimeUnit.SECONDS)readTimeout(NetworkDefaults.READ_TIMEOUT_SEC,TimeUnit.SECONDS)}.build()// apply 返回 Builderbuild() 才得到 OkHttpClient}by lazy保证首次访问时才创建重对象apply块内用this指向 Builder与let的it不同写多个 setter 更顺手。工程里也有在lazy内直接build()、未用apply的写法——功能等价但当配置项超过 3 个时apply通常更清晰private val retrofit: Retrofit by lazy { val client OkHttpClient.Builder() .connectTimeout(6, TimeUnit.SECONDS) .readTimeout(6, TimeUnit.SECONDS) .build() Retrofit.Builder() .baseUrl(https://shuaici.github.io/) .client(client) .addConverterFactory(ScalarsConverterFactory.create()) .build() }可读性迭代方向把OkHttpClient.Builder()段抽成apply块超时秒数迁入NetworkDefaults。踩坑忘记链末尾的.build()/.create()把 Builder 当成最终产品往下传。在apply块里returnapply只退出 lambda仍返回接收者——与let里裸return的外层逃逸不同需分清标签。动手练A用ImageRequest.Builder(context).apply { ... }.build()写 4 行骨架data、crossfade任选。B起点重构下面误用apply的映射与第 2 章练习呼应funtoDomainList(dtos:ListUserDto?):ListUserdtos?.apply{map(mapper::toDomain)}?:emptyList()第 5 章 also、run、with 与 runCatching 组合链also返回原对象适合「动一下再继续传」打日志、埋点、写入缓存、在集合构建中间插一步。also与let都用it但语义是副作用而非映射。读文件时把赋值与条件写进一行循环是also的典型技巧while (reader.readLine().also { line it } ! null) { builder.append(line).append(\n)run适合「以某对象为上下文算出一个结果」。非空断言后的局部计算可写成fundisplayName(user:User?):Stringuser?.run{$name($id)}?:访客with对已有非空引用批量操作避免重复写变量名funbindOrder(binding:ItemOrderBinding,order:Order)with(binding){titleText.textorder.title priceText.textorder.formattedPrice statusChip.textorder.status.label}与binding.run { }等价选型看团队习惯with更显式表达「以谁为接收者」。runCatching let组成完整 Repository 链捕获网络/解析异常在mapCatching内用let做空安全映射最后用getOrElse/recover决定降级策略。classUserRepository(privatevalapi:UserApi,privatevalmapper:UserMapper,privatevallogger:Logger,){suspendfunloadUser(id:String):UserrunCatching{api.fetch(id)}.mapCatching{dto-dto?.let(mapper::toDomain)?:throwIllegalStateException(empty body for$id)}.getOrElse{e-logger.warn(loadUser failed:$id,e)User.placeholder(id)// 或 rethrow按产品策略选三分支之一}}三分支回顾throw快速失败、Result.failure调用方fold、getOrElse默认值用户可见降级。踩坑also里改接收者内部可变状态却期望不可变链——副作用应集中、可测。runCatching吞掉所有Exception却不记录日志线上只剩「空白 UI」。动手练C在上面的loadUser上把getOrElse改成recover { User.placeholder(id) }说明与getOrElse的调用时机差异recover仍留在Result链上。B起点重构三层let嵌套坏代码funshowIfReady(navCategory:String?,filter:String?,repo:ItemRepository,){navCategory?.let{cat-filter?.let{f-repo.find(cat,f)?.let{item-ui.show(item)}}}}验收嵌套 ≤1 层或提取为私有函数 早返回。第 6 章 inline 边界与反模式何时不该再套一层标准库作用域函数全是inline fun调用点展开 lambda多数场景无额外分配。但有两类失效/代价需要知道Lambda 被储存或传给非 inline 函数体积极大的 lambda 仍可能生成独立类日常let几行配置无需担心数十 KB 级巨型 lambda 才需警惕 DEX 体积。非局部 returninline的let里裸return会从外层函数返回。若要把 lambda 传给另一个非 inline 高阶函数需crossinline禁止非局部 return若要把 lambda存字段或延迟执行参数须标noinline详细在 inline/reified 篇展开。反模式清单反模式问题改法dto?.apply { mapper.toDomain(this) }返回 DTO 类型改let三层?.let嵌套数据流不可读早返回、run、提取函数let里只有副作用语义错位改also处处with包单字段比直接访问更噪仅多字段批量时用动手练A写出crossinline一句用途为何 lambda 内不能return外层。D 验收清单自选仓库找 1 段if-null嵌套 ≥6 行改为?.let或?: return检查嵌套层数 ≤2、返回值类型与决策表一致。面试速查 · 追问链追问链 #1let 与 apply 怎么选 标准回答≤200 字看返回值。let用it返回 lambda 结果适合空安全映射和局部作用域apply用this返回接收者本身适合 Builder 配置。Repository 的UserDto? → User?必须用let把apply当映射是类型错误。追问 1let里写return会怎样答标准库let是inline裸return非局部返回直接从外层函数退出若 lambda 传给非 inline 函数则只是局部 return。团队规范常禁止在let内return。追问 2apply链末尾为什么要.build()答apply返回 Builder 自身不是产品类型必须再调build()/create()得到OkHttpClient等最终对象。追问链 #2also 和 let 都能用 it为什么两个 标准回答≤200 字引用方式相同返回值不同。let要映射结果also要原对象继续传递并插入副作用日志、缓存、集合 add。选错会导致链式类型断裂或把副作用藏在「映射」语义里。追问 1调试打印用also还是println在let里答打印后继续传原对象用also { log(it); it }或直接also { log(it) }若打印后还要变换类型用let。追问 2also和apply都返回接收者差别答apply用this配置内部状态also用it强调「旁路操作」一般不负责初始化整对象。追问链 #3五个函数哪个不是扩展函数 ⭐标准回答≤200 字with是顶层函数签名为with(receiver) { }不是T.with。其余四个是T的扩展。历史原因 API 风格with强调「把已有引用拉进块」早期 Kotlin 即如此设计。追问 1with(x) { }和x.run { }区别答非空x时几乎等价run是扩展可接?.runwith需先保证非空或包在let里。追问 2什么时候更倾向run而不是let答块内多次访问接收者字段、用this更短时选run单次映射it更清晰的用let。追问链 #4inline 作用域函数有什么代价 ⭐标准回答≤200 字inline在调用点复制字节码通常省分配lambda 被存储、跨非 inline 边界、或体积极大时可能增加 DEX 体积。crossinline/noinline用于控制 return 与参数传递。日常 Repositorylet不是性能瓶颈。追问 1为什么说「作用域函数不是零成本」仍可以放心用答相对网络与布局展开几行 lambda 成本可忽略真正要优化的是少嵌套、选对函数减少错误重试。追问 2runCatching和try/catch在 Repository 里怎么配let答runCatching得Result适合链式mapCatching/recoverlet处理可空 DTO。异常在Result层统一不在每个let里 try。追问链 #5嵌套 let 有什么问题 标准回答≤200 字每层缩进掩盖数据依赖Review 难追踪哪一步为 null且易与外层it阴影混淆。应用早返回、合并条件、提取private fun或run单块表达。追问 1团队 CR 规范可怎么写答例如「?.let嵌套不超过 2 层」「映射只用let、副作用只用also」。追问 2takeIf/takeUnless能替代部分let吗答条件过滤用takeIf更语义化需要映射类型仍用let。完整链路一句通先问返回值要新类型还是原对象→?.let做空安全映射→apply/lazy配 Builder→also插副作用→runCatching链上mapCatchinglet处理可空 DTO→控制嵌套与 inline return 边界。相关推荐Android 高级工程师面试Kotlin 语法基础 近1年高频追问 22 题Android 高级工程师面试Kotlin 协程 近1年高频追问 22 题