Как себе выстрелить в ногу в Kotlin

    Совсем недавно вышел релиз 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<String?, String?>?..

    Сделаем небольшой класс на 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, и все будет хорошо. Но если делать так:
    printString(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...
        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")
    

    То получится не очень хорошо
    Got foo from cache: bar
    baz is not in cache!
    IAmNull is not in cache!

    Зачем хранить null? Например, чтобы показать, что результат не вычислим. Конечно, тут было бы правильнее использовать Option или Either, но, к сожалению, ни того, ни другого в стандартной библиотеке нет (но есть, например, в funKTionale). Более того, как раз при реализации Either, я наступил на грабли этого пункта и предыдущего. Решить эту проблему с «двойным Nullable» можно, например, возвратом Pair или специального data class.

    Чё там в Scala?
    Никто не запретит сделать Option от Option. Надеюсь, понятно, что так все будет хорошо. Да и с null тоже:
    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.

    Only registered users can participate in poll. Log in, please.

    Как действительно можно себе что-нибудь неожиданно прострелить?

    • 23.2%1. Пост-инкремент и преинкремент как выражени25
    • 8.3%2. Одобренный способ9
    • 28.7%3. Переопределение invoke()31
    • 28.7%4. lateinit31
    • 34.3%5. Конструктор37
    • 38.0%6. Взаимодействие с Java41
    • 23.2%7. infix нотация и лямбды25
    • 31.5%8. Перегрузка методов и it34
    • 23.2%9. Почему не стоит думать о Nullable как об Option25
    • 23.2%10. Объявление методов25
    ИНФОРИОН
    Решения ИТ-инфраструктуры и защита информации

    Comments 37

      +1
      А в чем прикол с постинкрементом в самом первом примере?

      var i = 5
      i = i++ + i++
      println(i)

      Если он "пост" инкремент, то должен выполняться ПОСЛЕ вычисления всего выражения выражения, после "точки с запятой" которой в этом языке нет. Сначала i = 5+5, т.е.10, затем еще два раза ++, то есть 12. Почему 11?
        +3
        Так нагляднее:

        fun sum(a: Int, b: Int) = a + b
        
        var i = 5
        i = sum(i++, i++)
        println(i)
        // 11

        "После" — это после вычисления аргумента функции sum. Вычислили первый аргумент, добавили 1 к i, вычислили второй, добавили 1 к i, вычислили sum, присвоили значение в i.
        С оператором точно то же самое.
          +1
          Вот байт-код

          `
            L1
              LINENUMBER 2 L1
              ICONST_5
              ISTORE 1
             L2
              LINENUMBER 3 L2
              ILOAD 1
              IINC 1 1
              ILOAD 1
              IINC 1 1
              IADD
              ISTORE 1
             L3
              LINENUMBER 4 L3
              NOP
          `

          Прочитали 5, увеличили на 1, прочитали 6, увеличили его на 1, сложили 5 и 6, записали туда, где было 7. Упс, не успел.
          +3
          Меня, когда я впервые пробовал пописать на котлине (на дне открытых дверей в Jetbrains), очень сильно удивил приоритет elvis operator:

          fun main(args: Array<String>) {
              val i: Int? = 0
          
              println(2*1 + 1)
              println(i ?: 0 + 1)
          }

          Попытайтесь угадать результат
          3
          0


          Чё там в Scala?
          getOrElse — метод

          val i = Option(0)
          
          scala> i.getOrElse(0) + 1
          res0: Int = 1

          При желании
          Операторов в scala нет вообще, а методы, начинающиеся с букв, имеют наименьший приоритет при операторной записи, так что точно такой же выстрел в ногу возможен, просто так не пишут:

          scala> i getOrElse 0 + 1
          res1: Int = 0


            +1
            ну в варианте скалы вы обозначили сами приоритет
            это как в котлине написать println((i ?: 0) + 1)
              +1
              Блеск! Отличный выстрел
                +3
                Все же в парадигме языка приоритет мне кажется вполне правильным..)
                  0
                  Вполне возможно. Но когда я после a?.method() по аналогии написал a?:0 + 1, я несколько удивился результату.
                  Да, понятен сценарий применения (например throw MyException...).
                  И привыкнуть можно, но для меня это было способом выстрелить себе в ногу.
                  Самое неприятное, что нет возможности этого избежать без дополнительного уровня вложенности скобочек вокруг потенциально длинного выражения с nullable результатом.
                  +1
                  А я сижу и не пойму, в чем разница между Elvis operator и Null coalescing operator. Она есть?
                  0
                  Мне кажется, тут очевидный приоритет элвис оператора, если понимать как он работает с nullable.
                  По другому просто нельзя, верней можно, но это будет ну очень странно.
                  +1
                  Final by default действительно очень спорный вопрос. Как по мне, было бы гораздо лучше если бы везде было open by default. По этому поводу на форуме kotlin, как раз идет большой холивар.
                  Рассуждение авторов языка на тему выбора defaults: статья
                    0
                    Эта тема вообще достойна отдельной статьи. Они ссылаются на спорный пункт Effective Java, который, в свою очередь, ссылается на другой спорный пункт Effective Java. При этом в других частях языка Effective Java может не выполняться.
                    +1
                    В общем по пунктам. Согласен только с тремя: 5, 8 и 9.
                    Остальное работает так, как и должно. Не увидел ничего странного.
                    Что касается первого пункта, просто не надо так писать. Это код с неочевидным результатом на большинстве языков. От такого всегда стоит избавляться.

                    Для сравнения приведу паззлеры на груви. Вот там действительно не очевидно.
                      0
                      Повторюсь: если инкремент нельзя так использовать, зачем тогда оставлять его как expression? В питоне, например, его убрали как раз из тех соображений, что на нем можно написать что-то неочевидное.
                      +1
                      Совершенно непонятна претензия к инкременту, он абсолютно логично работает, причем идентично Java и Groovy.
                      Так же странным выглядит упоминание оператора !!, совершенно непонятно как можно выстрелить себе в ногу. Максимум что тут можно ожидать, это статическая проверка на уровне IDE.
                        –1
                        Ну например, вы проверили var i на null, а потом везде пишете i!!. В это время другой тред изменил значение i на null, и вы ловите NPE.
                          +5
                          Если вы именно таким способом используете переменную в разных потоках, то боюсь что вы выстрелили в ногу себе давно и!!! ваша наименьшая проблема
                            +1
                            Использование этого оператора в любом виде подразумевает, что вы будете стрелять себе в ногу (получением NPE как в примерах или просто плохим кодом, где можно было использовать smart-cast или дополнительную переменную). Опишите хороший кейс для этого оператора, где выстрелить невозможно, если вы считаете, что упоминание этого оператора излишне.
                        0
                        Пункт 5 (про инициализацию полей, названный почем-то "Конструктором") работает так же как в яве (где это описано в JLS).
                          –4
                          Прошу Вас, называйте язык так, как хотели разработчики — джава, а не ява. Умоляю. Всем сердцем.
                            +2
                            Да хоть oak, какая разница? Название происходит от Java coffee, а остров Java у нас зовётся Ява, а не Джава.
                              –3
                              И это правда. Поэтому я и написал "как хотели разработчики", а они его называют так.
                            0
                            Не совсем — примитивы инициализируются сразу. А вот ссылочные поля да, по порядку, и тоже можно выстрелить.
                            +6
                            Если это все выстрелы, то все очень даже неплохо. В scala есть один хороший способ выстрелить себе одновременно в ногу, затылок и задницу, и задеть всех, кто стоит рядом. Это implicit. И если его добавить в пункт 5, то будет просто прекрасно.
                            Вообще во многих случаях scala ведет себя так, что то, что написано — это совсем не то, что имеется ввиду. В лучшем случае, вы смотрите на кусок кода и не понимаете что к чему без знания всего контекста. Так уж закономерность — чем мощнее инструмент, тем легче им отрезать палец.

                            P.S. в 9-м пункте ключи разные:
                            cache.put(«IAmNull», null)
                            getter(«IamNull»)
                              –3
                              Жалко кармы не хватает плюсануть… полностью согласен с автором!

                              п.с. вообще возникает ощущение что "последователи scala" не спят, и только и делают что ищут отрицательные комментарии в сторону своего детища, чтобы потом их безжалостно топить…
                                0
                                Может дело просто в том, что многие из этих комментариев просто не по существу?
                                +1
                                Аналог implicit здесь — extension method, если вы про implicit class. Если вы про что-то другое — можно пример?
                                Насчет закономерности мощность/возможность выстрелить я в целом согласен, хотя приведу два контр примера — C и Haskell.
                                9 пункт поправил, спасибо.
                                  +1
                                  В Scala есть еще implicit convertions и implicit parameters. При помощи них можно придумать много чего самострельного. Хуже то, что для этого не нужно что-то изобретать — наступить эти на грабли можно даже не специально. Если Вам нужны примеры, зайдите на StackOverflow и поищите по «scala implicit» и среди 5800 результатов посмотрите какие у людей с ним проблемы. Кроме всего прочего, такой инструмент при неумелом использовании пораждает различные bad practice. А для умелого использования не хватает четко обозначенных use cases, где использовать это необходимо или разумно.

                                  Не понял на счет контрпримеров. C — это мощный, сложный, но слаботипизированный язык (как говорил мой препод — язык среднего уровня), утыканный граблями. Haskell — наоборот, простой язык с четкой идеей.
                                    +3
                                    Насчет implicit conversions — согласен (для тех, кому лень гуглить — вот пример). Насчет разумного — более-менее адекватно выглядит implicit conversion арифметики, например Int в Long, Double в MySuperDuperComplex и т.п. Кстати, в котлине придется явно указать преобразование.

                                    Насчет мощности — мы, наверно, по-разному понимаем ее. В моем понимании мощность близка к выразительности и набору возможностей. И при таком раскладе — С не мощный, выстрелить элементарно, Haskel — мощный, выстрелить трудно.
                                +1
                                >> lateinit
                                >> lateinit var без аннотации
                                Вы просто не поняли, зачем нужен lateinit.
                                Для того, что у вас написано, действительно нужны Delegates.nonNull или nullable var.

                                >> Почему не стоит думать о Nullable как об Option
                                А почему вы решили, что стоит?..

                                >> внутри run… что надо использовать?
                                Из сигнатуры же очевидно, что this.
                                  0
                                  Хорошо, а для чего его тогда, по-вашему, надо использовать? Цитирую документацию:

                                  Normally, properties declared as having a non-null type must be initialized in the constructor. However, fairly often this is not convenient. For example, properties can be initialized through dependency injection, or in the setup method of a unit test. In this case, you cannot supply a non-null initializer in the constructor, but you still want to avoid null checks when referencing the property inside the body of a class.

                                  To handle this case, you can mark the property with the lateinit modifier

                                  Почему многие подумают, что Nullable — это замена Option, я написал в начале статьи.
                                  Сигнатуру я могу прочитать. Вот только если бы этот момент был бы очевидным, читать бы сигнатуру не пришлось.
                                    –1
                                    Исключительно для того, что сказано в документации, IMHO, в первую очередь для injected-объектов. С другими кейсами я лично не сталкивался, может быть, тесты — но их я еще не писал.

                                    IMHO, lateinit это практически костыль для ограниченного набора случаев, и не стоит его использовать, когда можно обойтись чем-то другим.
                                      +1
                                      В документации прямо говорится, что это костыль, чтобы избежать лишних проверок. Вы можете некорректно сделать инъекцию, забыть вызвать setup метод — на 100% быть уверенным в том, что поле будет проинициализировано, нельзя. В статье я написал, что его вообще использовать не стоит.
                                        0
                                        >> некорректно сделать инъекцию
                                        >> забыть вызвать setup метод
                                        И что, это штатная ситуация, которая должна быть обработана на уровне отдельного объекта? Конечно нет, это ваша ошибка, которую вам нужно как можно скорее исправить. Значит, использование nullable, которое вы предлагаете в посте совершенно некорректно. Зачем лепить проверки на null на поле, которое в нормальном состоянии всегда проинициализировано?

                                        Для таких случаев нужно использовать Delegates.notNull или lateinit — в зависимости от прочих условий. Они следят за выполнением контракта «доступ к полю до инициализации — исключительная ситуация». Дальше вы это исключение обрабатываете или нет, это отдельный вопрос.

                                        Почему же тогда я назвал lateinit «практически костылем»? Потому что он дублирует на уровне языка функциональность, которая реализована в стандартной библиотеке как delegated property Delegates.notNull.
                                        Зачем он введен? Затем, что у delegated property в общем случае нет backing field, на который может потребоваться применить аннотацию, скажем, инжектора.
                                        То есть, lateinit это элемент совместимости совместимости с деталями платформы, которые отсутствуют в языке в явном виде. То есть, практически костыль.
                                  0
                                  От перестановки слагаемых…
                                  Разумеется: 25 28
                                  Удачной рыбалки.
                                    0
                                    От рид-онли nicky1038:

                                    Что мне ещё не понравилось (это не к выстрелам в ногу относится) — что нет clone/copy методов у коллекций
                                    А то, вроде, несколько строк только написал, а уже забомбило.
                                    youtrack.jetbrains.com/issue/KT-11221

                                    Only users with full accounts can post comments. Log in, please.