Совсем недавно вышел релиз Kotlin, а его команда разработчиков предлагала задавать вопросы про язык. Он сейчас на слуху и, возможно, многим хочется его попробовать. Пару недель назад тимлид сделал для компании презентацию о том, что в Котлине хорошо. Одним из самых интересных вопросов был "А как в Котлине выстрелить себе в ногу?" Так получилось, что ответил на этот вопрос я.

Disclaimer: Не стоит воспринимать эту статью как "Kotlin - отстой". Хотя я отношусь скорее к категории тех, кому и со Scala хорошо, я считаю, что язык неплохой. Все пункты спорные, но раз в год и палка стреляет. Когда-то вы себе прострелите заодно и башку, а когда-то у вас получится выстрелить только в полночь полнолуния, если вы предварительно совершите черный ритуал создания плохого кода.

Наша команда недавно закончила большой проект на Scala, сейчас делаем проект помельче на Kotlin, поэтому в спойлерах будет сравнение со Scala. Я буду считать, что Nullable в Kotlin - это эквивалент Option, хотя это совсем не так, но, скорее всего, большинство из тех, кто работал с Option, будут вместо него использовать Nullable.

1. Пост-инкремент и преинкремент как выражения

Цитирую вопрошавшего: "Фу, это ж баян, скучно". Столько копий сломано, миллион вопросов на собеседованиях C++... Если есть привычка, то можно было его оставить инструкцией (statement'ом). Справедливости ради, другие операторы, вроде +=, являются инструкциями. Цитирую одного из разработчиков, @abreslav:

Смотрели на юзкейсы, увидели, что поломается, решили оставить.

Замечу, что у нас тут не С++, и на собеседовании про инкремент спросить особо нечего. Разве что разницу между префиксным и постфиксным.

На нет и суда нет. Разумеется, в здравом уме никто так делать не будет, но случайно - может быть.

    var i = 5
    i = i++ + i++
    println(i)
Никакого undefined behaviour, результат, очевидно, 12 11

    var a = 5
    a = ++a + ++a
    println(a)
Тут все проще, конечно, 14 13

Больше примеров
    var b = 5
    b = ++b + b++
    println(b)
Банальная логика говорит, что ответ должен быть между 11 и 13 да, 12

    var c = 5
    c = c++ + ++c
    println(c)
От перестановки мест слагаемых сумма не меняется разумеется, 12

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

    var e = 5
    e = ++e + ++e + e++ + e
    println(e)
От перестановки мест слагаемых сумма не меняется!

Разумеется:

25
28

Чё там в Scala? Ничего интересного, в Scala инкрементов нет. Компилятор скажет, что нет метода ++ для Int. Но если очень захотеть, его, конечно, можно определить.

2. Одобренный способ

    val foo: Int? = null
    val bar = foo!! + 5
Что хотели, то и получили
Exception in thread "main" kotlin.KotlinNullPointerException

В документации говорится, что так делать стоит только если вы очень хотите получить NullPointerException. Это хороший метод выстрелить себе в ногу: !! режет глаз и при первом взгляде на код все понятно. Разумеется, использование !! предполагается тогда, когда до этого вы проверили значение на null и smart cast по какой-нибудь причине не сработал. Или когда вы почему-то уверены, что там не может быть null.

Чё там в Scala?
   val foo: Option[Int] = None
   val bar = foo.get + 5
Что хотели, то и получили
Exception in thread "main" java.util.NoSuchElementException: None.get

3. Переопределение invoke()

Начнем с простого: что делает этот кусок кода и какой тип у a?

    class A(){...}
    val a = A()
На глупый вопрос - глупый ответ Правильно, создает новый объект типа A, вызывая конструктор по умолчанию.

А здесь что будет?
    class В private constructor(){...}
    val b = B()
Ну, наверно, ошибка компиляции будет...

А вот и нет!

class B private constructor(){
   var param = 6

   constructor(a: Int): this(){
       param = a
   }

   companion object{
       operator fun invoke() = B(7)
   }
}

Для класса может быть определена фабрика. А если бы она была в классе A, то там все равно вызывался бы конструктор.


Теперь вы ко всему готовы:
    class С private constructor(){...}
    val c = C()
Тут создается объект класса С через фабрику, определенную в объекте-компаньоне класса С.

Конечно же нет!

class C private constructor(){
   ...
    companion object{
        operator fun invoke() = A(9)
    }
}

У переменной c будет тип A. Заметьте, что A и С не связаны родственными узами.

Полный код
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}")
}

Результат выполнения:

class A, 5
class B, 7
class A, 9
class D, 10

К сожалению, придумать короткий пример, где у вас реально все поломается, я не смог. Но пофантазировать немного можно. Если вы вернете левый класс, как в примере с классом C, то скорее всего, компилятор вас остановит. Но если вы никуда не передаете объект, то можно сымитировать утиную типизацию, как в примере. Ничего криминального, но человек, читающий код, может сойти с ума и застрелиться, если у него не будет исходника класса. Если у вас есть наследование и функции для работы с базовым классом (Animal), а invoke() от одного наследника (Dog) вернет вам другого наследника (Duck), то тогда при проверке типов (Animal as Dog) вы можете накрякать себе беду.

Чё там в Scala?

В Scala проще - есть new, который всегда вызывает конструктор. Если не будет new, то всегда вызывается метод apply у компаньона (который тоже может вернуть левый тип). Разумеется, если что-то вам не доступно из-за private, то компилятор ругнется. Все то же самое, только очевиднее.

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()
Результат предсказуем
Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property value has not been initialized
А как правильно?
class SlowBro(){
    val value: String? = null

    fun test(){
        if (value == null) {
            println("null")
            return
        }
        if (value == "ololo")
            println("ololo!")
        else
            println("alala!")
    }
}
SlowBro().test()
Результат
null

Я бы сказал, что это тоже одобренный способ, но при чтении кода это неочевидно, в отличие от !!. В документации немного завуалированно говорится, что, мол, проверять не надо, если что, мы кинем тебе Exception. По идее, этот модификатор используется тогда, когда вы точно уверены, что поле будет инициализированно кем-то другим. То есть никогда. По моему опыту, все поля, которые были lateinit, рано или поздно стали Nullable. Неплохо это поле вписалось в контроллер JavaFX приложения, где GUI грузится из FXML, но даже это "железобетонное" решение было свергнуто после того, как появился альтернативный вариант без пары кнопок. Один раз так получилось, что в SceneBuilder изменил fx:id, а в коде забыл. В первые дни кодинга на Kotlin немного взбесило, что нельзя сделать lateinit Int. Я могу придумать, почему так сделали, но сомневаюсь, что совсем нет способа обойти эти причины (читай: сделать костыль).

Чё там в Scala?

А там аналога lateinit как такового и нет. По крайней мере, я не обнаружил.

5. Конструктор

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()
}
Результат
Initializing by default with 0
Initializing by default with null

Все просто - к полю идет обращение до того, как оно было инициализировано. Видимо, тут стоит немного доработать компилятор. По идее, если вы пишете код хорошо, такая проблема у вас не должна возникнуть, но всякое бывает, не с потолка же я взял этот пример (коллега себе так выстрелил в ногу, случайно через цепочку методов в редко срабатывающем коде вызвал поле, которое было не инициализировано).

Чё там в Scala?

Все то же самое.

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()
  }
}
Результат
Initializing by default with 0
Initializing by default with null

6. Взаимодействие с Java

Для выстрела тут простор достаточно большой. Очевидное решение - считать все, что пришло из Java, Nullable. Но тут есть долгая и поучительная история. Как я понял, она связана в основном с шаблонами, наследованием, и цепочкой Java-Kotlin-Java. И при таких сценариях приходилось делать много костылей, чтобы заработало. Поэтому решили от идеи "все Nullable" отказаться. Но вроде как один из основных сценариев — свой код пишем на Kotlin, библиотели берем Java (как видится мне, простому крестьянину-кодеру). И при таком раскладе, лучше безопасность в большей части кода и явные костыли в небольшой части кода, которые видно, чем "красиво и удобно" + внезапные грабли в рантайме (или яма с кольями, как повезет). Но у разработчиков другое мнение:

Одна из основных причин была в том, что писать на таком языке было неудобно, а читать его — неприятно. Повсюду вопросительные и восклицательные знаки, которые не очень-то помогают из-за того, что расставляются в основном, чтобы удовлетворить компилятор, а не чтобы корректно обработать случаи, когда выражение вычисляется в null. Особенно больно в случае дженериков: например, Map?.
Сделаем небольшой класс на Java:
public class JavaCopy {
    private String a = null;

    public JavaCopy(){};

    public JavaCopy(String s){
        a = s;
    }

    public String get(){
        return a;
    }
}

И попробуем его вызвать из Kotlin:

    fun printString(s: String) {
        println(s)
    }

    val j1 = JavaCopy()
    val j1Got = j1.get()
    printString(j1Got)
Результат
Exception in thread "main" java.lang.IllegalStateException: j1Got must not be null

Тип у j1 - String! и исключение мы получим только тогда, когда вызовем printString. Ок, давайте явно зададим тип:

    val j2 = JavaCopy("Test")
    val j3 = JavaCopy(null)

    val j2Got: String = j2.get()
    val j3Got: String = j3.get()

    printString(j2Got)
    printString(j3Got)
Результат
Exception in thread "main" java.lang.IllegalStateException: j3.get() must not be null 

Все логично. Когда мы явно указываем, что нам нужен NotNullable, тогда и ловим исключение. Казалось бы, указывай у всех переменных Nullable, и все будет хорошо. Но если делать так:

kotlinprintString(j2.get())

то ошибку вы можете обнаружить нескоро.

Чё там в Scala?

Никаких гарантий, NPE словить можно элементарно. Решение - оборачивать все в Option, у которого, напомню, есть хорошее свойство, что Option(null) = None. С другой стороны, тут нет иллюзий, что java interop безопасен.

7. infix нотация и лямбды

Сделаем цепочку из методов и вызовем ее:

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")
}
Результат
calling first
calling first body
Oops!
calling second body

Подождите-ка... тут какая-то подстава! И правда, "забыл" один метод вставить:

fun<T> second(func: () -> T): T{
    println("Oops!")
    return func()
}

И чтобы заработало "как надо", нужно было написать так:

first {
    println("calling first body")
} second {
    println("calling second body")
}
Результат
calling first
calling first body
calling second
calling second body

Всего один перенос строки, который легко при переформатировании удалить/добавить переключает поведение. Основано на реальных событиях: была цепочка методов "сделай в background" и "потом сделай в ui треде". И был метод "сделай в ui" с таким же именем.

Чё там в Scala?

Синтаксис немного отличается, поэтому так просто тут себе не выстрелишь:

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")
    }
  }
}

Зато, пытаясь подогнать скаловский код хотя бы для неочевидности засчет implicit/underscore, я взорвал все вокруг.

Осторожно! Кровь, кишки и расчлененка...
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")
    }
  }
}

И результат:

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. Перегрузка методов и it

Это, скорее, метод подгадить другим. Представьте, что вы пишете библиотеку, и в ней есть функция

fun applier(x: String, func: (String) -> Unit){
    func(x)
}

Разумеется, народ ее использует довольно прозрачным способом:

    applier ("arg") {
        println(it)
    }
    applier ("no arg") { 
        println("ololo")
    }

Код компилируется, работает, все довольны. А потом вы добавляете метод

fun applier(x: String, func: () -> Unit){
    println("not applying $x")
    func()
}

И чтобы компилятор не ругался, пользователям придется везде отказаться от it (читай: переписать кучу кода):

    applier ("arg") { it -> //FIXED
        println(it)
    }
    applier ("no arg") { -> //yes, explicit!
        println("ololo")
    }

Хотя, теоретически, компилятор мог бы и угадать, что если есть it, то это лямбда с 1 входным аргументом. Думаю, что с развитием языка и компилятор поумнеет, и этот пункт — временный.

Чё там в Scala?

Без аргументов придется явно указать, что это лямбда. А при добавлении нового метода поведение не изменится.

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. Почему не стоит думать о Nullable как об Option

