Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin custom Getter makes `val` and `var` confusing?

Tags:

kotlin

In Kotlin, var is mutable and val should be assigned only once.

However, consider val foo in the following example:

var counter = 0

val foo: String
  get(){
    counter++
    return "val$counter"
  }

fun main(): String {
    val a = foo
    val b = foo
    val c = foo
    return "we got: $a $b $c"
    // output: we got: val1 val2 val3
}

The get() method is executed each time we try to access foo, resulting different values for val.

Since the value of foo is changing, I tried to use var. The compiler then complained about "Property must be initialized". So I had to give it a default value:

var foo: String = "default value that will never be used"
  get(){
    counter++
    return "val$counter"
  }

I don't like either approach here. What's the correct practice?

like image 335
user1032613 Avatar asked Dec 10 '18 02:12

user1032613


2 Answers

In Kotlin, var is mutable and val should be assigned only once.

For local variables, yes. For properties, not really: val means "only has a getter", var means "has both a getter and a setter". This getter (and setter) is allowed to do pretty much anything. You could just return a random value each time, for example.

An exception is reassigning the backing field for a val:

val foo: Int = 0
  get(){
    field++
    return field
  }

won't compile.

like image 186
Alexey Romanov Avatar answered Oct 24 '22 10:10

Alexey Romanov


This is already reported in YouTrack, as KT-16681, "kotlin allows mutating the field of read-only property".

As you can see in the reply in KT-16681, the custom getter is compiled into another function, which makes field foo and method getFoo() become two unrelated things.

Also from the reply in KT-16681, this kind of violation (Reassignment of read-only property via backing field) will produce an error since Kotlin 1.3.

Update: In comment, the original poster mentioned that KT-16681 is different from this question. However, inspired from that issue, we can see Kotlin bytecode there by Tools -> Kotlin -> Show Kotlin Bytecode (removed metadata etc.):

public final class Test53699029Kt {


  // access flags 0xA
  private static I counter

  // access flags 0x19
  public final static getCounter()I
   L0
    LINENUMBER 3 L0
    GETSTATIC Test53699029Kt.counter : I
    IRETURN
   L1
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x19
  public final static setCounter(I)V
   L0
    LINENUMBER 3 L0
    ILOAD 0
    PUTSTATIC Test53699029Kt.counter : I
    RETURN
   L1
    LOCALVARIABLE <set-?> I L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x19
  public final static getFoo()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 7 L0
    GETSTATIC Test53699029Kt.counter : I
    DUP
    ISTORE 0
    ICONST_1
    IADD
    PUTSTATIC Test53699029Kt.counter : I
   L1
    LINENUMBER 8 L1
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "val"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    GETSTATIC Test53699029Kt.counter : I
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ARETURN
   L2
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x19
  public final static main()V
   L0
    LINENUMBER 12 L0
    INVOKESTATIC Test53699029Kt.getFoo ()Ljava/lang/String;
    ASTORE 0
   L1
    LINENUMBER 13 L1
    INVOKESTATIC Test53699029Kt.getFoo ()Ljava/lang/String;
    ASTORE 1
   L2
    LINENUMBER 14 L2
    INVOKESTATIC Test53699029Kt.getFoo ()Ljava/lang/String;
    ASTORE 2
   L3
    LINENUMBER 15 L3
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "we got: "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    BIPUSH 32
    INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder;
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    BIPUSH 32
    INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3
   L4
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 3
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L5
   L6
    LINENUMBER 17 L6
    RETURN
   L7
    LOCALVARIABLE c Ljava/lang/String; L3 L7 2
    LOCALVARIABLE b Ljava/lang/String; L2 L7 1
    LOCALVARIABLE a Ljava/lang/String; L1 L7 0
    MAXSTACK = 2
    MAXLOCALS = 4

  // access flags 0x1009
  public static synthetic main([Ljava/lang/String;)V
    INVOKESTATIC Test53699029Kt.main ()V
    RETURN
    MAXSTACK = 0
    MAXLOCALS = 1

As we can see, there is no field for foo, just a getFoo(), comparing for a normal val declaration:

public final class Test53699029Kt {


  // access flags 0xA
  private static I counter

  // access flags 0x19
  public final static getCounter()I
   L0
    LINENUMBER 1 L0
    GETSTATIC Test53699029Kt.counter : I
    IRETURN
   L1
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x19
  public final static setCounter(I)V
   L0
    LINENUMBER 1 L0
    ILOAD 0
    PUTSTATIC Test53699029Kt.counter : I
    RETURN
   L1
    LOCALVARIABLE <set-?> I L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1A
  private final static Ljava/lang/String; foo = "aaa"
  @Lorg/jetbrains/annotations/NotNull;() // invisible

  // access flags 0x19
  public final static getFoo()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 3 L0
    GETSTATIC Test53699029Kt.foo : Ljava/lang/String;
    ARETURN
   L1
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x19
  public final static main()V
   L0
    LINENUMBER 6 L0
    GETSTATIC Test53699029Kt.foo : Ljava/lang/String;
    ASTORE 0
   L1
    LINENUMBER 7 L1
    GETSTATIC Test53699029Kt.foo : Ljava/lang/String;
    ASTORE 1
   L2
    LINENUMBER 8 L2
    GETSTATIC Test53699029Kt.foo : Ljava/lang/String;
    ASTORE 2
   L3
    LINENUMBER 9 L3
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "we got: "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    BIPUSH 32
    INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder;
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    BIPUSH 32
    INVOKEVIRTUAL java/lang/StringBuilder.append (C)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3
   L4
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 3
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L5
   L6
    LINENUMBER 11 L6
    RETURN
   L7
    LOCALVARIABLE c Ljava/lang/String; L3 L7 2
    LOCALVARIABLE b Ljava/lang/String; L2 L7 1
    LOCALVARIABLE a Ljava/lang/String; L1 L7 0
    MAXSTACK = 2
    MAXLOCALS = 4

  // access flags 0x1009
  public static synthetic main([Ljava/lang/String;)V
    INVOKESTATIC Test53699029Kt.main ()V
    RETURN
    MAXSTACK = 0
    MAXLOCALS = 1

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 3 L0
    LDC "aaa"
    PUTSTATIC Test53699029Kt.foo : Ljava/lang/String;
    RETURN
    MAXSTACK = 1
    MAXLOCALS = 0

using val foo = "aaa" will produce a normal final static String foo field and final static String getFoo() method, but using val foo: String with get() will not produce that field, just produce a method. This getter function is generated by Kotlin, I believe the lost of field comes from the lost of initial assignment in declaration of val, but I can't find the real documentation of that, in question like Getters and Setters in Kotlin just directly use this conclusion.

So, this seems a bypass for modification of final static.

Problems occur when an immutable field referenced a mutable field. val is not re-assignable, but it referenced a var, a re-assignable field, this results in the modification of val.

like image 35
Geno Chen Avatar answered Oct 24 '22 11:10

Geno Chen