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?
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.
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With