[译]Kotlin的属性委托和惰性初始化是如何工作

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

Kotlin的属性委托和惰性初始化是如何工作

访问属性在支持面向对象范式的编程语言中非常常见。Kotlin也提供了许多类似的方法,by lazy进行惰性初始化就是一个很好的例子

在本文中,我们将看看如何使用Kotlin的委托来处理属性,以及by lazy的惰性初始化,然后深入了解它们的工作方式。

Nullable 类型

我认为你们中的许多人可能已经知道nullable,但让我们再来看看它。使用Kotlin的Android组件代码可以写成:

class MainActivity : AppCompatActivity() {
    private var helloMessage : String = "Hello"
}

在自己的生命周期初始化和Nullable类型

在上面的例子中,如果在创建对象时可以进行初始化,没有什么大问题。但是,如果在特定的初始化过程之后引用它,则不能预先声明和使用某个值,因为它有自己的生命周期来初始化自己。

我们来看看一些熟悉的Java代码。

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

可以通过声明为nullable类型来编写Kotlin代码,如下所示。

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
    }
}

Non-null类型

上面的代码运行良好,但是在使用属性之前每次检查它是否为null都有点繁琐。可以通过使用一个非null类型来忽略它(你相信)它总是有一个值。

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

当然,需要使用lateinit说明之后会给这个控件赋值

lateinit:我将之后初始化非null属性

与我们通常谈论的延迟初始不同,lateinit允许编译器识别非null属性的值未存储在构造器阶段中以便正常编译。

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

更多详情见这里

只读属性

通常,如果组件的字段不是基本类型或者内置类型,则可以看到引用保存在组建的整个生命周期中。

例如,在Android应用程序中,大多数控件引用在activity的生命周期中保持不变。 换句话说,这意味着很少需要改变分配的引用。

在这一点上,我们可以轻松有以下想法:

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

我想是这样。要做到这一点,乍一看,只需要一点努力就可以用val替换var

只读属性的非null的困境

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

class MainActivity : AppCompatActivity() {
    private val mWelcomeTextView: TextView
    override 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
    }
}

现在让我们试着解决最后的问题:

“如何使用之后分配只读属性”

惰性初始化

在Kotlin中,对只读属性执行延迟初始化时by lazy可能非常有用。

by lazy { ... }在属性第一次使用时执行初始化,而不是声明时。

class MainActivity : AppCompatActivity() {
    private val messageView : TextView by lazy {
        // runs on first access of messageView
        findViewById(R.id.message_view) as TextView
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
    fun onSayHello() {
        // Initialization would be run at here!!
        messageView.text = "Hello"
    }
}

现在,我们可以声明一个只读属性,而不用担心messageView的初始化位置。让我们看看by lazy如何工作的。

代理属性 101

代表团的字面意思是委派。这意味着委托人可以替代原来的访问者执行一些操作。

委托属性代理了属性的getter/setter,它允许代理对象在读写值时执行一些内部操作。

Kotlin支持将接口(Class Delegation)或访问器(Delegated Properties)的实现委托给另一个对象。更多细节将在另一篇文章中介绍。:)

)](https://cdn-images-1.medium.com/max/1600/1*EThddmBwZW8EZT-JucFHcg.jpeg)

可以紧跟by 格式声明一个代理属性:

val / var \

`by lazy`如何工作的

现在让我们再次访问该属性的代码。

我们可以认为by lazy将一个属性作为带lazy委托的委托属性。

因此,lazy是如何工作的?让我们总结一下Kotlin标准库引用的lazy(),如下所示:

  1. lazy()返回Lazy示例,该示例储存了lambda初始化器。
  2. 第一次调用getter执行传递给lazy()的lambda表达式,并储存表达式的结果。
  3. 之后,getter返回储存的值。

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

带lazy()的代理属性

让我们写一个见得Kotlin代码来检查lazy的实现。

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

如果反编译成Java代码,可以看到如下代码:

public final class Demo {
    @NotNull
    private final Lazy myName$delegate;

    // $FF: synthetic field
    static final KProperty[] $$delegatedProperties = ...
    @NotNull
    public 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$delegategetValue()方法返回Lazy实例的值。

Lazy实现

lazy返回Lazy对象,该对象根据线程执行模型(LazyThreadSafetyMode)以些许不同的方式调用lambda方法(初始化器)执行初始化操作。

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

他们都负责调用给定的lambda块进行延迟初始化。

SYNCHRONIZED → SynchronizedLazyImpl

  • 仅在第一个调用的线程执行初始化。
  • 其他线程接着获得缓存值。
  • 默认模式(LazyThreadSafetyMode.SYNCHRONIZED)

PUBLICATION → SafePublicationLazyImpl

  • 它可以同时从多个线程中调用,并且初始化可以在全部或部分线程上同时完成。
  • 然而,如果值已经被其他线程初始化,它将直接放而不执行初始化操作。

NONE → UnsafeLazyImpl
* 只需在第一次访问时初始化,或者返回存储的值。
* 没有考虑多线程,所以它是不安全的。

Lazy实现的默认行为

SynchronizedLazyImplSafePublicationLazyImplUnsafeLazyImpl 通过以下流程执行惰性初始化。让我们看看前面的例子。

  1. 属性的initializer存储传入的初始化lambda表达式。

  2. 通过_value属性存储值。这个属性的 初始值是UNINITIALIZED_VALUE

  3. 如果读操作时,_value的值是UNINITIALIZED_VALUE,则执行initializer表达式。
  4. 如果_valueUNINITIALIZED_VALUE,读操作将返回_value,因为初始化已经完成了。

SynchronizedLazyImpl

如果不指定模式,惰性实现则使用SynchronizedLazyImpl,该模式只执行一次初始化操作。让我们看看它的实现代码。

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

看起来有些复杂。但它与多线程实现方式相同。

  • synchronized()执行初始化块。
  • 由于其他线程可能已经完成了初始化操作,因此它执行了双重检查。如果初始化已经完成,它直接返回存储的值。
  • 如果没有初始化,他讲执行lambda表达式并存储返回值。接着initializer赋值为null,因为初始化完成后不再需要它了。

Kotlin的代理属性

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

但是,如果您对这些情况非常小心,那么Kotlin的惰性初始化可以让我们免于担心线程安全和性能问题。

我们还确认了惰性初始化是运算符和惰性函数的结果。还有更多的代理方式,比如ObservablenotNull。如果有必要,你也可以实现有趣的委托属性。请享用吧!

你可能感兴趣的:(Android)