10

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?

WSBT
  • 33,033
  • 18
  • 128
  • 133
  • 1
    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 Sep 27 '19 at 00:31
  • 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 Sep 27 '19 at 07:04
  • 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 Sep 27 '19 at 21:00
  • 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 Sep 28 '19 at 07:12
  • 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 Sep 30 '19 at 01:04
  • @MarcPlano-Lesay `lateinit` modifier is not allowed on properties with a custom getter or setter; `lazy` would only execute once so the value will not change on subsequent calls. I still don't think this whole wall of texts will help further readers. I will delete all mine and you can leave a more concise comment if you would like. – WSBT Sep 30 '19 at 01:08

2 Answers2

5

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.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
3

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.

Geno Chen
  • 4,916
  • 6
  • 21
  • 39
  • KT-16681 talks about reassigning the backing field in a getter, e.g. `field = "changed"`. But in my example, no one is reassigning the backing field. The `counter++` statement could as well happen in `main()` function or any other parts of the program. – WSBT Dec 10 '18 at 03:21
  • @user1032613 I updated my answer, but I am not sure if this update fits your question. – Geno Chen Dec 10 '18 at 03:47
  • This is definitely a bug and still exists in the kotlin version 1.8.10. I don't care how it is implemented--I want consistency: **vals** should be immutable. – SMBiggs Feb 18 '23 at 20:42