kotlin中lateinit var和val by lazy的区别

12 阅读3分钟

在学习kotlin的过程中遇到lateinit var 和 val by lazy,他们都用于延迟初始化,但是对它们的使用场景又有些模糊。问了一下AI感觉回答的还挺清晰,记录一下


1. 核心区别

特性lateinit varval by lazy
可变性可变 (var)不可变 (val)
初始化时机显式初始化(开发者手动赋值)首次访问时自动初始化(懒加载)
线程安全需要开发者自行保证线程安全默认线程安全(可配置)
适用类型仅限非空类型(不能是原始类型)支持所有类型(包括原始类型)
异常风险未初始化时访问会抛出 UninitializedPropertyAccessException初始化代码块中的异常会被缓存并重复抛出
是否可重新初始化可重新赋值一旦初始化,值不可变

2. 典型使用场景

lateinit var 适用场景

  1. 依赖注入
    当变量需要由框架(如 Dagger、Koin 或 Spring)在运行时注入时:

    class MyService {
        lateinit var database: Database
    }
    
  2. Android 的 View 初始化
    Activity/Fragment 的视图需要在 onCreate 或 onCreateView 中初始化:

    class MainActivity : AppCompatActivity() {
        lateinit var recyclerView: RecyclerView
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            recyclerView = findViewById(R.id.recycler_view)
        }
    }
    
  3. 单元测试的 Setup
    在测试类的 @Before 方法中初始化:

    class MyTest {
        lateinit var httpClient: HttpClient
        @Before
        fun setup() {
            httpClient = HttpClient.create()
        }
    }
    
  4. 需要重新赋值的场景
    变量可能在生命周期内被多次初始化:

    class ConfigManager {
        lateinit var currentConfig: Config
        fun updateConfig(newConfig: Config) {
            currentConfig = newConfig
        }
    }
    

val by lazy 适用场景

  1. 昂贵的资源初始化
    初始化成本高且可能不会被使用的资源:

    class ImageLoader {
        val heavyResource: HeavyResource by lazy {
            HeavyResource.loadFromDisk() // 仅在第一次访问时加载
        }
    }
    
  2. 不可变单例或共享状态
    需要线程安全的单例或共享对象:

    class AppSettings {
        val instance: AppSettings by lazy { AppSettings() }
    }
    
  3. 依赖其他属性的初始化
    初始化需要依赖对象的其他属性:

    class UserViewModel(userId: String) {
        private val userRepository = UserRepository()
        val user: User by lazy { userRepository.getUser(userId) }
    }
    
  4. 配置灵活初始化逻辑
    支持自定义初始化逻辑和异常处理:

    val config: Config by lazy(LazyThreadSafetyMode.NONE) {
        if (isDebug) DebugConfig() else ProductionConfig()
    }
    

3. 关键注意事项

lateinit var

  • 必须确保在使用前初始化,否则会抛出异常。
  • 可以通过 ::myProperty.isInitialized 检查是否已初始化。
  • 不能用于原始类型(如 IntBoolean)。

val by lazy

  • 初始化代码块 ({}) 只会在第一次访问时执行。

  • 默认线程安全(LazyThreadSafetyMode.SYNCHRONIZED),可通过参数调整:

    val data: Data by lazy(LazyThreadSafetyMode.NONE) { ... } // 非线程安全
    
  • 如果初始化代码抛出异常,后续访问会重复抛出同一异常。


4. 代码示例对比

使用 lateinit var

class NetworkClient {
    lateinit var endpoint: String

    fun init(endpoint: String) {
        this.endpoint = endpoint
    }

    fun sendRequest() {
        if (::endpoint.isInitialized) {
            println("Sending request to $endpoint")
        }
    }
}

使用 val by lazy

class AppConfig {
    val serverUrl: String by lazy {
        val env = System.getenv("APP_ENV")
        if (env == "prod") "https://api.prod.com" else "https://api.dev.com"
    }

    fun logConfig() {
        println("Server URL is $serverUrl") // 首次调用时初始化
    }
}

5. 如何选择?

  • 用 lateinit var 如果:
    需要可变性、显式控制初始化时机,或者变量可能被重新赋值。
  • 用 val by lazy 如果:
    需要不可变性、线程安全、或初始化逻辑复杂且需要懒加载。