第11章:泛型与异常处理

掌握 Kotlin 类型参数化与错误处理机制,编写更安全、更灵活的代码

🎩

福尔摩斯

类型推理,穷举所有可能

亲爱的华生,一桩好案件的关键在于:先圈定嫌疑范围,再依据铁证逐一排除,直到只剩下唯一的真相。泛型就是这门推理术——类型参数 T 是嫌疑人档案,约束条件是筛选的证据链,而异常处理则是为每一条可能的死胡同提前布下的应急预案。今天,让我们以侦探的眼光,穷举类型系统与错误处理的一切可能。

11.1 泛型基础

华生,你是否注意到,调查案件时我们经常需要一套通用的推理框架——无论嫌疑人是商人、贵族还是仆从,推理的方法是相同的,变化的只是具体的人物。编程中也是如此:一段逻辑对多种类型都适用,如果为每种类型各写一份代码,不仅冗余而且难以维护。泛型(Generics)正是这套"通用推理框架"——它让我们在定义函数、类或接口时使用类型参数(即嫌疑人的"占位符"),在实际调用时再填入具体类型,从而同时获得类型安全代码复用两大优势。

泛型函数

最基础的泛型用法,就像建立一份"万能嫌疑人档案"——在函数名前用尖括号声明类型参数 T,调用时再推理出具体类型:

fun <T> printItem(item: T) {
    println("项目: $item")
}

// 调用时可以显式指定类型,也可以让编译器自动推断
printItem<String>("Hello")   // 显式指定
printItem(42)                 // 编译器推断为 Int

泛型类

泛型类是最常见的应用场景——想象一个"证物箱",它能安全地保管任何类型的物品,但取出时你总能确切知道里面是什么:

class Box<T>(val value: T) {
    fun getContent(): T = value

    fun describe(): String = "Box 中装的是: $value"
}

// 使用
val stringBox = Box("Kotlin")      // Box<String>
val intBox = Box(2024)              // Box<Int>

println(stringBox.getContent())     // "Kotlin"
println(intBox.describe())          // "Box 中装的是: 2024"

多类型参数

一桩案件往往涉及多位嫌疑人——当我们需要多个类型参数时,用逗号分隔即可:

class Pair<A, B>(
    val first: A,
    val second: B
) {
    fun toFormattedString(): String = "($first, $second)"
}

val nameAge = Pair("张三", 25)
println(nameAge.toFormattedString())  // "(张三, 25)"

泛型接口

接口同样支持泛型。这就像制定一套标准调查流程——无论是调查人物、物品还是事件,操作步骤(查询、保存、删除)是统一的:

interface Repository<T> {
    fun getById(id: Int): T?
    fun getAll(): List<T>
    fun save(item: T)
    fun delete(item: T)
}

data class User(val id: Int, val name: String)

class UserRepository : Repository<User> {
    private val users = mutableListOf<User>()

    override fun getById(id: Int): User? = users.find { it.id == id }
    override fun getAll(): List<User> = users.toList()
    override fun save(item: User) { users.add(item) }
    override fun delete(item: User) { users.remove(item) }
}
泛型命名约定

常用的类型参数名称:T(Type)、E(Element)、K(Key)、V(Value)、R(Result)。虽然可以使用任何名称,但遵循约定有助于代码可读性——正如侦探档案中的编号系统,统一命名才能高效检索。

C++ 开发者对照

Kotlin 泛型有类型擦除(运行时不知道 T 是什么),C++ 模板是编译期实例化(每个 T 生成独立代码)。Kotlin 的 List<String> 运行时只是 List,C++ 的 vector<string> 是完全独立的类型。换言之,Kotlin 在运行时"销毁了嫌疑人档案",而 C++ 为每位嫌疑人都建立了独立的卷宗。

11.2 类型约束与型变

上界约束

华生,调查案件时我们不会漫无目的地排查全城居民——我们会根据证据缩小嫌疑范围。类型约束也是同理:通过上界约束(Upper Bound),我们限制类型参数必须是某个类型或其子类型,就像排除掉那些显然不可能的嫌疑人:

fun <T : Comparable<T>> findMax(a: T, b: T): T {
    return if (a > b) a else b
}

println(findMax(10, 20))           // 20
println(findMax("apple", "banana")) // "banana"

由于约束了 T : Comparable<T>,编译器就掌握了一条铁证:传入的类型一定实现了比较接口。有了这条证据,使用 > 运算符就是完全安全的——我们已经排除了所有不具备"可比较"能力的类型。

多重约束(where 子句)

有时一条线索不足以锁定嫌疑人,我们需要交叉比对多条证据。当类型参数需要同时满足多个约束时,使用 where 关键字——这就是联合多条证据链的推理:

fun <T> copyIfValid(item: T)
    where T : Comparable<T>,
          T : Cloneable {
    println("该元素可比较且可克隆: $item")
}

// 泛型类同样可以使用 where
class SortedStorage<T>
    where T : Comparable<T>,
          T : Any {
    private val items = mutableListOf<T>()

    fun add(item: T) {
        items.add(item)
        items.sort()
    }

    fun getAll(): List<T> = items.toList()
}

协变(out)——生产者

现在来谈谈型变(Variance),这是泛型推理中最精妙的一环。华生,请思考:如果 DogAnimal 的子类,那么 List<Dog> 能当作 List<Animal> 使用吗?这就像问:一份只记录了警犬的名单,能否作为动物名单使用?

答案取决于证据的流向。使用 out 关键字可以声明协变——即泛型类只"输出"(生产)该类型的值,不"输入"(消费)它。证据只从内部流向外部,如同线人只提供情报、不接受指令:

open class Animal(val name: String)
class Dog(name: String) : Animal(name)
class Cat(name: String) : Animal(name)

// 使用 out 声明协变:只能读取 T,不能写入 T
interface Source<out T> {
    fun next(): T    // 可以:T 出现在返回值位置
    // fun add(item: T)  // 不可以:T 不能出现在参数位置
}

class DogSource : Source<Dog> {
    override fun next(): Dog = Dog("旺财")
}

// 因为 Source 是协变的,所以 Source<Dog> 可以赋值给 Source<Animal>
val animalSource: Source<Animal> = DogSource()
println(animalSource.next().name)  // "旺财"

Kotlin 标准库中的 List<out E> 就是协变的——它是只读列表,只产出元素,因此 List<Dog> 可以安全地作为 List<Animal> 使用。推理至此,铁证如山。

逆变(in)——消费者

与协变相反,in 关键字声明逆变——泛型类只"消费"该类型的值。证据只从外部流入内部,如同法庭只接收证据、不对外发布:

// 使用 in 声明逆变:只能写入 T,不能读取 T
interface Consumer<in T> {
    fun consume(item: T)  // 可以:T 出现在参数位置
    // fun produce(): T     // 不可以:T 不能出现在返回值位置
}

class AnimalHandler : Consumer<Animal> {
    override fun consume(item: Animal) {
        println("处理动物: ${item.name}")
    }
}

// 因为 Consumer 是逆变的,所以 Consumer<Animal> 可以赋值给 Consumer<Dog>
val dogHandler: Consumer<Dog> = AnimalHandler()
dogHandler.consume(Dog("小白"))  // "处理动物: 小白"

推理一下便知:一个能处理所有 Animal 的处理器,当然也能处理 Dog——这就像苏格兰场的资深探长,既能办大案也能办小案。标准库中的 Comparator<in T> 就是逆变的典型例子。

型变对照表

关键字 名称 T 的位置 子类型关系 类比
out 协变 只能作为返回值(输出) Dog : Animal => C<Dog> : C<Animal> 生产者(Producer)
in 逆变 只能作为参数(输入) Dog : Animal => C<Animal> : C<Dog> 消费者(Consumer)
不变 可作为参数和返回值 C<Dog> 与 C<Animal> 无关 MutableList<T>
PECS 原则

