第2章:Kotlin基础回顾:高阶函数、扩展函数、委托属性——为DI打基础

说实话,很多同学一上来就学Dagger、Hilt,结果被各种注解和编译期代码搞得晕头转向。我个人觉得,依赖注入的本质其实很简单——就是把「创建对象」这件事从你手里拿走,交给框架去管。而Kotlin的几个语言特性,恰好能帮我们理解DI到底在干什么。

这一章,咱们不聊框架,就聊Kotlin本身。我会带你过一遍高阶函数、扩展函数和委托属性。这些都是DI实现的核心工具。你想想看,如果没有这些特性,我们写依赖注入得有多痛苦?

2.1 高阶函数:把行为当参数传递

高阶函数,说白了就是能接收函数作为参数,或者返回一个函数的函数。这在DI里太常见了——比如你想延迟创建某个对象,或者按需创建,高阶函数就是最好的载体。

核心概念:函数是一等公民,可以像变量一样传来传去。

来看个最简单的例子:

// 一个普通的高阶函数
fun <T> lazyCreate(provider: () -> T): T {
    return provider()
}

// 使用
val user = lazyCreate {
    User("张三", 25)
}

我在项目中遇到过这样的场景:某个网络请求库需要传入一个「创建OkHttpClient」的工厂方法。用高阶函数写,代码会非常干净:

class ApiClient(private val clientProvider: () -> OkHttpClient) {
    fun execute(request: Request): Response {
        val client = clientProvider()  // 每次请求都创建新实例?还是复用?由调用方决定
        return client.newCall(request).execute()
    }
}

为什么这对DI重要?因为依赖注入框架本质上就是在做一件事:把「如何创建对象」这个行为,注入到需要它的地方。高阶函数让这种行为传递变得极其自然。

我的习惯:在写工具类时,如果某个行为可能变化,我就用高阶函数把它抽象出来。这样调用方可以自由定制,而不需要修改我的代码。

2.2 扩展函数:给别人的类加方法

扩展函数是Kotlin最让我舒服的特性之一。它允许你在不修改原有类的情况下,给这个类添加新的方法。这在DI里有什么用?嗯,用处大了去了。

比如,你想给Android的Context加一个获取AppComponent的方法:

// 扩展函数:给Context添加获取依赖容器的方法
fun Context.appComponent(): AppComponent {
    return (applicationContext as MyApp).appComponent
}

// 使用
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val component = appComponent()  // 直接调用,就像Context自带的方法一样
    }
}

我曾经接手过一个老项目,里面到处都是(application as MyApp).appComponent这种代码。我花了一个下午,把所有地方都改成了扩展函数。代码瞬间清爽了很多。

扩展函数的本质其实就是一个静态方法,第一个参数是接收者类型。但语法上看起来就像是在调用对象的方法。这种「语法糖」在DI框架中大量使用——比如Koin的by inject(),本质上就是扩展函数。

注意:扩展函数不能被子类重写。如果你需要多态行为,应该用成员方法或者接口。我刚开始学的时候踩过这个坑。

2.3 委托属性:把get/set交给别人管

委托属性是Kotlin里最接近「依赖注入」原生实现的一个特性。它允许你把属性的读取和赋值逻辑委托给另一个对象去处理。

标准库提供了几个常用的委托:

委托类型 作用 DI中的应用
lazy 延迟初始化,第一次访问时才创建 单例对象的懒加载
observable 属性变化时触发回调 观察依赖变化
vetoable 属性变化前可以否决 校验依赖是否合法
notNull 保证属性在使用前一定被赋值 延迟注入的占位

来看一个lazy委托的典型用法:

class UserViewModel : ViewModel() {
    // 使用lazy委托,userRepository只在第一次访问时创建
    private val userRepository by lazy {
        UserRepository(
            apiService = ApiService(),
            cacheDao = CacheDao()
        )
    }

    fun loadUser(id: String) {
        userRepository.getUser(id)  // 第一次调用时,lazy才会执行
    }
}

你发现没有?by lazy本质上就是一种最简单的依赖注入——它把「创建UserRepository」这件事延迟到了真正需要的时候。而且只创建一次,天然就是单例。

我在项目中经常用by lazy来管理那些创建成本高、但不一定每次都用到的对象。比如数据库实例、网络客户端等。

核心思想:委托属性把「属性存取」和「属性逻辑」解耦了。这正是DI要做的事——把「对象创建」和「对象使用」解耦。

2.4 三者如何协同工作?

好了,三个特性都讲完了。你可能会问:它们各自独立,怎么组合起来为DI服务?

我举个例子你就明白了。假设我们要手写一个极简的DI容器:

// 1. 定义容器接口
interface Container {
    fun <T> register(key: Class<T>, provider: () -> T)  // 高阶函数:接收创建行为
    fun <T> resolve(key: Class<T>): T
}

// 2. 实现容器
class SimpleContainer : Container {
    private val providers = mutableMapOf<Class<*>, () -> Any>()

    override fun <T> register(key: Class<T>, provider: () -> T) {
        providers[key] = provider  // 存储创建行为
    }

    @Suppress("UNCHECKED_CAST")
    override fun <T> resolve(key: Class<T>): T {
        val provider = providers[key] ?: throw IllegalStateException("No provider for $key")
        return provider() as T  // 执行创建行为
    }
}

// 3. 使用扩展函数让调用更优雅
fun Container.registerSingleton(provider: () -> Any) {
    val instance by lazy { provider() }  // 委托属性:保证单例
    // 这里简化了,实际需要注册到容器中
}

// 4. 使用
val container = SimpleContainer()
container.register(UserRepository::class.java) {
    UserRepository(ApiService(), CacheDao())
}
val repo = container.resolve(UserRepository::class.java)

你看,这个极简的DI容器,核心就是高阶函数(存储创建行为)+ 委托属性(实现单例)+ 扩展函数(提供语法糖)。Kotlin的三个特性完美配合,实现了依赖注入的基本功能。

我的建议:在学习Dagger或Koin之前,先自己动手写一个这样的迷你容器。你会对DI的原理有更深刻的理解。我曾经让团队里的新人这么做,效果比直接看文档好得多。

2.5 避坑指南

最后,分享几个我踩过的坑:

  • 高阶函数的内存泄漏:如果高阶函数捕获了外部引用,而外部引用生命周期较长,可能导致内存泄漏。我曾经在Fragment里用高阶函数传了一个Activity的引用,结果Activity销毁了还被持有。解决方案是用WeakReference或者确保在合适的时机清理。
  • 扩展函数的命名冲突:如果两个库都定义了同名的扩展函数,编译器会报错。我遇到过toast()扩展函数冲突的情况。解决方案是使用@JvmName注解或者改用成员方法。
  • 委托属性的线程安全lazy默认是线程安全的,但如果你用lazy(LazyThreadSafetyMode.NONE),在多线程环境下可能会出问题。我曾在并发场景下踩过这个坑,数据被创建了多次。

好了,这一章的内容就到这里。下一章我们会正式进入依赖注入的核心概念,看看Kotlin的这些特性在主流DI框架中是如何被发挥到极致的。