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 12eleven
var a = 5 a = ++a + ++a println(a)
It's all easier, of course, 1413
More examples var b = 5 b = ++b + b++ println(b)
Banal logic says that the answer should be between 11 and 13yes 12
var c = 5 c = c++ + ++c println(c)
From the permutation of the terms of the sum does not changecertainly 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 gotException 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 gotException 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 answerCorrectly, 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){
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()
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() val twentySecondParam = 10 fun initSecondParam(): Int{ println("Initializing by default with $twentySecondParam") return twentySecondParam } } class IAmInHurryWithStrings(){ val param = initSecondParam() val twentySecondParam = "Default value of param" fun initSecondParam(): String{ println("Initializing by default with $twentySecondParam") return twentySecondParam } } fun main(args: Array<String>){ IAmInHurry() IAmInHurryWithStrings() }
ResultInitializing 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() val twentySecondParam = 10 def initSecondParam(): Int = { println(s"Initializing by default with $twentySecondParam") twentySecondParam } } class IAmInHurryWithStrings(){ val param = initSecondParam() val twentySecondParam = "Default value of param" def initSecondParam(): String = { println(s"Initializing by default with $twentySecondParam") twentySecondParam } } override def main(args: Array[String]){ new IAmInHurry() new IAmInHurryWithStrings() } }
ResultInitializing 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)
ResultException 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)
ResultException 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") }
Resultcalling 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") }
Resultcalling 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 { () =>
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 ->
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 wellGot 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)){
then get an exception when
i<3
.