记忆口诀:Producer-Extends, Consumer-Super(对应 Kotlin 中的 outin)。用侦探的话说:如果你只从线人那里获取情报,用 out;如果你只向档案室提交证据,用 in

C++ 开发者对照

C++ 没有内建的型变标注(out/in),模板天然不支持协变和逆变。在 C++ 中如果需要类似效果,必须手动通过模板特化或 SFINAE 约束来处理。Kotlin 的声明处型变(declaration-site variance)比 Java 的使用处通配符(? extends / ? super)更简洁,而 C++ 则完全没有这层自动推理。

星投影(Star Projection)

当你不关心具体的类型参数,或者不知道嫌疑人究竟是谁时——就像收到一个匿名线报,你只知道"有人"但不知道具体身份——可以使用星投影 *

fun printListSize(list: List<*>) {
    println("列表大小: ${list.size}")
    // 可以安全读取为 Any?
    for (item in list) {
        println("  元素: $item")
    }
}

printListSize(listOf(1, 2, 3))
printListSize(listOf("a", "b"))

List<*> 等价于 List<out Any?>,即你可以安全地读取元素(类型为 Any?),但不能向其中写入任何东西——就像你可以观察匿名线人提供的物品,但无法向他交付任务。

11.3 reified 类型参数

类型擦除问题

华生,这是一桩令人头疼的"证据消失案"。Kotlin 运行在 JVM 上,而 JVM 的泛型采用类型擦除(Type Erasure)机制——在编译后,泛型的类型信息被抹去了。这意味着在运行时,我们无法推理出 T 究竟是谁——嫌疑人档案在进入法庭前被人销毁了:

// 以下代码无法编译!类型擦除导致运行时不知道 T 是什么
fun <T> isType(value: Any): Boolean {
    return value is T  // 编译错误:Cannot check for instance of erased type: T
}

inline + reified 的解决方案

但一位出色的侦探绝不会被证据的消失所难倒。Kotlin 提供了 inline 函数配合 reified 类型参数来破解此案。inline 函数在调用处内联展开,编译器在展开时将具体的类型信息"嵌入"到代码中——相当于在每个调查现场都保留了一份嫌疑人档案的副本:

inline fun <reified T> isType(value: Any): Boolean {
    return value is T  // 现在可以了!
}

println(isType<String>("hello"))  // true
println(isType<Int>("hello"))     // false
println(isType<Int>(42))           // true
C++ 开发者对照

reified 解决了类型擦除问题,效果类似 C++ 模板特化——在编译期为每个具体类型生成专用代码。但实现机制不同:C++ 模板在编译期实例化出完整的独立函数,Kotlin 的 reified 则通过 inline 内联展开来保留类型信息。C++ 天然没有类型擦除问题(typeiddynamic_cast 始终可用),因此不需要类似的"补救"机制。

实用示例:类型过滤

下面模拟标准库 filterIsInstance 的功能——从一堆混杂的证据中,精准筛选出某一类别的物证:

inline fun <reified T> List<Any>.filterByType(): List<T> {
    val result = mutableListOf<T>()
    for (item in this) {
        if (item is T) {
            result.add(item)
        }
    }
    return result
}

val mixed = listOf(1, "hello", 3.14, "world", 42)
val strings = mixed.filterByType<String>()  // ["hello", "world"]
val ints = mixed.filterByType<Int>()        // [1, 42]

实用示例:简化 JSON 解析

在 Android 开发中,reified 经常用于简化 Gson/Moshi 等 JSON 库的调用——让编译器自动推理目标类型,省去手动传递 Class 参数的繁琐:

import com.google.gson.Gson

inline fun <reified T> fromJson(json: String): T {
    return Gson().fromJson(json, T::class.java)
}

// 使用:无需手动传递 Class 参数
data class Product(val name: String, val price: Double)

val json = """{"name":"手机","price":4999.0}"""
val product: Product = fromJson(json)
println(product.name)  // "手机"
reified 的限制

reified 只能用在 inline 函数中,不能用在类的类型参数上。此外,由于内联会导致代码膨胀,不宜在过长的函数上使用——就像不能在每份案卷里都完整复印整个档案库。

11.4 泛型在 Android 中的应用

密封类 + 泛型:统一的 API 响应封装

华生,在真实的调查工作中,每次行动的结果无非三种:成功获取线索、遭遇失败、仍在进行中。Android 网络请求也是如此。用密封类配合泛型,我们可以穷举所有可能的结果,不遗漏任何一条分支——这正是侦探精神的体现:

sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Error(val message: String, val code: Int) : ApiResult<Nothing>()
    object Loading : ApiResult<Nothing>()
}

// 在 ViewModel 中使用
class UserViewModel {
    suspend fun fetchUser(id: Int): ApiResult<User> {
        return try {
            val user = User(id, "用户$id")
            ApiResult.Success(user)
        } catch (e: Exception) {
            ApiResult.Error(e.message ?: "未知错误", 500)
        }
    }
}

// 调用处使用 when 进行模式匹配
fun handleResult(result: ApiResult<User>) {
    when (result) {
        is ApiResult.Success -> println("用户: ${result.data.name}")
        is ApiResult.Error   -> println("错误: ${result.message}")
        is ApiResult.Loading -> println("加载中...")
    }
}

泛型 RecyclerView Adapter 思路

在 Android 中,我们经常需要为不同的数据类型编写相似的列表适配器。利用泛型抽取公共逻辑,就像建立一套标准化的调查程序——无论调查对象是谁,流程保持一致:

abstract class BaseAdapter<T, VH : RecyclerView.ViewHolder> :
    RecyclerView.Adapter<VH>() {

    protected val items = mutableListOf<T>()

    fun submitList(newItems: List<T>) {
        items.clear()
        items.addAll(newItems)
        notifyDataSetChanged()
    }

    override fun getItemCount(): Int = items.size

    fun getItem(position: Int): T = items[position]
}

泛型扩展函数

泛型扩展函数是 Kotlin 的一大利器——可以为已有类型添加类型安全的通用方法。就像一位侦探可以随时为调查工具箱增添新工具,而不必改造工具箱本身:

// 安全类型转换
inline fun <reified T> Any.safeCast(): T? = this as? T

// 非空时执行操作并返回结果
fun <T, R> T?.whenNotNull(block: (T) -> R): R? {
    return if (this != null) block(this) else null
}

// 列表安全获取
fun <T> List<T>.safeGet(index: Int, default: T): T {
    return if (index in indices) this[index] else default
}

// 使用示例
val result = "hello".safeCast<String>()  // "hello"
val number = "hello".safeCast<Int>()     // null

val name: String? = "Kotlin"
name.whenNotNull { println("名称: $it") }  // "名称: Kotlin"

val list = listOf(10, 20, 30)
println(list.safeGet(5, 0))  // 0(越界时返回默认值)

11.5 异常处理基础

华生,即便是最周密的调查计划,也无法预见所有突发状况:线人失联、证据被篡改、通讯中断……编程同理,运行时总会遇到不可预见的情况:网络断开、文件不存在、数据格式错误等。异常处理机制就是我们为每一桩案件预先制定的应急预案——确保即使遭遇意外,调查(程序)也不会彻底崩溃。

try / catch / finally

Kotlin 的异常处理使用 trycatchfinally 三个关键字——分别对应"尝试执行调查"、"遭遇意外时的应对"和"无论结果如何都要执行的收尾工作":

fun readNumber(input: String): Int {
    try {
        return input.toInt()
    } catch (e: NumberFormatException) {
        println("无法将 '$input' 转换为数字: ${e.message}")
        return 0
    } finally {
        println("转换操作完成")
    }
}

println(readNumber("42"))    // 42
println(readNumber("abc"))   // 0

finally 块中的代码无论是否发生异常都会执行——这是案件收尾的铁律,无论调查成功与否,现场都必须恢复原状。

try 作为表达式

与 Java 不同,Kotlin 中的 try 是一个表达式,可以直接将其结果赋给变量。这意味着我们的调查结论可以直接写入案卷,无需迂回:

val number = try {
    "123".toInt()
} catch (e: NumberFormatException) {
    0
}
println(number)  // 123

val fallback = try {
    "abc".toInt()
} catch (e: NumberFormatException) {
    -1
}
println(fallback) // -1
C++ 开发者对照

Kotlin 无受检异常,所有异常都像 C++ 的运行时异常;C++ 有 noexcept 说明符来标注函数不抛异常,Kotlin 没有对应机制。关键区别:Kotlin 的 try表达式(有返回值,可以赋给变量),C++ 的 try 是语句(没有返回值)。这让 Kotlin 的错误处理可以写得更简洁。

多重 catch 块

一桩复杂案件可能面临多种不同类型的意外——我们需要针对每种情况分别制定应对策略。可以使用多个 catch 块来穷举各种异常类型:

fun processData(data: String, index: Int): Char {
    return try {
        val number = data.toInt()
        val text = "结果是 $number"
        text[index]
    } catch (e: NumberFormatException) {
        println("格式错误: ${e.message}")
        '?'
    } catch (e: IndexOutOfBoundsException) {
        println("索引越界: $index")
        '!'
    } catch (e: Exception) {
        println("其他异常: ${e.message}")
        '#'
    }
}
catch 块顺序很重要

多个 catch 块时,应将更具体的异常类型放在前面,更通用的放在后面。如果把 Exception 放在第一个,后面的 catch 块将永远不会被执行——这就像在排除嫌疑人时,如果一开始就说"所有人都有嫌疑",那后续的精细筛查便毫无意义。

Kotlin 没有受检异常

与 Java 不同,Kotlin 没有受检异常(Checked Exceptions)。Kotlin 的设计者经过审慎推理,认为受检异常在实践中弊大于利:

在 Kotlin 中,所有异常都是非受检的。如果需要与 Java 互操作并声明异常,可以使用 @Throws 注解:

@Throws(IOException::class)
fun readFile(path: String): String {
    // Java 代码调用此函数时,编译器会强制处理 IOException
    return File(path).readText()
}

11.6 自定义异常与最佳实践

创建自定义异常

华生,当标准的报案类型无法准确描述案情时,我们就需要创建专属的案件分类。同理,当标准异常类型不足以描述业务错误时,应创建自定义异常类——为每种"案件类型"建立独立的档案:

// 业务异常基类
open class BusinessException(
    message: String,
    val errorCode: Int
) : Exception(message)

// 具体异常类
class UserNotFoundException(
    userId: Int
) : BusinessException("用户 $userId 不存在", 404)

class InsufficientBalanceException(
    required: Double,
    available: Double
) : BusinessException(
    "余额不足: 需要 $required,当前 $available", 402
)

// 使用示例
fun withdraw(amount: Double, balance: Double): Double {
    if (amount > balance) {
        throw InsufficientBalanceException(amount, balance)
    }
    return balance - amount
}

try {
    withdraw(1000.0, 500.0)
} catch (e: InsufficientBalanceException) {
    println("[${e.errorCode}] ${e.message}")
    // [402] 余额不足: 需要 1000.0,当前 500.0
}

Nothing 类型

Nothing 是 Kotlin 中一个特殊的类型,表示"永远不会有返回值"——就像一条必然导致案件终结的线索:一旦走上这条路,就不可能再回头。当一个函数总是抛出异常或者是一个无限循环时,可以将其返回类型声明为 Nothing

fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

// Nothing 的好处:编译器知道此函数永远不会正常返回
fun getUserName(user: User?): String {
    val name = user?.name ?: fail("用户不能为空")
    // 编译器在此处确定 name 一定是 String(非空),
    // 因为如果 user?.name 为 null,fail() 会抛出异常
    return name
}

前置条件函数

Kotlin 标准库提供了一组前置条件函数,如同侦探出发前的安全检查清单——在不满足条件时快速抛出异常,将问题扼杀在萌芽阶段:

fun createUser(name: String, age: Int): User {
    // require —— 验证参数,不满足时抛出 IllegalArgumentException
    require(name.isNotBlank()) { "用户名不能为空" }
    require(age in 0..150) { "年龄必须在 0 到 150 之间,当前: $age" }

    return User(0, name)
}

class Connection {
    var isConnected = false

    fun sendData(data: String) {
        // check —— 验证对象状态,不满足时抛出 IllegalStateException
        check(isConnected) { "连接未建立,无法发送数据" }
        println("发送数据: $data")
    }
}

// error —— 直接抛出 IllegalStateException
fun getStatus(code: Int): String = when (code) {
    200 -> "成功"
    404 -> "未找到"
    500 -> "服务器错误"
    else -> error("未知状态码: $code")
}
函数 用途 抛出的异常
require(condition) 验证函数参数 IllegalArgumentException
requireNotNull(value) 验证参数非空 IllegalArgumentException
check(condition) 验证对象状态 IllegalStateException
checkNotNull(value) 验证状态值非空 IllegalStateException
error(message) 直接抛出异常并终止 IllegalStateException
选择合适的前置条件函数

推理原则很简单:验证外部输入(函数参数,即"证人证词")用 require,验证内部状态(对象自身状态,即"调查进度")用 check,标记"不应该到达的代码"(即"推理中的不可能分支")用 error

11.7 runCatching 与 Result

华生,传统的 try-catch 就像在案件现场亲自处置每一个突发状况——有效但不够优雅。Kotlin 标准库提供了 runCatching 函数和 Result<T> 类型,这是一种更精密的方法——将每次调查的结论封装为一个案件裁决(Result),无论成功还是失败,都以统一的形式呈堂。这种函数式的处理方式更适合链式推理和证据传递。

runCatching 基础

runCatching 会执行给定的代码块,如果成功则返回 Result.success(value)(案件告破),如果抛出异常则返回 Result.failure(exception)(调查受阻):

val result = runCatching {
    "42".toInt()
}

println(result.isSuccess)    // true
println(result.getOrNull())  // 42

val failed = runCatching {
    "abc".toInt()
}

println(failed.isFailure)    // true
println(failed.getOrNull())  // null

Result 的丰富 API

Result<T> 提供了多种处理成功和失败结果的方法——就像侦探针对不同的案件裁决,可以选择不同的后续行动方案:

val result = runCatching {
    fetchDataFromNetwork()
}

// getOrDefault:失败时返回默认值
val data1 = result.getOrDefault("默认数据")

// getOrElse:失败时通过 lambda 计算返回值
val data2 = result.getOrElse { exception ->
    println("请求失败: ${exception.message}")
    "备用数据"
}

// onSuccess / onFailure:分别在成功或失败时执行操作
result
    .onSuccess { data -> println("获取成功: $data") }
    .onFailure { error -> println("获取失败: ${error.message}") }
C++ 开发者对照

Kotlin 的 Result<T> 类似 C++23 的 std::expected<T, E>,两者都将成功值或错误封装在同一个对象中。但 Kotlin 的 Result 更函数式——内置 mapfoldrecover 等链式操作,而 std::expected 的链式 API 相对有限(and_thentransform)。此外,Kotlin 的 Result 固定以 Throwable 作为错误类型,C++ 的 std::expected 则允许自定义错误类型 E

Result 的链式操作

Result 支持 mapfold 等链式操作,使得错误处理可以像推理链条一样层层递进——每一步的结论成为下一步推理的输入:

fun parseAge(input: String): String {
    return runCatching {
        input.trim().toInt()
    }.map { age ->
        // 成功时转换结果
        when {
            age < 18 -> "未成年"
            age < 60 -> "成年人"
            else     -> "老年人"
        }
    }.getOrDefault("输入无效")
}

println(parseAge("25"))   // "成年人"
println(parseAge("abc"))  // "输入无效"

