1

I have read that using !! should generally be avoided. Is there a way to write the following code in a more elegant way without having to add something like obsolete null checks and duplicated or dead blocks of code?

class A(var field: Thing?) {
    fun getField(): Thing {
        if (field == null) {
            field = Thing()
        }
        return field!!
    }
}

Also I don't understand why the compiler requires the !!-'pray-this-isn't-null-operator' to be satisfied in this scenario.

EDIT: Consider that it is important to me that a potential solution uses lazy initialization if the field is null!

maths.js
  • 33
  • 6

3 Answers3

2

Problem

As Enzokie already mentioned in the comments, another thread could have changed field after the null check. The compiler has no way of knowing that, so you have to tell it.

class A(var field: Thing?) {

    fun getField(): Thing {
        if (field == null) {
            field = Thing()
        }

        // another thread could have assigned null to field

        return field!! // tell the compiler: I am sure that did not happen
    }
}

Solution (Eager)

In you particular case it would be a good idea to use a parameter f (you could name it "field" too, but I avoided that for clarity) in the constructor (without val/var) and afterwards assign it to a property field to which you assign either f or a new instance of Thing.

This can be expressed really concise with the Elvis operator :? which takes the left hand side if not null and the right hand side of the expression otherwise. So, in the end field will be of type Thing.

class A(f: Thing?) {
    val field = f ?: Thing() // inferred type Thing
}

Solution (Lazy)

Since it was mentioned by gidds, if you need to initialize field lazyly you could do it like this using delegated properties:

class A(f: Thing?) {
    val field by lazy {
        f ?: Thing() // inferred type Thing
    }
}

The call site does not change:

val a = A(null) // field won't be initialized after this line...
a.field // ... but after this
Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
  • 1
    I'm guessing there's a typo and you meant `val field = f ?: Thing()` in the last line...? – gidds Jan 26 '19 at 09:48
  • 2
    Cool. Might also be worth pointing out that the nice concise solution changes the initialisation from lazy (first time the property is accessed) to eager (when the instance is first constructed). In most cases this isn't significant; but if `Thing()` was e.g. a database connection that wasn't always needed, eager initialisation might cause unnecessary work. If so, another approach might be to use a `by lazy` delegate -- that would be simple and concise *and* take away the subtle issues around threading and correctness. – gidds Jan 26 '19 at 12:11
  • 1
    I like your explanation and your effort put into providing an alternative solution, but in this particular case, I was indeed looking for a lazy solution. The 'Things' in my project potentially have a very large tail of associated data, stuff that isn't needed before the element is accessed for the first time. I will try to figure out what @gidds meant with 'by lazy delegate'. – maths.js Jan 26 '19 at 14:22
  • @gidds even before op asked I wanted to incorparate your idea but now op even explicitely asked about it. thanks for your comment :) – Willi Mentzel Jan 26 '19 at 14:29
1

How about this?

class A(field: Thing?) {

    private lateinit var field: Thing

    init {
        field?.let { this.field = it }
    }

    fun getField(): Thing {
        if (!this::field.isInitialized) {
            field = Thing()
        }
        return field
    }
}
SuuSoJeat
  • 1,086
  • 1
  • 8
  • 18
1

When you define a field, you actually define a variable plus two accessor methods:

val counter: Integer = 0

It is possible to customize the accessor methods by writing this instead:

val n = 0
val counter: Integer
    get() = n++

This will execute the n++ each time you access the counter field, which therefore returns different values on each access. It is uncommon and unexpected but technically possible.

Therefore the Kotlin compiler cannot assume that two accesses to the same field return the same value twice. Usually they do, but it is not guaranteed.

To work around this, you can read the field once by copying it into a local variable:

fun count() {
    val counter = counter
    println("The counter is $counter, and it is still $counter.")
}
Roland Illig
  • 40,703
  • 10
  • 88
  • 121