📜 ⬆️ ⬇️

How to shoot yourself in Kotlin

Most recently, Kotlin was released , and its development team offered to ask questions about the language. He is now at the hearing, and perhaps many would like to try it.
A couple of weeks ago, the team leader made a presentation for the company that Kotlin was good. One of the most interesting questions was “How can I shoot myself in Kotlin in the foot?” It so happened that I answered this question.

Disclaimer:
Do not take this article as "Kotlin - sucks." Although I rather belong to the category of those who are good with Scala, I think that the language is not bad.
All points are controversial, but once a year the stick shoots. Sometime you will shoot your head at the same time, and someday you will be able to shoot only at midnight of the full moon, if you first perform a black ritual of creating bad code.

Our team recently completed a big project on Scala, now we are doing a smaller project on Kotlin, so the spoilers will have a comparison with Scala. I will assume that Notolble in Kotlin is the equivalent of Option, although this is not at all the case, but most likely, those who have worked with Option will use Nullable instead.
')

1. Post-increment and pre-increment as expressions


I quote the questioner: “Ugh, this is the button accordion, boring.” So many copies broken, a million questions in C ++ interviews ... If you have a habit, then you could leave it with an instruction (statement). In fairness, other operators, like + =, are instructions.
I quote one of the developers, abreslav :
We looked at the cases, saw what was going to break, we decided to leave.

I note that we do not have C ++ here, and there is nothing to ask about the increment at the interview. Is that the difference between prefix and postfix.
There is no court. Of course, in their right mind no one will do that, but by chance - maybe.
var i = 5 i = i++ + i++ println(i) 

No undefined behavior, the result is obviously 12
eleven

  var a = 5 a = ++a + ++a println(a) 

It's all easier, of course, 14
13

More examples

  var b = 5 b = ++b + b++ println(b) 

Banal logic says that the answer should be between 11 and 13
yes 12

  var c = 5 c = c++ + ++c println(c) 

From the permutation of the terms of the sum does not change
certainly 12

  var d = 5 d = d + d++ + ++d + ++d println(d) var e = 5 e = ++e + ++e + e++ + e println(e) 

From the permutation of the terms of the sum does not change!
Of course:
25
28


What is there in Scala?
There is nothing interesting, there are no increments in Scala. The compiler will say that there is no ++ method for int. But if you really want to, it, of course, can be determined.

2. Approved method


  val foo: Int? = null val bar = foo!! + 5 

What they wanted was what they got
Exception in thread "main" kotlin.KotlinNullPointerException

The documentation states that you only need to do this if you really want to get a NullPointerException. This is a good method to shoot yourself in the leg: !! cuts the eye and at first glance at the code everything is clear. Of course, use !! it is supposed then, when you checked the value for null before and the smart cast did not work for some reason. Or when for some reason you are sure that there can not be null.

What is there in Scala?
  val foo: Option[Int] = None val bar = foo.get + 5 

What they wanted was what they got
Exception in thread "main" java.util.NoSuchElementException: None.get


3. Override invoke ()


Let's start with a simple one: what does this piece of code do and what type of a?
  class A(){...} val a = A() 

To a stupid question - a stupid answer
Correctly, creates a new object of type A, invoking the default constructor.

And what will happen here?
  class  private constructor(){...} val b = B() 

Well, probably, the compilation error will be ...
And no!
 class B private constructor(){ var param = 6 constructor(a: Int): this(){ param = a } companion object{ operator fun invoke() = B(7) } } 

For a class, a factory can be defined. And if she were in class A, then the constructor would still be called there.

Now you are ready for everything:
  class  private constructor(){...} val c = C() 

Here an object of class C is created through a factory defined in a companion object of class C.
Of course not!
 class C private constructor(){ ... companion object{ operator fun invoke() = A(9) } } 

Variable c will have type A. Notice that A and C are not related.

