Bug or Feature: Kotlin allows to change 'val&#

2020-02-26 05:07发布

问题:

I just started to explore the language Kotlin. I'm struggling with inheritance, var&val and side-effects.

If I declare a trait A with a val x and override x in AImpl it is possible to override it as var (see code below). Surprisingly the print() method in A is affected by the reassignment of x even though x is a value in A. Is this a bug or a feature?

Code:

trait A {
  fun print() {
    println("A.x = $x")
  }

  val x : Int;
}

class AImpl(x : Int) : A {
  override var x = x; // seems like x can be overriden as `var`
}

fun main(args: Array<String>) {

  val a = AImpl(2)

  a.print() // A.x = 2

  a.x = 3; // x can be changed

  // even though print() is defined in trait A
  // where x is val it prints x = 3
  a.print() // A.x = 3

}

I'm aware of the fact that if I define a with type A explicitly it is not allowed to change x:

val a = AImpl(2) : A
a.x = 3 // ERROR: value x cannot be reassigned

But as the first case shows, inheritance can cause side effects which are clearly not intended in A. How do I protect values from being changed by inheritance?

回答1:

You can make your val final, i.e. forbid overriding it at all. If you define a val in a class, it is final by default.

Also, if you need to override a val with a var, but do not want the setter to be public, you can say so:

override var x = 1
    private set

Overriding a val with a var is a feature. It is equivalent to adding a set-method while in the superclass there was only a get-method. And this is rather important in implementing some patterns, such as read-only interfaces.

There's no way to "protect" your val from being overridden in a way that allows changing mutation other than making it final, because val does not mean "immutable reference", but merely "read-only property". In other words, when your trait A declares a val, it means that through a reference of type A the client can not write this val, no other guarantees intended, or indeed possible.

P.S. Semicolons are optional in Kotlin, feel free to omit them altogether



回答2:

I would consider this a feature, as changing val to var imposes weaker usage restrictions and can't break any superclass code. Similar situation can be observed with visibility modifiers:

trait A {
  protected fun print() {
    ...
  }
}

class AImpl: A {
  public override fun print() {
    ...
  }
}

In this example visibility restrictions are also relaxed by a subclass, although some people are considering this technique as an antipattern.

How do I protect values from being changed by inheritance?

In kotlin you can explicitly define if any particular class member can be overriden by a subclass using open modifier. In traits, however, all the members are open by default. The solution is to replace trait with class, so you'll be able to control inheritance:

abstract class A {
  fun print() {
    ...
  }

  val x : Int = 2;
}

class AImpl(x : Int) : A() {
  override var x = x // compilation error
}