简介《Atomic Kotlin》英文版PDF面向希望系统入门Kotlin的开发者与Android学习者以“原子化”小章节拆分知识点便于按主题逐步推进。内容从Why Kotlin、Hello World、var与val、数据类型、函数、if表达式、字符串模板、数值与布尔、while与for循环、in关键字、表达式与语句讲起并延伸至对象、类、属性、构造器、可见性、包、测试、异常和列表等主题兼顾语法基础与面向对象思维。压缩包共1个PDF文件约2.79MB目录完整、层级清晰适合作为随查随学的英文教材。已有351人学习读者可借助短小章节和目录结构快速定位概念配合示例理解Kotlin的值语义、类型系统与常用控制流也为后续高阶函数、lambda与协程等进阶内容打基础。1. 从“原子”切进 Kotlin英文版电子书适合谁、能省掉哪段弯路如果你在搜 Atomic Kotlin pdf 下载 英文版多半不是为了收藏一份电子书而是想找一本能把 Kotlin 从语法、对象模型到函数式写法拆开讲透的英文参考书。这本英文版把语言拆成几十个“原子”小节每节围绕一个概念配可运行示例和练习按编程基础、对象、可用性、函数式、面向对象、防错、进阶工具七个部分推进。它适合刚转 Kotlin 的 Java 开发者、移动端或服务端需要补语言基础的人也适合想用英文材料建立术语体系的人不适合想直接抄框架项目的人。它解决的是“语法看得懂、写起来处处别扭”的问题把val/var、空安全、扩展函数、lambda、Sequence 这些点逐个压实。读之前最好能跑kotlinc否则示例只能停在纸面。2. 原子式学习路径怎么落地从 kotlinc 到 val/var、函数与 when 的可运行骨架这本英文书的最大特点是“小节短、示例小、练习密”。如果按传统方式从第一章顺读到附录很容易在val和var的区别上觉得“懂了”到集合操作又发现写不出来。我的做法是给每个原子小节建一个最小可运行文件先让编译器说话再回头核对英文术语。下面这套骨架适合在本地命令行跑通不依赖大型项目结构也方便后面把对象、空安全、lambda 的示例继续塞进来。2.1 先搭一个最小可运行环境kotlinc 与脚本式验证Kotlin 的源码文件可以单独编译成 jar也可以直接用kotlinc -script跑脚本片段。对这本以短示例为主的书先确认 JDK 和编译器可用比急着装插件更稳。常见做法是建一个空目录把每节示例按章节-主题.kt命名编译产物统一丢到build目录避免源文件和 class 文件混在一起。# 建一个专门跑书中示例的目录避免污染其他项目 mkdir -p atomic-kotlin-lab/src atomic-kotlin-lab/build cd atomic-kotlin-lab # Kotlin 编译器依赖 JVM 工具链先确认 java 和 kotlinc 都在 PATH 中 java -version kotlinc -version # 如果没有 kotlinc按系统包管理器安装即可这里只验证命令是否存在 command -v kotlinc这段命令的逻辑很直接java -version确认 JVM 可用kotlinc -version确认编译器版本command -v用来判断命令是否在 PATH 中。参数上没有特别需要调的地方唯一要注意的是kotlinc启动比javac慢短示例不必反复全量编译可以先用脚本模式验证语法再落到文件。目录名用src和build分开是后面排查“为什么类找不到”的第一道保险。// src/Hello.kt fun main() { val language Kotlin println(Hello, $language!) // 字符串模板直接展开变量 }# 编译时把 Kotlin 运行时一起打进 jar避免运行时缺类 kotlinc src/Hello.kt -include-runtime -d build/hello.jar # 运行生成的 jar java -jar build/hello.jar逻辑说明fun main()是入口函数val声明不可重新赋值的局部变量$language是字符串模板。参数说明-include-runtime会把 Kotlin 运行时打包进 jar适合单文件验证-d指定输出路径。如果去掉-include-runtime运行时就需要额外的 classpath初学阶段容易在“编译通过但运行报 NoClassDefFoundError”上翻车。这里不追求工程化只追求每个原子示例都能独立跑。2.2 用 val/var 和函数建立“值语义”直觉书中把var和val放在很靠前的位置不是为了讲两个关键字而是为了建立“值语义”和“不可变优先”的直觉。val只表示引用不可重新赋值不表示对象内部状态不可变var也不是洪水猛兽局部状态该用就用。英文书里会反复出现immutable、read-only、reassign这些词读的时候最好用代码把边界跑出来不然很容易把val当成常量。// src/Temperature.kt fun describeTemperature(celsius: Int): String { val fahrenheit celsius * 9 / 5 32 return when { celsius 0 - 冰点以下$celsius°C / $fahrenheit°F celsius in 1..25 - 舒适区间$celsius°C / $fahrenheit°F else - 偏热$celsius°C / $fahrenheit°F } } fun main() { var sample 18 println(describeTemperature(sample)) sample 30 println(describeTemperature(sample)) }逻辑说明describeTemperature接收Int参数返回String函数体内用val保存中间结果避免后续误改。when在这里是表达式每个分支直接返回值所以不需要return在每个分支里重复写。参数说明celsius是摄氏度整数1..25是闭区间。把sample声明为var是为了演示重新赋值如果实际业务里这个值只赋值一次就应该改成val。运行后能看到两条不同分支的输出这比只看英文解释更能记住when的表达式语义。// src/ValBoundary.kt class Counter(var value: Int) fun main() { val counter Counter(0) counter.value 10 // val 限制的是 counter 引用不是对象内部字段 println(counter.value) // counter Counter(1) // 这行会编译失败val 不能重新赋值 }逻辑说明Counter的主构造器里用var value所以counter.value可以改counter本身是val所以不能指向新对象。参数说明Counter(0)的0是初始值。这个示例专门用来纠正“val等于常量”的误读。英文书里出现read-only时指的就是这种引用只读不是深度不可变。把这个边界跑一遍后面读到data class的copy()、List的只读接口时才不会把概念混在一起。2.3 字符串模板与 when把表达式思维写进肌肉记忆Kotlin 的if、when、try都可以是表达式这本英文书在基础部分就反复强调expression和statement的差别。字符串模板也是同理它不是语法糖那么简单而是让“拼接字符串”从方法调用变成可读的模板。下面这个示例把when、字符串模板和函数默认参数放在一起接近书里“小步叠加”的节奏。// src/LogFormatter.kt enum class Level { INFO, WARN, ERROR } fun formatLog( level: Level, message: String, source: String app ): String { val prefix when (level) { Level.INFO - [INFO] Level.WARN - [WARN] Level.ERROR - [ERROR] } return $prefix [$source] $message } fun main() { println(formatLog(Level.INFO, 服务启动)) println(formatLog(Level.WARN, 重试次数接近上限, source scheduler)) println(formatLog(Level.ERROR, 任务失败)) }逻辑说明Level是枚举formatLog的第三个参数有默认值app调用时可以省略。when覆盖三个枚举分支编译器能检查穷尽性后续新增枚举值会提醒补分支。参数说明level决定前缀message是日志正文source用来标记来源模块。字符串模板$prefix [$source] $message比拼接更直观但要注意$后面跟复杂表达式时需要花括号例如${source.uppercase()}。这些细节在英文书里通常用一两句话带过真正记住要靠自己改参数跑一遍。提示每读完一个原子小节至少改一次参数或分支再运行。只读不改英文术语和 Kotlin 语法会停在“眼熟”层面。3. 对象与空安全用 data class、构造器和可空类型搭一个配置解析模块基础语法跑通后英文书会进入对象部分类、属性、构造器、可见性、包、测试、异常、集合。这一段的难点不是语法本身而是 Kotlin 把“可空性”放进了类型系统。很多从 Java 转过来的人会下意识用!!把可空类型转成非空短期能跑长期就是给运行时埋雷。更稳的做法是用一个配置解析小模块把data class、主构造器默认参数、可空字段、安全调用、Elvis 操作符和扩展函数串起来。这样读英文书时nullable receiver、safe call、Elvis operator这些词才有落点。3.1 用 data class 和默认参数表达配置结构配置对象通常字段不多但每个字段都有“可能缺失”或“有默认值”的情况。data class适合这种以承载数据为主的类型主构造器可以直接写默认参数copy()也方便做局部修改。英文书里讲data class时会提到equals()、hashCode()、toString()、copy()和componentN()这些不用死记写一个配置类跑一遍输出就能看到差别。// src/UserConfig.kt data class UserConfig( val name: String, val email: String?, val retryCount: Int 3, val tags: ListString emptyList() ) fun main() { val base UserConfig( name alpha, email alphasite, tags listOf(reader, kotlin) ) val changed base.copy(retryCount 5) println(base) println(changed) println(name 相同${base.name changed.name}) println(retryCount 不同${base.retryCount ! changed.retryCount}) }逻辑说明UserConfig的主构造器里email是String?表示可能为空retryCount和tags有默认值调用时可以省略。copy()生成新对象只改指定字段原对象不变。参数说明name是非空字符串email可空retryCount是整数默认 3tags是只读列表默认空列表。输出里能看到toString()的自动生成结果比较的是结构相等而不是引用相等。这个示例跑完再读英文书里data class一节componentN()和解构声明就不会陌生。// src/ConfigDestructure.kt data class DbConfig( val host: String, val port: Int, val user: String? null ) fun main() { val config DbConfig(local, 5432) val (host, port, user) config println(host$host, port$port, user${user ?: 未配置}) }逻辑说明val (host, port, user) config是解构声明按主构造器参数顺序取component1()、component2()、component3()。参数说明host和port必填user可空且有默认值。user ?: 未配置是 Elvis 操作符左侧为空时取右侧。这里不要用user!!否则默认配置下会直接抛异常。英文书里讲解构时通常会和data class一起出现但真正容易错的是参数顺序和可空字段的处理跑一遍输出最清楚。3.2 可空类型、安全调用与扩展函数把空值处理收进一处空安全是 Kotlin 类型系统的核心卖点也是英文书里最容易“读懂但写错”的部分。?.是安全调用左侧为空时整个表达式返回null?:是 Elvis左侧为空时给默认值!!是强制非空失败就抛NullPointerException。更稳的做法是把可空字段的处理收进扩展函数调用方只拿非空结果不把null判断散落到业务代码里。// src/EmailMask.kt data class UserConfig( val name: String, val email: String?, val retryCount: Int 3 ) fun UserConfig.maskedEmail(): String { val value email ?: return 未提供邮箱 val at value.indexOf() return if (at 0) { 格式可疑 } else { ${value.first()}***${value.substring(at)} } } fun main() { val configs listOf( UserConfig(alpha, alphasite), UserConfig(beta, null), UserConfig(gamma, broken-email, retryCount 5) ) configs.forEach { config - println(${config.name}: ${config.maskedEmail()}, retry${config.retryCount}) } }逻辑说明maskedEmail()是UserConfig的扩展函数内部先用 Elvis 处理email为空的情况提前return后面就能把value当非空字符串用。indexOf()返回位置at 0覆盖“没有 ”和“ 在开头”两种可疑格式。参数说明email可空retryCount默认 3substring(at)保留及后面的域名部分。这里没有用!!所以null配置不会崩。英文书里讲safe call和Elvis时篇幅不长真正要练的是“把空值挡在边界”的习惯。// src/SafeCallChain.kt data class Profile(val nickname: String?) data class Account(val profile: Profile?) fun displayName(account: Account?): String { return account?.profile?.nickname?.takeIf { it.isNotBlank() } ?: 匿名用户 } fun main() { println(displayName(Account(Profile(kappa)))) println(displayName(Account(null))) println(displayName(null)) }逻辑说明account?.profile?.nickname是安全调用链任意一环为空就返回nulltakeIf { it.isNotBlank() }在昵称为空白时也返回null最后用 Elvis 给默认值。参数说明Account?表示整个账户可空Profile?表示资料可空nickname可空。这个链式写法比多层if (account ! null)更紧凑但不要为了紧凑把业务判断全塞进一条链可读性优先。英文书里会强调let、run、apply等作用域函数初学阶段先把?.和?:用稳再考虑作用域函数。3.3 构造器与可见性别让“能编译”掩盖初始化顺序问题对象部分还会讲主构造器、次构造器、初始化块和可见性。Kotlin 的初始化顺序是按代码从上到下执行属性初始化器、init块、次构造器之间有固定顺序。英文书里通常用短示例说明但实际写代码时构造器里调用开放方法、在init块里依赖尚未初始化的属性都是常见翻车点。更稳的做法是让构造器只做赋值和简单校验复杂逻辑放到工厂函数或普通方法里。// src/ConstructorOrder.kt class Session( val user: String, rawTimeout: Int ) { val timeout: Int init { require(rawTimeout 0) { timeout 必须大于 0当前为 $rawTimeout } timeout rawTimeout.coerceAtMost(600) } override fun toString(): String Session(user$user, timeout$timeout) } fun main() { println(Session(alpha, 120)) println(Session(beta, 900)) try { Session(gamma, 0) } catch (e: IllegalArgumentException) { println(捕获${e.message}) } }逻辑说明rawTimeout不是val/var只是构造器参数供init块使用timeout在init块里赋值并用coerceAtMost(600)限制上限。参数说明user是只读属性rawTimeout是临时参数require在条件不满足时抛IllegalArgumentException。这里把校验放在init块避免构造出非法对象。英文书里讲check、require、assert时会区分使用场景初学阶段记住“外部输入用require内部状态用check”就够用。注意init块按出现顺序执行属性初始化器也在其中。不要在init块里调用可能被重写的开放方法否则子类初始化顺序会让结果变得难以排查。4. 函数式与集合操作lambda、高阶函数、Sequence 的惰性边界函数式部分是这本英文书的重头戏lambda、成员引用、高阶函数、集合操作、构建 Map、Sequence、局部函数、fold、递归。Kotlin 的集合操作写起来很顺但顺不等于对。filter、map、flatMap、fold的顺序一变结果和性能都可能变Sequence也不是所有场景都比List快。更稳的学习方式是拿一组订单数据把链式操作、终端操作、惰性求值分别跑一遍再看英文书里对eager和lazy的解释。4.1 lambda 与高阶函数把“函数作为参数”写成可读的报表高阶函数不是高级语法炫技它的价值在于把变化点抽成参数。比如报表生成筛选条件和格式化方式经常变把这两个点做成函数参数主流程就能复用。英文书里讲higher-order function时会用(A) - B这种类型写法初看抽象落到代码里就是“接收一个函数返回一个结果”。// src/Report.kt data class Order( val id: String, val amount: Int, val status: String ) fun buildReport( orders: ListOrder, predicate: (Order) - Boolean { true }, formatter: (Order) - String { ${it.id}:${it.amount} } ): ListString { return orders.filter(predicate).map(formatter) } fun main() { val orders listOf( Order(A-01, 120, PAID), Order(A-02, 80, PAID), Order(A-03, 300, REFUNDED), Order(A-04, 50, PAID) ) val paidLines buildReport( orders orders, predicate { it.status PAID }, formatter { ${it.id} - ${it.amount} } ) println(paidLines.joinToString(\n)) }逻辑说明buildReport接收订单列表、筛选函数和格式化函数默认值让调用方可以只传必要参数。predicate的类型是(Order) - Booleanformatter是(Order) - String。参数说明orders是输入集合predicate决定保留哪些订单formatter决定每行输出格式。it是单参数 lambda 的隐式名称。这个示例跑通后再把formatter换成成员引用例如Order::id就能理解英文书里member reference的用法。// src/MemberRef.kt data class Order(val id: String, val amount: Int, val status: String) fun main() { val orders listOf( Order(A-01, 120, PAID), Order(A-02, 80, PAID) ) val ids orders.map(Order::id) val amounts orders.map { it.amount } println(ids) println(amounts) }逻辑说明Order::id是属性引用等价于{ order - order.id }{ it.amount }是普通 lambda。参数说明map接收一个转换函数返回新列表不修改原列表。成员引用在可读性上更好但不是所有场景都适用带额外逻辑的转换还是写 lambda 更清楚。英文书里会把成员引用和构造函数引用放在一起讲构造引用常见写法是::Order但需要参数匹配初学阶段先掌握属性引用即可。4.2 集合链式操作filter、map、flatMap 的顺序会改变结果集合操作最容易被低估的是顺序。filter在前可以减少后续map的输入量map在前可能先生成大量中间结果flatMap会把每个元素展开成多个元素。英文书里会用小列表示意但真正记住要靠自己构造“顺序不同、结果不同”的例子。// src/CollectionChain.kt data class Order( val id: String, val amount: Int, val status: String, val tags: ListString ) fun main() { val orders listOf( Order(A-01, 120, PAID, listOf(vip, new)), Order(A-02, 80, PAID, listOf(new)), Order(A-03, 300, REFUNDED, listOf(vip)), Order(A-04, 50, PAID, emptyList()) ) val paidTags orders .filter { it.status PAID } .flatMap { it.tags } .distinct() val allTags orders .flatMap { it.tags } .distinct() println(已支付订单标签$paidTags) println(全部订单标签$allTags) }逻辑说明filter先保留已支付订单flatMap把每个订单的标签列表展开成一个标签流distinct去重。参数说明status是字符串状态tags是字符串列表flatMap的 lambda 必须返回可迭代对象。顺序上如果先flatMap再filter就需要在标签层面判断订单状态逻辑会变复杂。英文书里讲flatMap时会强调“转换后展开”这个示例的输出能直接看到展开和去重的效果。// src/FoldExample.kt data class Order(val id: String, val amount: Int, val status: String) fun main() { val orders listOf( Order(A-01, 120, PAID), Order(A-02, 80, PAID), Order(A-03, 300, REFUNDED), Order(A-04, 50, PAID) ) val paidTotal orders .filter { it.status PAID } .fold(0) { acc, order - acc order.amount } val grouped orders.groupBy { it.status } .mapValues { (_, value) - value.sumOf { it.amount } } println(已支付总额$paidTotal) println(按状态汇总$grouped) }逻辑说明fold从初始值0开始把每个元素累积到accgroupBy按状态分组mapValues对每组求和。参数说明fold的初始值类型决定累积器类型这里用IntsumOf接收一个返回数值的 lambda。groupBy返回MapString, ListOrdermapValues生成新的映射。英文书里讲fold时会和reduce对比reduce没有初始值空集合会抛异常初学阶段用fold更稳。4.3 Sequence 的惰性边界不是所有链式操作都该加 asSequenceSequence是这本英文书里很容易被误用的点。它把操作分成中间操作和终端操作中间操作返回新的 Sequence只有终端操作触发计算。数据量小、链式短时直接用List更直观数据量大、链式长、或者有提前终止条件时Sequence 才有优势。英文书里会讲lazy但不会替你做性能决策。// src/SequenceBoundary.kt data class Order(val id: String, val amount: Int, val status: String) fun main() { val orders (1..1_000_000).map { index - Order(A-$index, index % 500, if (index % 3 0) PAID else REFUNDED) } val firstLargePaid orders .asSequence() .filter { it.status PAID } .filter { it.amount 400 } .firstOrNull() println(firstLargePaid) }逻辑说明asSequence()把列表转成惰性序列两个filter是中间操作firstOrNull()是终端操作找到第一个满足条件的元素后就停止不会把一百万条全部处理完。参数说明1..1_000_000生成大列表仅用于演示实际项目里数据来源可能是数据库或文件firstOrNull()找不到时返回null。如果把asSequence()去掉filter会先生成中间列表再找第一个内存和 CPU 都更浪费。英文书里讲terminal operation时重点就是“没有终端操作Sequence 不会执行”。// src/SequenceTrap.kt fun main() { val numbers sequenceOf(1, 2, 3, 4, 5) val mapped numbers.map { value - println(map $value) value * 2 } println(还没有终端操作) println(mapped.toList()) }逻辑说明sequenceOf创建序列map是中间操作创建mapped时不会打印任何map日志只有toList()触发终端操作才会逐个执行。参数说明toList()是终端操作first()、count()、sum()也都是终端操作。这个示例用日志把惰性求值过程暴露出来比只看英文解释更直观。实际写代码时如果发现 Sequence 链没有输出先检查是不是漏了终端操作。提示集合操作先用List写对再用Sequence优化。顺序是“正确性优先惰性其次”反过来容易在调试时找不到中间结果。5. 避坑与排查英文版示例在本地编译时最容易翻车的 5 个现场英文版技术书有一个天然门槛示例代码、术语解释、版本行为三者交织。书里的短示例在作者环境里能跑到你本地可能因为编译器版本、包名、运行时依赖或者语义理解偏差而翻车。下面这 5 个现场来自我拆这类书稿时反复遇到的类型按“现象 → 原因 → 解决”拆开写方便你遇到问题时直接对照。5.1 编译期与运行期的 3 个现象现象一单文件示例编译通过运行时报NoClassDefFoundError。原因通常是编译时没有把 Kotlin 运行时带进去或者运行时 classpath 不完整。书里为了篇幅通常只给源码不给完整构建脚本读者容易只执行kotlinc Hello.kt然后直接java Hello。解决方式是单文件验证时加-include-runtime -d build/hello.jar再用java -jar运行如果走 classpath就把 Kotlin 运行时的 jar 一起加进去。命令行验证阶段不要急着上构建工具先把“源码 → 字节码 → 运行”这条链路跑通。现象二示例里出现when分支不穷尽、sealed class子类找不到的编译错误。原因往往是文件里同时放了多个顶层声明但包名和目录结构不一致或者sealed class的子类没有放在同一文件/同一包下。Kotlin 对sealed的继承位置有约束英文书里可能用一句话带过。解决方式是先检查文件顶部的package声明再确认sealed class的子类是否在允许的位置。初学阶段可以把一个示例的所有声明放在同一个文件里减少包结构干扰等示例跑通后再拆文件。现象三集合示例输出顺序和书里不一致。原因可能不是代码错而是Set、Map的迭代顺序本身不保证或者用了groupBy后直接打印Map。英文书里有时会默认读者知道LinkedHashMap和HashMap的差异。解决方式是不要依赖未定义顺序做断言需要稳定输出时用sortedBy、sortedMap或toSortedMap()。如果是Set可以改成LinkedHashSet或先distinct()再排序。排查时先把集合类型打印出来别只看最终结果。5.2 语义与阅读的 2 个现象现象四把val当常量结果发现对象内部状态还能改。原因是val只限制引用重新赋值不限制对象内部可变字段。英文书里用read-only描述引用用immutable描述对象状态两个词经常相邻出现。解决方式是用data class的copy()代替修改原对象如果确实需要可变容器就在命名和注释里标清楚。读英文书时看到reassign、mutate、read-only这些词最好停下来用代码验证一遍别用 Java 的final直觉直接套。现象五Sequence 链写完后没有输出以为代码没执行。原因是 Sequence 的中间操作是惰性的没有终端操作就不会触发计算。英文书里会讲intermediate和terminal但短示例容易让人忽略。解决方式是补上toList()、first()、count()、sum()等终端操作调试时可以在中间map里加日志确认执行顺序。另一个相关坑是把Sequence用在很小的集合上性能没提升调试反而更麻烦。我的血泪经验是先用List把逻辑写对再用asSequence()优化并且每次优化后都补一条结果断言。注意英文版里的示例代码通常追求“短”不追求“工程完整”。读的时候把示例当最小验证不要当项目模板直接复制。6. 把原子概念变成回归验证三组断言与一个长期习惯这本英文书的结构适合“读一节、跑一节、验一节”但很多人跑到对象和函数式部分就只剩“看”了。更稳的方式是给每个原子概念配一条最小断言让编译器和你自己都能复核。比如温度转换、字符串模板、空安全处理各写一个checkEqual不需要测试框架命令行就能跑。下面这个文件把三组断言放在一起读英文小节后改参数再跑能快速发现“看懂了但写错了”的情况。// src/AtomicCheck.kt fun fahrenheit(celsius: Int): Int celsius * 9 / 5 32 fun maskEmail(email: String?): String { val value email ?: return 未提供邮箱 val at value.indexOf() return if (at 0) 格式可疑 else ${value.first()}***${value.substring(at)} } fun checkEqual(expected: Any, actual: Any, label: String) { if (expected ! actual) { error($label期望 $expected实际 $actual) } println(通过$label) } fun main() { checkEqual(32, fahrenheit(0), 0°C 转华氏) checkEqual(212, fahrenheit(100), 100°C 转华氏) checkEqual(-40, fahrenheit(-40), 交点温度) checkEqual(未提供邮箱, maskEmail(null), 空邮箱) checkEqual(a***site, maskEmail(alphasite), 正常邮箱) checkEqual(格式可疑, maskEmail(broken), 异常邮箱) }逻辑说明checkEqual用Any接收期望值和实际值比较失败时用error抛异常并带标签。参数说明fahrenheit接收整数摄氏度maskEmail接收可空字符串maskEmail里的?: return提前处理空值。三组温度断言覆盖常规值和交点三组邮箱断言覆盖空值、正常值和异常格式。这个文件不依赖测试框架适合放在示例目录里当“后悔药”改完示例后跑一遍能立刻知道哪条语义被改坏了。从那以后我每读一个原子小节都会先写一个最小可运行文件再补一条断言不让自己停在“看懂了”。如果你正需要一本能把 Kotlin 拆到原子粒度的英文参考书这份电子版适合放在手边按上面的路径跑一遍再决定深入哪一部分。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?