Пусть у нас есть обертка для кэша:

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]
}

И простой сценарий использования:

    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")
Результат довольно предсказуем
Got foo from cache: bar
baz is not in cache!

Но если мы вдруг захотим к кэше хранить Nullable... ```kotlin val cache = Cache() 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")

<details class="spoiler">
	<summary class="spoiler">То получится не очень хорошо</summary>
	

Got foo from cache: bar baz is not in cache! IAmNull is not in cache!


</details>

Зачем хранить `null`? Например, чтобы показать, что результат не вычислим. Конечно, тут было бы правильнее использовать `Option` или `Either`, но, к сожалению, ни того, ни другого в стандартной библиотеке нет (но есть, например, в <a href="https://github.com/MarioAriasC/funKTionale/wiki">funKTionale</a>). Более того, как раз при реализации `Either`, я наступил на грабли этого пункта и предыдущего. Решить эту проблему с "двойным Nullable" можно, например, возвратом `Pair` или специального `data class`.


<details class="spoiler">
	<summary class="spoiler">Чё там в Scala?</summary>
	
Никто не запретит сделать Option от Option. Надеюсь, понятно, что так все будет хорошо. Да и с `null` тоже:
```scala
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")
  }
Все хорошо
Got foo from cache: bar
baz is not in cache!
Got IAmNull from cache: null

10. Объявление методов

Бонус для тех, кто раньше писал на Scala. Спонсор данного пункта - @lgorSL. Цитирую:

...
Или, например, синтаксис объявления метода:

В scala: def methodName(...) = {...}

В kotlin возможны два варианта — как в scala (со знаком =) и как в java (без него), но эти два способа объявления неэквивалентны друг другу и работают немного по-разному, я однажды кучу времени потратил на поиск такой "особенности" в коде.
....

Я подразумевал следующее:
fun test(){ println("it works") } 
fun test2() = println("it works too")
fun test3() = {println("surprise!")}

Чтобы вывести "surprise", придётся написать test3()(). Вариант вызова test3() тоже нормально компилируется, только сработает не так, как ожидалось — добавление "лишних" скобочек кардинально меняет логику программы.

Из-за этих граблей переход со скалы на котлин оказался немного болезненным — иногда "по привычке" в объявлении какого-нибудь метода пишу знак равенства, а потом приходится искать ошибки.

Заключение

На этом список наверняка не исчерпывается, поэтому делитесь в комментариях, как вы шли дорогой приключений, но потом что-то пошло не так... У языка много положительных черт, о которых вы можете прочитать на официальном сайте, в статьях на хабре и еще много где. Но лично я не согласен с некоторыми архитектурными решениями (классы final by default, java interop) и иногда чувствуется, что языку нехватает единообразия, консистентности. Кроме примера с lateinit Int приведу еще два. Внутри блоков let используем it, внутри with - this, а внутри run, который является комбинацией let и this что надо использовать? А у класса String! можно вызвать методы isBlank(), isNotBlank(), isNullOrBlank(), а "дополняющего" метода вроде isNotNullOrBlank нет:( После Scala нехватает некоторых вещей - Option, Either, matching, каррирования. Но в целом язык оставляет приятное впечатление, надеюсь, что он продолжит достойно развиваться.

P.S. Хабровская подсветка Kotlin хромает, надеюсь, что администрация @habrahabr это когда-нибудь поправит...

UPD: Выстрелы от комментаторов (буду обновлять)

Неочевидный приоритет оператора elvis. Автор - @senia.

UPD2

Обратите еще внимание на статью Kotlin: без Best Practices и жизнь не та. В комментариях там есть еще один шикарный выстрел от @Artem_zin: возможность переопределить get() у val и возвращать значения динамически.

UPD3

Еще некоторые новички могут подумать, что операторы and и or для булевых переменных - это такой сахар для "нечитаемых" && и ||. Однако это не так: хоть результат вычислений будет тем же, но "старые" операторы вычисляются лениво. Поэтому если вы вдруг напишете так:

if ((i >= 3) and (someArray[i-3] > 0)){
    //do something
}

то получите исключение при i<3.

Скрин опроса: