Lateinit - The illusion of null safety?


#1

Hello,

I really like the null features of Kotlin but I’m wondering if the lateinit keyword is a backdoor that renders all the other null safety features moot?

Don’t get me wrong, I love the lateinit keyword, and it is hard to not have that feature if you want to be able to late bind, but failed late binding seems to be something that could replace null pointer exceptions as a gotcha in Kotlin.

I wonder if true null safety is possible at all and was failed lateinit bindings recognised as a weakness, and if so, can you think of a truly safe way to achieve late binding without simply deferring to a runtime check that may or may not fail?

Chris


#2

Well, as we’re often reminded, Kotlin is intended to be a pragmatic rather than a purely academic language and it seems to me that the existence of features such as 'lateinit' and '!!' is just an acknowledgement that sometimes the programmer will know better than the compiler about whether an object reference is going to be null or not. If the programmer is wrong about this then he/she will, of course, be punished with an exception.

It may be that the compiler could be made smarter as regards null checking but only at the expense of having complicated rules or flow analysis which will inevitably slow down the compilation process. So I think what we have at present is a reasonable balance which can be overridden using the aforementioned features when it seems appropriate to do so.

My personal view is that we’ll never be able to eliminate the need for null entirely. One thing I try and do (and I remember someone else saying the same on this forum recently) is to use references to ‘default’ objects rather than null whenever possible. However, this isn’t always feasible and, even where it is, it may be at the expense of creating an additional object which your program doesn’t otherwise need and which may cause problems if it is actually used.


#3

Late init is a powerfull idea but, if the programmer fail, the app fail… Is as array index bounds or others exceptions.

In some frameworks, the “contructors” is not a contructors, as Android Activity when the “contructors” is onCreate, so we need use late init and set the value in onCreate, but, if the programmer forget set the value…


#4

It can always happens that a lateinit fails, or is forgotten when code evolves.

For those cases it might be nice to be able to have some sort of assertion, either in the language or in the library, that can verify if at that point all lateinit’s have been initialized - so that at the end of your onCreate or other method, you can check with an assertion that everything is initialized, and the likelyhood is increased that such problem will be found during development / in-house testing rather than after the app goes live.


#5

I think that the main problem of null assignment is not null-s themselves, but a possibility that this null will be further assigned to some variable which was not intended to be null at first place. Sometimes it is very hard to track where this null was assigned in the first place. Lateinit could provide invalid state, but it does not generate this problem since you will get an error first time you are requesting its value, you can’t propagate this value further into the code.


#6

I think you mean “moot.”


#7

I think I do mean moot. Thanks for the correction :slight_smile:


#8

Basically this.

lateinit is a tool for the programmer, as is casting. You wouldn’t argue that casting is the illusion of type safe.


#9

I think @Tim_van_der_Leeuw.1 is on to something here. If we could have an annotation we could apply to a method (e.g., Android Activity.onCreate() method override) that would cause a RuntimeException to be thrown if lateinit vars aren’t defined yet, that would provide insulation against many possible failure modes with lateinit.


#10

@dpisoni Wouldn’t compile time error, or warning be better? (Or pre-compile even - a warning in the IDE if no assignments exist)


#11

An assignment might exist, but the path to the assignment might not be taken due to runtime conditions that the compiler cannot verify.

Also, with autowiring by units like Spring Framework, Guice etc, or entity loading by a JPA or other ORM, assignments might happen that are invisible to the compiler.

Thus trying to enforce this in the compiler can produce both false positives and false negatives.

As developer however you usually can set a point in code at which you know that object initialization must be finished, thus an assertion can be put there that all lateinit vars must be assigned or there’s a program error.


#12

Right. In the Android case the example that came to mind for me is a protected method in an abstract subclass of Activity: the descendent onCreate method would be responsible for defining the value of the lateinit var.

Reality is that applying this runtime check on a lifecycle method is almost as good as a compile-time check, since the lifecycle method would be called every time you used the component, so you would fail fast. (Just not quite as fast as a compiler/linter check.)


#13

Maybe a “constructor annotation” could be as alternative to late init in some cases.

class MyActivity : Activity {
  val foo: Bar  //Error, no initialized

  fun onCreate() {
     foo = Bar();
  }
}

class MyActivity : Activity {
  val foo: Bar  //Ok, is initialized in @Contructor function
  val baz: Bar //Error, no initialized

 @Constructor
  fun onCreate() {
     foo = Bar();
  }
}


#14

Maybe a “constructor annotation” could be as alternative to late init in some cases.

Construction is part of lifecycle of class object. Annotation does not guarantee that other methods or properties of class cannot be called before call annotated method.


#15

It will not help in cases like Spring or Guice framework autowiring your class dependencies in a way that’s happening totally outside your class, or Hibernate loading your object properties from database.
JPA and Spring do have their own annotations for methods to be called post-setup, so presumably at the end of any method with such (configurable?) annotation, a check could be performed that all late-init vars are initialized… But something explicitly invoked by developer is probably easier and more reliable.


#16

There’s one thing that seems to not have come up yet: not all lateinits are equal within the same class, for example there may be a late variable that’s initialized in onCreate, but it’s also possible that there’s another field that is only initialized “later”, for example in onContentChanged.


#17

IMHO this would not be an appropriate use of lateinit. It’s “too late” for safely – in this situation I would personally use a nullable and accept the usual annoyances around nullables in exchange for the safety. :slight_smile: