[译]带你揭开Kotlin中属性代理和懒加载语法糖衣

2024-08-27 14:38

本文主要是介绍[译]带你揭开Kotlin中属性代理和懒加载语法糖衣,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

翻译说明:

原标题: How Kotlin’s delegated properties and lazy-initialization work

原文地址: https://medium.com/til-kotlin/how-kotlins-delegated-properties-and-lazy-initialization-work-552cbad8be60

原文作者: Chang W. Doh

在这里插入图片描述

在支持面向对象范式的编程语言中,相信大家对访问属性应该非常熟悉了吧。Kotlin就提供了很多这样的方法,通过by lazy实现属性的懒加载就是一个很好的例子。

在这篇文章中,我们将一起去看看如何使用Kotlin中的委托属性以及by lazy的懒加载然后深入了解它们内部的工作原理,一步步揭开它们语法糖衣。

可空类型

我认为你们中很多人对nullable已经了然于胸,但是让我们再来看看它。我们使用Kotlin来开发Android时你可能会像如下这样写:

class MainActivity : AppCompatActivity() {private var helloMessage : String = "Hello"
}
可空类型在自己生命周期内初始化

在上述例子中,在对象创建的时候就初始化,这也没什么大的问题。然而,如果在特定的初始化过程之后引用它,则不能提前声明和使用值,因为它有自己的生命周期来初始化自身。

让我们一起来看下一些熟悉Java代码

public class MainActivity extends AppCompatActivity {private TextView mWelcomeTextView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mWelcomeTextView = (TextView) findViewById(R.id.msgView);}
}

你可以使用Kotlin来实现上述代码,通过将上述mWelcomeTextView声明成可空类型就可以了.

class MainActivity : AppCompatActivity() {private var mWelcomeTextView: TextView? = null//声明成可空类型override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)mWelcomeTextView = findViewById(R.id.msgView) as TextView}
}

非空类型

上面那个例子代码运行良好,但是在代码中使用属性的之前每次都需要检查它是否为null就显得难受了。这一点你完全可以使用非空类型实现它。

class MainActivity: AppCompatActivity () { private var mWelcomeTextView: TextView... 
}

当然上述代码,你需要使用lateinit来告诉编译器,你将稍后为组件mWelcomeTextView初始化值。

lateinit: 我稍后会初始化非空类型的属性

与我们通常讨论的延迟初始化(lazy initialization) 不同的是,lateinit允许编译器识别非空类型属性的值不存储在构造函数阶段以致于可以正常编译。

class MainActivity : AppCompatActivity() {private lateinit var mWelcomeTextView: TextViewoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)mWelcomeTextView = findViewById(R.id.msgView) as TextView}
}

如果你想要了解更多,请查看这里

只读属性

通常,如果组件的字段不是基本数据类型或者内置类型,则可以发现引用是保留在组件整个生命周期中的。

例如,在Android应用程序中,大多数的组件引用是在Acitivity生命周期中保持不变的。换句话说,这就意味着你很少需要更改组件的引用。

基于这一点,我们可以很容易想到以下这点:

“如果属性的值通常保留在组件的生命周期中,那么只读类型的属性是否足以保持该值?”

我认为可以的,要做到这一点,乍看一眼只需将var改为var一点改动就可以了。

非空只读属性的问题

但是,当我们声明只读属性时,我们面临的问题是无法定义执行初始化的位置

class MainActivity : AppCompatActivity() {private val mWelcomeTextView: TextViewoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)// Where do I move the initialization code????? 我应该把这个初始化的代码移到哪呢???// mWelcomeTextView = findViewById(R.id.msgView) as TextView}
}

现在让我们尝试解决最后一个问题:

“我们在哪初始化只读属性呢”

懒加载(lazy Initialization)

当实现在Kotlin中执行延迟初始化的只读属性是,by lazy也许就特别有用了。

by lazy{...}执行初始化程序,其中首先使用的是定义的属性,而不是它的声明。

class MainActivity : AppCompatActivity() {private val messageView : TextView by lazy {// 下面这段代码将在第一次访问messageView时执行findViewById(R.id.message_view) as TextView}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)}fun onSayHello() {// 真正初始化将会在这执行!!messageView.text = "Hello"}
}

现在,我们可以声明一个只读属性,而不必担心messageView的初始化点的问题。让我们看看懒加载背后原理是怎么样的。

属性委托(代理)

Delegation的意思就是委托。它意味着通过委托者可以执行某些操作,而不是直接通过原始访问者执行操作。

属性委托是委托属性的getter/setter方法,它允许委托对象在读取和写入值时插入执行一些中间操作。

Kotlin将支持接口(类委托)或访问器(委托属性)的实现委托给另一个对象。


Delegation is a something with a historic background. ? (Source: Wikipedia commons )

你可以通过by <delegate>形式来声明一个委托属性

val / var <property name>: <Type> by <delegate>

属性的委托可以像如下方式定义:

class Delegate {operator fun getValue(thisRef: Any?,property: KProperty<*>): String {// return value}operator fun setValue(thisRef: Any?,property: KProperty<*>, value: String) {// assign}
}

对值的所有读取操作都会委托调用getValue()方法,同理,对值的所有写操作都会委托调用setValue()方法。

by lazy的工作原理

现在让我们再次重新研究下上述例子中属性的代码。

它实际上就是一个属性委托!

我们可以把by lazy修饰的属性理解为是具有lazy委托的委托属性。

所以,lazy是如何工作的呢? 让我们一起在Kotlin标准库参考中总结lazy()方法,如下所示:

  • 1、lazy() 返回的是一个存储在lambda初始化器中的Lazy<T>类型实例。
  • 2、getter的第一次调用执行传递给lazy()的lambda并存储其结果。
  • 3、后面的话,getter调用只返回存储中的值。

简单地说,lazy创建一个实例,在第一次访问属性值时执行初始化,存储结果并返回存储的值。

带有lazy()的委托属性

让我们编写一个简单的Kotlin代码来检查lazy的实现。

class Demo {val myName: String by lazy { "John" }
}

如果你将其反编译为Java代码,则可以看到以下代码:

public final class Demo {@NotNullprivate final Lazy myName$delegate;// $FF: synthetic fieldstatic final KProperty[] $$delegatedProperties = ...@NotNullpublic final String getMyName() {Lazy var1 = this.myName$delegate;KProperty var3 = $$delegatedProperties[0];return (String)var1.getValue();}public Demo() {this.myName$delegate =LazyKt.lazy((Function0)null.INSTANCE);}
}
  • $delegate后缀被拼接到字段名称后面: myName$delegate
  • 注意myName$delegate的类型是Lazy类型不是String类型
  • 在构造器中,LazyKt.lazy()函数返回值赋值给了myName$delegate
  • LazyKt.lazy()方法负责执行指定的初始化块

调用getMyName()方法实际过程是将通过调用myName$delegateLazy
实例中的getValue()方法并返回相应的值。

Lazy的具体实现

lazy()方法返回的是一个Lazy<T>类型的对象,该对象处理lambda函数(初始化程序块),根据线程执行模式(LazyThreadSafetyMode)以稍微几种不同的方式执行初始化。

@kotlin.jvm.JvmVersion
public fun <T> lazy(mode: LazyThreadSafetyMode,initializer: () -> T
): Lazy<T> =when (mode) {LazyThreadSafetyMode.SYNCHRONIZED ->SynchronizedLazyImpl(initializer)LazyThreadSafetyMode.PUBLICATION ->SafePublicationLazyImpl(initializer)LazyThreadSafetyMode.NONE ->UnsafeLazyImpl(initializer)}

所有这些都负责调用给定的lambda块进行延迟初始化

SYNCHRONIZEDSynchronizedLazyImpl

