9
votes

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?

2
While the answers are explaining why it's allowed and how it works, none of them gave the correct answer: you should not do that in the first place. Your foo property is not a property, but a function with side effects. By doing something like that, you're breaking all the assumptions that users of your property can make.Marc Plano-Lesay
Using either var or val should only affect foo, not anything else. You should not use that, and that's why you have this question in the first place. Saying "I wanted to use var" is just wanting to go against the idioms of the language.Marc Plano-Lesay
That's exactly my point. There's nothing wrong in implementing List.size as a property, because it doesn't have any side effects. Implementing List.pop as a property, on the other hand, wouldn't make any sense, because it does alter the state of the list just by reading it. Which is exactly what you're describing here: reading foo would change counter.Marc Plano-Lesay
I obviously can't speak for the language designers, but something like DateTime.now would be fine as a property, in my opinion. The difference between this and foo in your question being that while DateTime.now's value change over time, it's not altered by the fact that you read it or not (in addition to not changing anything else either, which is even worse).Marc Plano-Lesay
I think from your comment that what you're looking for might be a lazy property or the lateinit keyword. (I think that this conversation might actually have some value to any potential reader confused about val, var, and properties in general)Marc Plano-Lesay

2 Answers

5
votes

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.

2
votes

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.