Full code
 class A(){ var param = 5 constructor(a: Int): this(){ param = a } companion object{ operator fun invoke()= A(10) } } class B private constructor(){ var param = 6 constructor(a: Int): this(){ param = a } companion object{ operator fun invoke() = B(7) } } class C private constructor(){ var param = 8 constructor(a: Int): this(){ param = a } companion object{ operator fun invoke() = A(9) } } class D(){ var param = 10 private constructor(a: Int): this(){ param = a } companion object{ operator fun invoke(a: Int = 25) = D(a) } } fun main(args: Array<String>) { val a = A() val b = B() val c = C() val d = D() println("${a.javaClass}, ${a.param}") println("${b.javaClass}, ${b.param}") println("${c.javaClass}, ${c.param}") println("${d.javaClass}, ${d.param}") } 

Result of performance:
 class A, 5
 class B, 7
 class A, 9
 class D, 10


Unfortunately, I could not come up with a short example where you really break everything. But you can dream a little. If you return the left class, as in the example with class C, then most likely the compiler will stop you. But if you don’t pass the object anywhere, you can simulate the duck typing, as in the example. Nothing criminal, but a person reading the code can go crazy and shoot himself if he doesn’t have the class source.
If you have inheritance and functions for working with the base class (Animal), and invoke () from one heir (Dog) will return another heir to you (Duck), then when checking types (Animal as Dog) you can shake your misfortune.

What is there in Scala?
In Scala is easier - there is new, which always calls the constructor. If new is not present, the companion's apply method is always called (which can also return the left type). Of course, if something is not available to you because of private, then the compiler will curse. All the same, only more obvious.

4. lateinit


 class SlowPoke(){ lateinit var value: String fun test(){ if (value == null){ //  ,     (  ) println("null") return } if (value == "ololo") println("ololo!") else println("alala!") } } SlowPoke().test() 

The result is predictable.
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property value has not been initialized

What is the right way then?
 class SlowBro(){ val value: String? = null fun test(){ if (value == null) { println("null") return } if (value == "ololo") println("ololo!") else println("alala!") } } SlowBro().test() 

Result
null


I would say that this is also an approved way, but when reading the code it is not obvious, unlike the !!. The documentation says a bit covertly that, they say, it is not necessary to check, if anything, we will throw Exception to you. In theory, this modifier is used when you are sure that the field will be initialized by someone else. That is, never. In my experience, all fields that were lateinit, sooner or later became Nullable. Not bad, this field fit into the JavaFX application controller, where Gui is loaded from FXML, but even this “reinforced concrete” solution was overthrown after an alternative appeared without a pair of buttons. Once it happened that in SceneBuilder I changed fx: id, but I forgot it in the code. In the first days of coding on Kotlin, I got a little mad that you can't do lateinit Int. I can think of why they did it this way, but I doubt that there is no way around these reasons (read: to make a crutch).

What is there in Scala?
And there is no analogue of lateinit as such. At least I did not find.

5. Constructor


 class IAmInHurry(){ val param = initSecondParam() /*tons of code*/ val twentySecondParam = 10 /*tons of code*/ fun initSecondParam(): Int{ println("Initializing by default with $twentySecondParam") return twentySecondParam } } class IAmInHurryWithStrings(){ val param = initSecondParam() /*tons of code*/ val twentySecondParam = "Default value of param" /*tons of code*/ fun initSecondParam(): String{ println("Initializing by default with $twentySecondParam") return twentySecondParam } } fun main(args: Array<String>){ IAmInHurry() IAmInHurryWithStrings() } 

Result
Initializing by default with 0
Initializing by default with null

Everything is simple - the field is addressed before it was initialized. Apparently, here it is worth a little refine the compiler. In theory, if you write code well, you should not have such a problem, but anything can happen, but I didn’t take this example from the ceiling (a colleague shot himself in the leg, accidentally through a chain of methods in a rarely triggered code caused a field that was not initialized).

What is there in Scala?
All the same.
 object Initializer extends App{ class IAmInHurry(){ val param = initSecondParam() /*tons of code*/ val twentySecondParam = 10 /*tons of code*/ def initSecondParam(): Int = { println(s"Initializing by default with $twentySecondParam") twentySecondParam } } class IAmInHurryWithStrings(){ val param = initSecondParam() /*tons of code*/ val twentySecondParam = "Default value of param" /*tons of code*/ def initSecondParam(): String = { println(s"Initializing by default with $twentySecondParam") twentySecondParam } } override def main(args: Array[String]){ new IAmInHurry() new IAmInHurryWithStrings() } } 

Result
Initializing by default with 0
Initializing by default with null


6. Interacting with Java


There is enough space for a shot here. The obvious solution is to consider everything that came from Java, Nullable. But there is a long and instructive story . As I understand it, it is mainly related to patterns, inheritance, and the Java-Kotlin-Java chain. And with such scenarios, you had to make a lot of crutches to make it work. Therefore, we decided to abandon the idea of ​​"all Nullable".
But it seems like one of the main scenarios - we write our code in Kotlin, libraries take Java (as I see it, a simple peasant coder). And in this situation, better security for the most part of the code and obvious crutches in a small part of the code, which can be seen than “nice and comfortable” + a sudden rake at runtime (or a pit with stakes, as lucky). But the developers have a different opinion :
One of the main reasons was that writing in such a language was inconvenient, and reading it was unpleasant. There are question and exclamation marks everywhere, which do not really help because they are arranged mainly to satisfy the compiler, and not to correctly handle the cases when the expression is evaluated in null. Especially painful in the case of generics: for example, Map <String ?, String?>? ..

Make a small Java class:
 public class JavaCopy { private String a = null; public JavaCopy(){}; public JavaCopy(String s){ a = s; } public String get(){ return a; } } 

And try to call him from Kotlin:
  fun printString(s: String) { println(s) } val j1 = JavaCopy() val j1Got = j1.get() printString(j1Got) 

Result
Exception in thread "main" java.lang.IllegalStateException: j1Got must not be null

Type at j1 - String! and we will get an exception only when we call printString. Ok, let's set the type explicitly:
  val j2 = JavaCopy("Test") val j3 = JavaCopy(null) val j2Got: String = j2.get() val j3Got: String = j3.get() printString(j2Got) printString(j3Got) 

Result
Exception in thread "main" java.lang.IllegalStateException: j3.get () must not be null

Everything is logical. When we explicitly indicate that we need NotNullable, then we catch an exception. It would seem, indicate all variables Nullable, and everything will be fine. But if you do this:
 printString(j2.get()) 
then you may not find an error soon.

What is there in Scala?
No guarantees, NPE can be caught simply. The solution is to wrap everything in Option, which, I remind you, has a good property that Option (null) = None. On the other hand, there are no illusions that the java interop is safe.

7. infix notation and lambda


Make a chain of methods and call it:
 fun<R> first(func: () -> R): R{ println("calling first") return func() } infix fun<R, T> R.second(func: (R) -> T): T{ println("calling second") return func(this) } first { println("calling first body") } second { println("calling second body") } 

Result
calling first
calling first body
Oops!
calling second body

Wait ... there is some kind of setup! Indeed, I forgot to insert one method:
 fun<T> second(func: () -> T): T{ println("Oops!") return func() } 

And in order to work “as it should,” it was necessary to write this:
 first { println("calling first body") } second { println("calling second body") } 

Result
calling first
calling first body
calling second
calling second body

Just one line break, which is easy to remove / add behavior when reformatting. Based on real events: there was a chain of “make in background” and “then make in ui thread”. And there was a “do it in ui” method with the same name.

What is there in Scala?
The syntax is a little different, so it's so easy to shoot yourself here:
 object Infix extends App{ def first[R](func: () => R): R = { println("calling first") func() } implicit class Second[R](val value: R) extends AnyVal{ def second[T](func: (R) => T): T = { println("calling second") func(value) } } def second[T](func: () => T): T = { println("Oops!") func() } override def main(args: Array[String]) { first { () => println("calling first body") } second { () => //<--------type mismach println("calling second body") } } } 

But, trying to adjust the skalovsky code at least for non-obviousness due to implicit / underscore, I blew everything around.
Caution! Blood, guts and dismemberment ...
 object Infix2 extends App{ def first(func: (Unit) => Unit): Unit = { println("calling first") func() } implicit class Second(val value: Unit) extends AnyVal{ def second(func: (Unit) => Unit): Unit = { println("calling second") func(value) } } def second(func: (Unit) => Unit): Unit = { println("Oops!") func() } override def main(args: Array[String]) { first { _ => println("calling first body") } second { _ => println("calling second body") } } } 

And the result:
 Exception in thread "main" java.lang.VerifyError: Operand stack underflow
 Exception Details:
   Location:
     Infix2 $ Second $ .equals $ extension (Lscala / runtime / BoxedUnit; Ljava / lang / Object;) Z @ 40: pop
   Reason:
     Attempt to pop empty stack.
   Current Frame:
     bci: @ 40
     flags: {}
     locals: {'Infix2 $ Second $', 'scala / runtime / BoxedUnit', 'java / lang / Object', 'java / lang / Object', integer}
     stack: {}
   Bytecode:
     0000000: 2c4e 2dc1 0033 9900 0904 3604 a700 0603
     0000010: 3604 1504 9900 4d2c c700 0901 5701 a700
     0000020: 102c c000 33b6 0036 57bb 0038 59bf 3a05
     0000030: b200 1f57 b200 1fb2 001f 57b2 001f 3a06
     0000040: 59c7 000c 5719 06c6 000e a700 0f19 06b6
     0000050: 003c 9900 0704 a700 0403 9900 0704 a700
     0000060: 0403 ac                                
   Stackmap Table:
     append_frame (@ 15, Object [# 4])
     append_frame (@ 18, Integer)
     same_frame (@ 33)
     same_locals_1_stack_item_frame (@ 46, Null)
     full_frame (@ 77, {Object [# 2], Object [# 27], Object [# 4], Object [# 4], Integer, Null, Object [# 27]}, {Object [# 27]})
     same_frame (@ 85)
     same_frame (@ 89)
     same_locals_1_stack_item_frame (@ 90, Integer)
     chop_frame (@ 97.2)
     same_locals_1_stack_item_frame (@ 98, Integer)

	 at Infix2 $ .main (Infix.scala)

8. Overload methods and it


It is, rather, a method to play with others. Imagine that you are writing a library and there is a function in it
 fun applier(x: String, func: (String) -> Unit){ func(x) } 

Of course, the people use it in a rather transparent way:
  applier ("arg") { println(it) } applier ("no arg") { println("ololo") } 

The code is compiled, it works, everyone is happy. And then you add the method
 fun applier(x: String, func: () -> Unit){ println("not applying $x") func() } 

And so that the compiler does not swear, users will have to abandon it everywhere (read: rewrite a bunch of code):
  applier ("arg") { it -> //FIXED println(it) } applier ("no arg") { -> //yes, explicit! println("ololo") } 

Although, theoretically, the compiler could guess that if it is, then it is lambda with 1 input argument. I think that with the development of the language and the compiler will grow wiser, and this point is temporary.

What is there in Scala?
Without arguments, you have to explicitly indicate that this is lambda. And when you add a new method, the behavior will not change.
 object Its extends App{ def applier(x: String, func: (String) => Unit){ func(x) } def applier(x: String, func: () => Unit){ println("not applying $x") func() } override def main(args: Array[String]) { applier("arg", println(_)) applier("no arg", _ => println("ololo")) } } 

9. Why you shouldn’t think of Nullable as Option


Suppose we have a wrapper for the cache:
 class Cache<T>(){ val elements: MutableMap<String, T> = HashMap() fun put(key: String, elem: T) = elements.put(key, elem) fun get(key: String) = elements[key] } 

And a simple usage scenario:
  val cache = Cache<String>() cache.put("foo", "bar") fun getter(key: String) { cache.get(key)?.let { println("Got $key from cache: $it") } ?: println("$key is not in cache!") } getter("foo") getter("baz") 

The result is pretty predictable.
Got foo from cache: bar
baz is not in cache!

But if we suddenly want to keep the cache Nullable ...
  val cache = Cache<String?>() cache.put("foo", "bar") fun getter(key: String) { cache.get(key)?.let { println("Got $key from cache: $it") } ?: println("$key is not in cache!") } getter("foo") getter("baz") cache.put("IAmNull", null) getter("IAmNull") 

That will not work very well.
Got foo from cache: bar
baz is not in cache!
IAmNull is not in cache!

Why keep null? For example, to show that the result is not computable. Of course, it would be better to use Option or Either, but, unfortunately, neither the one nor the other is in the standard library (but there is, for example, in funKTionale ). Moreover, just when implementing Either, I stepped on the rake of this item and the previous one. To solve this problem with the “double Nullable” you can, for example, return a Pair or a special data class.

What is there in Scala?
No one forbids making Option from Option. I hope it is clear that everything will be alright. Yes, and with null too:
 object doubleNull extends App{ class Cache[T]{ val elements = mutable.Map.empty[String, T] def put(key: String, elem: T) = elements.put(key, elem) def get(key: String) = elements.get(key) } override def main(args: Array[String]) { val cache = new Cache[String]() cache.put("foo", "bar") def getter(key: String) { cache.get(key) match { case Some(value) => println(s"Got $key from cache: $value") case None => println(s"$key is not in cache!") } } getter("foo") getter("baz") cache.put("IAmNull", null) getter("IAmNull") } 
All is well
Got foo from cache: bar
baz is not in cache!
Got IAmNull from cache: null

10. Declaring methods


Bonus for those who previously wrote on Scala. The sponsor of this item is lgorSL .
I quote:
...
Or, for example, the syntax of a method declaration:
In scala: def methodName (...) = {...}
In kotlin, two options are possible - as in scala (with the = sign) and as in java (without it), but these two ways of declaring are not equivalent to each other and work a little differently, I once spent a lot of time searching for such a “feature” in code.
...

I meant the following:

fun test () {println ("it works")}
fun test2 () = println ("it works too")
fun test3 () = {println ("surprise!")}

To display “surprise”, you have to write test3 () (). The test3 () call option is also compiled normally, it only works not as expected - adding “extra” parentheses drastically changes the program logic.

Because of these rakes, the transition from cliff to Kotlin turned out to be a bit painful - sometimes I write an equal sign “out of habit” in declaring some method, and then I have to look for mistakes.


Conclusion


This list is probably not exhausted, so share in the comments how you were on the road to adventure, but then something went wrong ...
The language has many positive features about which you can read on the official website , in articles on Habré and many more where. But personally, I disagree with some architectural solutions (classes final by default, java interop) and sometimes it is felt that the language lacks uniformity and consistency. In addition to the example with lateinit Int, I will give two more. Inside the let blocks we use it, inside with with this, and inside run, which is a combination of let and this what should I use? And the class String! You can call the methods isBlank (), isNotBlank (), isNullOrBlank (), and there is no “complementary” method like isNotNullOrBlank :( After Scala lacks some things - Option, Either, matching, currying. But in general, the language leaves a pleasant impression, I hope that he will continue to develop with dignity

PS Kablin's illumination of Habrovskaya is lame, I hope that the administration of habrahabr will sometime correct this ...

UPD: Shots from commentators (I will update)


The obvious priority of the elvis operator . The author is senia .

UPD2


Pay attention to the article Kotlin: without Best Practices and life is not the same .
In the comments there is another smart shot from Artem_zin : the ability to redefine get () from val and return values ​​dynamically.

UPD3


Some more newbies might think that the and and or operators for Boolean variables are such a sugar for “unreadable” && and || . However, this is not so: although the result of the calculations will be the same, but the "old" operators are calculated lazily. Therefore, if you suddenly write like this:
 if ((i >= 3) and (someArray[i-3] > 0)){ //do something } 
then get an exception when i<3 .

Source: https://habr.com/ru/post/278169/


All Articles