  • 初始化操作仅仅在首先调用的第一个线程上执行
  • 然后,其他线程将引用缓存后的值。
  • 默认模式就是(LazyThreadSafetyMode.SYNCHRONIZED)

PUBLICATIONSafePublicationLazyImpl

  • 它可以同时在多个线程中调用,并且可以在全部或部分线程上同时进行初始化。
  • 但是,如果某个值已由另一个线程初始化,则将返回该值而不执行初始化。

NONEUnsafeLazyImpl

  • 只需在第一次访问时初始化它,或返回存储的值。
  • 不考虑多线程,所以它不是线程安全的。
Lazy实现的默认行为

SynchronizedLazyImplSafePublicationLazyImplUnsafeLazyImpl通过以下过程执行延迟初始化。我们来看看前面的例子。

  • 1、将传入的初始化lambda块存储在属性的 initializer

  • 2、通过属性_value来存储值。此属性最开始初始值为UNINITIALIZED_VALUE

  • 3、在执行读取操作(属性get访问器)时,如果_value的值是最开始初始值UNINITIALIZED_VALUE,那么就会去执行 initializer初始化器

  • 4、在执行读取操作(属性get访问器)时,如果_value的值不是等于UNINITIALIZED_VALUE, 那就说明初始化操作已经执行完成了。

SynchronizedLazyImpl

如果你没有明确指定具体模式,延迟具体实现就是SynchronizedLazyImpl,它默认只执行一次初始化。我们来看看它的实现代码。

private object UNINITIALIZED_VALUE
private class SynchronizedLazyImpl<out T>(initializer: () -> T,lock: Any? = null
) : Lazy<T>,Serializable {private var initializer: (() -> T)? = initializer@Volatile private var _value: Any? = UNINITIALIZED_VALUE// final field is required// to enable safe publication of constructed instanceprivate val lock = lock ?: thisoverride val value: Tget() {val _v1 = _valueif (_v1 !== UNINITIALIZED_VALUE) {@Suppress("UNCHECKED_CAST")return _v1 as T}return synchronized(lock) {val _v2 = _valueif (_v2 !== UNINITIALIZED_VALUE) {@Suppress("UNCHECKED_CAST") (_v2 as T)}else {val typedValue = initializer!!()_value = typedValueinitializer = nulltypedValue}}}override fun isInitialized(): Boolean =_value !== UNINITIALIZED_VALUEoverride fun toString(): String =if (isInitialized()) value.toString()else "Lazy value not initialized yet."private fun writeReplace(): Any =InitializedLazyImpl(value)
}

这看起来有点复杂。但它只是多线程的一般实现。

  • 使用synchronized()同步块执行初始化块
  • 由于初始化可能已经由另一个线程完成,它会进行双重锁检测(DCL),如果已经完成了初始化,则返回存储的值。
  • 如果它未初始化,它将执行lambda表达式并存储返回值。那么随后这个initializer将会置为null,因为初始化完成后就不再需要它了。

Kotlin的委托属性将会让你快乐

当然,延迟初始化有时会导致问题发生或通过绕过控制流并在异常情况下生成正常值来使调试变得困难。

但是,如果你对这些情况非常谨慎,那么Kotlin的延迟初始化可以使我们更加自由地避免对线程安全性和性能的担忧。

我们还研究了延迟初始化是运算符 bylazy函数的共同作用的结果。还有更多的委托,如ObservablenotNull。如有必要,你还可以实现有趣的委托属性。

读者有话说

闲聊几句,有一些小伙伴在私下问我对近期2019 Google IO 大会宣布Kotlin为Android开发首选语言,即Kotlin-First.这件事怎么看? 其实个人对于这个比较平常心,觉得不是什么特别新奇的事,说真的Kotlin自身有能力和优势充当这么个角色。如果你还在犹豫要不要学Kotlin这门语言的时候,那你就去看看Google IO使用Android代码例子几乎全是Kotlin编写,而且后面很多官方的框架和库都是Kotlin编写。咱们还是好好扎实把Kotlin中每个细节点吃透吧,那么你就会真正领会到Kotlin这门语言的设计哲学了。

说下为什么要翻译这篇文章?

这篇文章可以说把by lazy 属性懒加载的从使用场景到原理剖析都阐述的非常清楚,并以图表形式把by lazy调用过程逻辑呈现得简单易懂。

你是否在Kotlin代码使用过by lazy呢?那你知道什么时候该使用lateinit,什么时候使用by lazy吗?实际上这篇文章已经给出答案了,这里简单大致说下: by lazy正如文章中说的那样用于非空只读属性,需要延迟加载情况,而lateinit一般用于非空可变属性,需要延迟加载情况。后续会有相关文章详细阐述它们区别以及使用场景。

其实这篇文章还涉及到一个点,那就是 by lazy中的SynchronizedLazyImpl,实际上通过反编译后代码可知它是DCL(double check lock),所以可以利用Companion Object + by lazy可以实现Kotlin中的DCL单例模式。具体可参考我之前那篇文章当Kotlin完美邂逅设计模式之单例模式(一)

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~

Kotlin系列文章,欢迎查看:

Kotlin邂逅设计模式系列:

  • 当Kotlin完美邂逅设计模式之单例模式(一)

数据结构与算法系列:

  • 每周一算法之二分查找(Kotlin描述)
  • 每周一数据结构之链表(Kotlin描述)

翻译系列:

  • [译] Kotlin中关于Companion Object的那些事
  • [译]记一次Kotlin官方文档翻译的PR(内联类)
  • [译]Kotlin中内联类的自动装箱和高性能探索(二)
  • [译]Kotlin中内联类(inline class)完全解析(一)
  • [译]Kotlin的独门秘籍Reified实化类型参数(上篇)
  • [译]Kotlin泛型中何时该用类型形参约束?
  • [译] 一个简单方式教你记住Kotlin的形参和实参
  • [译]Kotlin中是应该定义函数还是定义属性?
  • [译]如何在你的Kotlin代码中移除所有的!!(非空断言)
  • [译]掌握Kotlin中的标准库函数: run、with、let、also和apply
  • [译]有关Kotlin类型别名(typealias)你需要知道的一切
  • [译]Kotlin中是应该使用序列(Sequences)还是集合(Lists)?
  • [译]Kotlin中的龟(List)兔(Sequence)赛跑

原创系列:

  • 教你如何完全解析Kotlin中的注解
  • 教你如何完全解析Kotlin中的类型系统
  • 如何让你的回调更具Kotlin风味
  • Jetbrains开发者日见闻(三)之Kotlin1.3新特性(inline class篇)
  • JetBrains开发者日见闻(二)之Kotlin1.3的新特性(Contract契约与协程篇)
  • JetBrains开发者日见闻(一)之Kotlin/Native 尝鲜篇
  • 教你如何攻克Kotlin中泛型型变的难点(实践篇)
  • 教你如何攻克Kotlin中泛型型变的难点(下篇)
  • 教你如何攻克Kotlin中泛型型变的难点(上篇)
  • Kotlin的独门秘籍Reified实化类型参数(下篇)
  • 有关Kotlin属性代理你需要知道的一切
  • 浅谈Kotlin中的Sequences源码解析
  • 浅谈Kotlin中集合和函数式API完全解析-上篇
  • 浅谈Kotlin语法篇之lambda编译成字节码过程完全解析
  • 浅谈Kotlin语法篇之Lambda表达式完全解析
  • 浅谈Kotlin语法篇之扩展函数
  • 浅谈Kotlin语法篇之顶层函数、中缀调用、解构声明
  • 浅谈Kotlin语法篇之如何让函数更好地调用
  • 浅谈Kotlin语法篇之变量和常量
  • 浅谈Kotlin语法篇之基础语法

Effective Kotlin翻译系列

  • [译]Effective Kotlin系列之考虑使用原始类型的数组优化性能(五)
  • [译]Effective Kotlin系列之使用Sequence来优化集合的操作(四)
  • [译]Effective Kotlin系列之探索高阶函数中inline修饰符(三)
  • [译]Effective Kotlin系列之遇到多个构造器参数要考虑使用构建器(二)
  • [译]Effective Kotlin系列之考虑使用静态工厂方法替代构造器(一)

实战系列:

  • 用Kotlin撸一个图片压缩插件ImageSlimming-导学篇(一)
  • 用Kotlin撸一个图片压缩插件-插件基础篇(二)
  • 用Kotlin撸一个图片压缩插件-实战篇(三)
  • 浅谈Kotlin实战篇之自定义View图片圆角简单应用

这篇关于[译]带你揭开Kotlin中属性代理和懒加载语法糖衣的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1111957

相关文章

高效+灵活,万博智云全球发布AWS无代理跨云容灾方案!

摘要 近日,万博智云推出了基于AWS的无代理跨云容灾解决方案,并与拉丁美洲,中东,亚洲的合作伙伴面向全球开展了联合发布。这一方案以AWS应用环境为基础,将HyperBDR平台的高效、灵活和成本效益优势与无代理功能相结合,为全球企业带来实现了更便捷、经济的数据保护。 一、全球联合发布 9月2日,万博智云CEO Michael Wong在线上平台发布AWS无代理跨云容灾解决方案的阐述视频,介绍了

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

proxy代理解决vue中跨域问题

vue.config.js module.exports = {...// webpack-dev-server 相关配置devServer: {host: '0.0.0.0',port: port,open: true,proxy: {'/api': {target: `https://vfadmin.insistence.tech/prod-api`,changeOrigin: true,p

C++语法知识点合集:11.模板

文章目录 一、非类型模板参数1.非类型模板参数的基本形式2.指针作为非类型模板参数3.引用作为非类型模板参数4.非类型模板参数的限制和陷阱:5.几个问题 二、模板的特化1.概念2.函数模板特化3.类模板特化(1)全特化(2)偏特化(3)类模板特化应用示例 三、模板分离编译1.概念2.模板的分离编译 模版总结 一、非类型模板参数 模板参数分类类型形参与非类型形参 非类型模板

Java基础回顾系列-第一天-基本语法

基本语法 Java基础回顾系列-第一天-基本语法基础常识人机交互方式常用的DOS命令什么是计算机语言(编程语言) Java语言简介Java程序运行机制Java虚拟机(Java Virtual Machine)垃圾收集机制(Garbage Collection) Java语言的特点面向对象健壮性跨平台性 编写第一个Java程序什么是JDK, JRE下载及安装 JDK配置环境变量 pathHe

HTML5自定义属性对象Dataset

原文转自HTML5自定义属性对象Dataset简介 一、html5 自定义属性介绍 之前翻译的“你必须知道的28个HTML5特征、窍门和技术”一文中对于HTML5中自定义合法属性data-已经做过些介绍,就是在HTML5中我们可以使用data-前缀设置我们需要的自定义属性,来进行一些数据的存放,例如我们要在一个文字按钮上存放相对应的id: <a href="javascript:" d

Python中的属性装饰器:解锁更优雅的编程之道

引言 在Python的世界里,装饰器是一个强大的工具,它允许我们以一种非侵入性的方式修改函数或方法的行为。而当我们谈论“属性装饰器”时,则是在探讨如何使用装饰器来增强类中属性的功能。这不仅让我们的代码更加简洁、易读,同时也提供了强大的功能扩展能力。本文将带你深入了解属性装饰器的核心概念,并通过一系列实例展示其在不同场景下的应用,从基础到进阶,再到实际项目的实战经验分享,帮助你解锁Python编程

Hibernate框架中,使用JDBC语法

/*** 调用存储过程* * @param PRONAME* @return*/public CallableStatement citePro(final String PRONAME){Session session = getCurrentSession();CallableStatement pro = session.doReturningWork(new ReturningWork<C