第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框架中是如何被发挥到极致的。