fold:统一处理成功与失败

fold 方法接受两个 lambda,分别处理成功和失败的情况,并返回统一类型的结果——穷举了所有可能的案件走向,不留任何死角:

fun loadUserProfile(userId: Int): String {
    return runCatching {
        // 模拟可能失败的操作
        if (userId <= 0) throw IllegalArgumentException("无效 ID")
        User(userId, "用户$userId")
    }.fold(
        onSuccess = { user -> "欢迎, ${user.name}!" },
        onFailure = { error -> "加载失败: ${error.message}" }
    )
}

println(loadUserProfile(1))    // "欢迎, 用户1!"
println(loadUserProfile(-1))   // "加载失败: 无效 ID"

对象上调用 runCatching

runCatching 也可以作为扩展函数在对象上调用,此时 lambda 内的 this 指向该对象——就像以某个特定嫌疑人为切入点展开调查:

val input = "  123  "

val result = input.runCatching {
    trim().toInt()  // this 就是 input 字符串
}

println(result.getOrNull())  // 123

try-catch 与 runCatching 对照表

特性 try-catch runCatching
代码风格 命令式 函数式
链式调用 不支持 支持(map, fold, onSuccess 等)
结果传递 通过变量或 return 通过 Result<T> 对象
多异常类型 多个 catch 块分别处理 在 onFailure/fold 中统一处理
finally 支持 原生支持 无直接支持,但可使用 also
适用场景 复杂的分类错误处理、需要 finally 结果转换、链式处理、传递错误

实际应用:组合使用

在实际项目中,两种方式经常结合使用。下面是一个综合示例,展示如何用泛型和异常处理构建一个健壮的数据加载工具——穷举所有成功与失败的可能性,确保推理链条的每一环都无懈可击:

sealed class LoadResult<out T> {
    data class Success<T>(val data: T) : LoadResult<T>()
    data class Failure(val error: Throwable) : LoadResult<Nothing>()
}

// 通用的安全加载函数
inline fun <T> safeLoad(block: () -> T): LoadResult<T> {
    return runCatching { block() }
        .fold(
            onSuccess = { LoadResult.Success(it) },
            onFailure = { LoadResult.Failure(it) }
        )
}

// 使用
fun main() {
    val userResult = safeLoad {
        // 模拟网络请求
        User(1, "张三")
    }

    when (userResult) {
        is LoadResult.Success -> {
            println("加载成功: ${userResult.data.name}")
        }
        is LoadResult.Failure -> {
            println("加载失败: ${userResult.error.message}")
        }
    }

    // 演示多步骤链式处理
    val displayName = runCatching { fetchUserFromApi(1) }
        .map { user -> user.name.uppercase() }
        .recover { "匿名用户" }
        .getOrDefault("未知")

    println("显示名称: $displayName")
}
选择建议

对于简单的局部错误处理,try-catch 如同现场即时决断,清晰直接;对于需要传递错误、链式转换或函数式风格的场景,runCatching + Result 如同将案件裁决书逐级上报,更为系统化。在 Android 的 Repository 层和 ViewModel 层,runCatching 配合密封类是当下最流行的推理模式。

本章练习

练习 1:泛型栈 简单

实现一个泛型类 Stack<T>,支持 push(item: T)pop(): T?peek(): T?isEmpty(): Boolean 四个操作。

查看答案
class Stack<T> {
    private val elements = mutableListOf<T>()

    fun push(item: T) {
        elements.add(item)
    }

    fun pop(): T? = if (elements.isNotEmpty()) {
        elements.removeAt(elements.size - 1)
    } else null

    fun peek(): T? = elements.lastOrNull()

    fun isEmpty(): Boolean = elements.isEmpty()
}

// 测试
val stack = Stack<Int>()
stack.push(1)
stack.push(2)
stack.push(3)
println(stack.peek())  // 3
println(stack.pop())   // 3
println(stack.pop())   // 2

练习 2:带约束的泛型排序 中等

编写一个泛型函数 sortedBetween,接受一个 List<T> 和上下界 minmax,返回列表中值在 [min, max] 范围内的元素并排序。要求 T 必须实现 Comparable<T>

查看答案
fun <T : Comparable<T>> sortedBetween(
    list: List<T>,
    min: T,
    max: T
): List<T> {
    require(min <= max) { "min 不能大于 max" }
    return list.filter { it in min..max }.sorted()
}

// 测试
val numbers = listOf(5, 3, 8, 1, 9, 2, 7)
println(sortedBetween(numbers, 3, 8))  // [3, 5, 7, 8]

val words = listOf("dog", "cat", "bird", "elephant")
println(sortedBetween(words, "c", "e"))  // [cat, dog]

练习 3:reified 类型安全转换 中等

编写一个 inline + reified 的扩展函数 castOrDefault,将 Any 安全转换为目标类型 T,转换失败时返回给定的默认值。

查看答案
inline fun <reified T> Any.castOrDefault(default: T): T {
    return if (this is T) this else default
}

// 测试
val a: Any = "Hello"
println(a.castOrDefault<String>("默认"))  // "Hello"
println(a.castOrDefault<Int>(0))          // 0

val b: Any = 42
println(b.castOrDefault<Int>(0))          // 42
println(b.castOrDefault<String>("默认"))  // "默认"

练习 4:runCatching 链式处理 中等

编写一个函数 parseConfig,接受一个 JSON 格式的字符串(如 "port:8080"),使用 runCatching 链式操作来:(1) 以 : 分割字符串,(2) 取第二部分转为 Int,(3) 验证端口范围在 1~65535 之间。任何一步失败都返回默认端口 8080

查看答案
fun parseConfig(config: String): Int {
    return runCatching {
        val parts = config.split(":")
        require(parts.size == 2) { "格式错误" }
        parts[1].trim().toInt()
    }.map { port ->
        require(port in 1..65535) { "端口越界" }
        port
    }.getOrDefault(8080)
}

// 测试
println(parseConfig("port:3000"))   // 3000
println(parseConfig("port:abc"))    // 8080
println(parseConfig("port:99999"))  // 8080
println(parseConfig("invalid"))     // 8080

练习 5:协变与逆变实战 困难

设计一个事件系统:(1) 定义基类 Event 及其子类 ClickEventKeyEvent;(2) 定义协变接口 EventProducer<out T : Event> 和逆变接口 EventConsumer<in T : Event>;(3) 实现具体的 ClickProducerAnyEventLogger(消费所有 Event);(4) 演示协变赋值(EventProducer<ClickEvent> 赋给 EventProducer<Event>)和逆变赋值(EventConsumer<Event> 赋给 EventConsumer<ClickEvent>)。

查看答案
open class Event(val timestamp: Long = System.currentTimeMillis())
class ClickEvent(val x: Int, val y: Int) : Event()
class KeyEvent(val keyCode: Int) : Event()

// 协变:只产出事件
interface EventProducer<out T : Event> {
    fun produce(): T
}

// 逆变:只消费事件
interface EventConsumer<in T : Event> {
    fun consume(event: T)
}

class ClickProducer : EventProducer<ClickEvent> {
    override fun produce(): ClickEvent = ClickEvent(100, 200)
}

class AnyEventLogger : EventConsumer<Event> {
    override fun consume(event: Event) {
        println("事件记录 @ ${event.timestamp}")
    }
}

// 协变演示:ClickEvent 是 Event 的子类,
// 所以 EventProducer<ClickEvent> 可以赋给 EventProducer<Event>
val producer: EventProducer<Event> = ClickProducer()
val event = producer.produce()

// 逆变演示:EventConsumer<Event> 可以赋给 EventConsumer<ClickEvent>
val clickConsumer: EventConsumer<ClickEvent> = AnyEventLogger()
clickConsumer.consume(ClickEvent(50, 75))
« 上一章:新人成长路线 目录 下一章:Flow 响应式编程 »