什么是 Kotlin 后备字段?

What's Kotlin Backing Field For?

作为一名 Java 开发人员,支持字段的概念对我来说有点陌生。鉴于:

class Sample {
    var counter = 0 // the initializer value is written directly to the backing field
    set(value) {
        if (value >= 0) field = value
    }
}

这个支持字段有什么用? Kotlin docs 说:

Classes in Kotlin cannot have fields. However, sometimes it is necessary to have a backing field when using custom accessors.

为什么?在 setter 中使用属性名称本身有什么区别,例如 *

class Sample {        
    var counter = 0
    set(value) {
        if (value >= 0) this.counter = value // or just counter = value?
    }
}

因为,如果您没有 field 关键字,您实际上将无法 set/get get()set(value) 中的值。它使您能够访问自定义访问器中的支持字段。

这是您示例的等效 Java 代码:

class Sample {
    private int counter = 0;
    public void setCounter(int value) {
        if (value >= 0) setCounter(value);
    }
    public int getCounter() {
        return counter;
    }
}

显然这不好,因为 setter 只是对自身的无限递归,从不改变任何东西。请记住,在 kotlin 中,每当您编写 foo.bar = value 时,它都会被转换为 setter 调用而不是 PUTFIELD.


编辑:Java 有 字段 而 Kotlin 有 属性 ,这是比字段更高级别的概念。

有两种类型的属性:一种有支持字段,一种没有。

具有支持字段的 属性 将以字段的形式存储值。该字段使在内存中存储值成为可能。 属性 的一个例子是 Pairfirstsecond 属性。 属性 将更改 Pair.

在内存中的表示

没有支持字段的 属性 将不得不以其他方式存储它们的值,而不是直接将其存储在内存中。它必须根据其他属性或对象本身计算得出。 属性 的一个例子是 Listindices 扩展 属性,它没有字段支持,而是基于 size 属性。所以它不会改变 List 的内存表示(它根本不能这样做,因为 Java 是静态类型的)。​​

支持字段适用于 运行 验证或触发状态更改事件。想一想您向 Java setter/getter 添加代码的次数。支持字段在类似情况下会很有用。当您需要控制或查看 setters/getters.

时,您可以使用支持字段

使用字段名称本身分配字段时,您实际上是在调用 setter(即 set(value))。在您的示例中, this.counter = value 将递归到 set(value) 直到我们溢出堆栈。使用 field 绕过 setter(或 getter)代码。

最初,我也很难理解这个概念。所以让我用一个例子向你解释一下。

考虑这个 Kotlin class

class DummyClass {
    var size = 0;
    var isEmpty
        get() = size == 0
        set(value) {
            size = size * 2
        }
}

现在,当我们查看代码时,我们可以看到它有 2 个属性,即 - size(使用默认访问器)和 isEmpty(使用自定义访问器)。但它只有 1 个字段,即 size。要了解它只有 1 个字段,让我们看一下 Java 等效于此 class.

转到工具 -> Kotlin -> 在 Android Studio 中显示 Kotlin 字节码。点击反编译。

   public final class DummyClass {
   private int size;

   public final int getSize() {
      return this.size;
   }

   public final void setSize(int var1) {
      this.size = var1;
   }

   public final boolean isEmpty() {
      return this.size == 0;
   }

   public final void setEmpty(boolean value) {
      this.size *= 2;
   }
}

很明显我们可以看到javaclass对于isEmpty只有getter和setter两个函数,并且没有为其声明字段.同样,在 Kotlin 中,属性 isEmpty 没有支持字段,因为 属性 根本不依赖于该字段。因此没有支持字段。


现在让我们删除 isEmpty 属性 的自定义 getter 和 setter。

class DummyClass {
    var size = 0;
    var isEmpty = false
}

而上面 class 的 Java 等价于

public final class DummyClass {
   private int size;
   private boolean isEmpty;

   public final int getSize() {
      return this.size;
   }

   public final void setSize(int var1) {
      this.size = var1;
   }

   public final boolean isEmpty() {
      return this.isEmpty;
   }

   public final void setEmpty(boolean var1) {
      this.isEmpty = var1;
   }
}

这里我们看到字段 sizeisEmptyisEmpty 是支持字段,因为 isEmpty 属性 的 getter 和 setter 依赖于它。

我的理解是使用 field 标识符作为对 属性 在 get 或 [=18 中的值的引用=]set,当你想在 getset 中更改或使用 属性 的值时。

例如:

class A{
    var a:Int=1
        get(){return field * 2}    // Similiar to Java: public int geta(){return this.a * 2}
        set(value) {field = value + 1}
}

然后:

var t = A()
println(t.a)    // OUTPUT: 2, equal to Java code: println(t.a * 2)
t.a = 2         // The real action is similar to Java code: t.a = t.a +1
println(t.a)    // OUTPUT: 6, equal to Java code: println(t.a * 2)

术语backing field充满了神秘色彩。使用的关键字是 fieldget/set方法,紧跟在即将通过此门保护方法机制getset的成员变量之后。 field关键字只是引用要setget的成员变量。目前Kotlin,你不能直接在getset保护门方法中引用成员变量,因为它会不幸地导致无限递归,因为它将重新调用 getset 从而导致运行时陷入深渊。

C#中,你可以直接在getter/setter方法中引用成员变量。我引用这个比较来提出这个 field 关键字是当前 Kotlin 实现它的方式的想法,但我希望它会在以后的版本中被删除,并允许我们直接引用成员变量而不会导致无限递归。