Kotlin, puzzlers and 2 Kekses: Вы уверены, что знаете, как ведет себя Kotlin?

    Вначале была Java (ладно, не то чтобы в самом начале… но наша история начинается именно здесь), шло время, и спустя 20 с небольшим лет умные ребята из JetBrains спроектировали и зарелизили Kotlin, «более лучшую» Java, универсальный язык, понятный, мощный и прозрачный.

    В свое время Андрей abreslav Бреслав говорил, что Kotlin разрабатывался как удобный и предсказуемый язык. Тогда же прозвучало мнение, что в этом языке вы не найдете паззлеров (коротких кусочков кода, результаты выполнения которых оказываются неожиданными, пугающими или разочаровывающими). Ну что же, Антон antonkeks Кекс поколдовал в IDEA и кое-что все-таки накопал, да еще на наглядных примерах рассказал о своих находках в паре с Филиппом Кексом. Смотрите сами:



    Под катом — подборка таких паззлеров и развернутые комментарии к ним. В основе материала доклад Антона Кекса (Codeborne) и Филиппа Кекса (Creative mobile) на конференции Мобиус 2017 (Санкт-Петербург).

    Начнем с Котлина. Все говорят, что на Яве куча проблем: на острове куча вулканов, там землетрясения. Ее нужно спасать.


    Поэтому на ум приходит другой остров — Котлин.


    Там спокойно, ничего не происходит. Он очень плоский, никаких вулканов. Находится здесь рядом. Поэтому Kotlin — это спаситель Java, особенно для Android-разработчиков — таких, как мы.

    Несколько слов о Котлине


    Что такое Kotlin, здесь более-менее все знают. Потому что какой дурак сегодня пишет под Android без Котлина? Это, мне кажется, уже мазохизм. Он отлично работает. Пару недель назад вышел первый билд Kotlin native. Скоро, может быть, будем и под iOS писать на Котлине.

    Это прагматический язык, open-source, очень прикольный тулинг — он был задизайнен, чтобы хорошо работала IDE. Это камень в огород Apple-овского языка Swift и ему подобных. JetBrains хорошо push-ит Kotlin — специально дизайнит язык под свою IDE.

    Все мы знаем, что Kotlin очень долго разрабатывался. Прошло шесть лет, прежде чем была выпущена версия 1.0. JetBrains очень старались, но, видимо, сделать новый язык не так просто. За прошедшие годы (2010 — 2016) они даже успели поменять логотип на более современный.


    Учитывая, как долго его разрабатывали, язык должен быть превосходный. Это должен быть самый лучший язык в мире, так как многие другие языки девелопились гораздо быстрее. Например, всем известно, что JavaScript был сделан за две недели. Хотя, это, конечно, не rocket science (rocket science — это SpaceX, которые за четыре года научились садиться на платформу на настоящей ракете).

    И самое главное — вы все должны гордиться, потому что Kotlin разрабатывается в Петербурге, и это один из немногих русских языков программирования. Старые русские языки программирования выглядели приблизительно вот так (справа):


    К счастью, этот язык был ориентирован на международную аудиторию, поэтому вместо кейворда «фу» его создатели все-таки решили использовать кейворд fun, над которым все «making fun». Так что это веселый язык.

    Пазлеры


    Что такое пазлеры?

    Это короткие программы, написанные на Котлине, с интересным поведением. А вы будете угадывать, что они печатают. Вы будете голосовать за предложенные варианты ответа, а тот, кто первый поднимет руку и не только угадает правильный вариант, но и объяснит, почему так происходит, получит приз.

    Первая половина пазлеров ориентирована на тех, кто не очень хорошо знаком с Kotlin; вторая половина — для хардкорных Kotlin-разработчиков.

    Kotlin известен тем, что не повторяет некоторые известные пазлеры Java. Однако в идеальном языке программирования не должно быть пазлеров вообще. Получается, что и Kotlin не идеален — не бывает идеальных языков.

    Но Kotlin уже взлетел во многих мобильных приложениях. Этот язык создавался как прагматичный и удобный во многих кейсах. Он удобен в том числе с точки зрения юзабилити. И он все еще развивается. С разработчиками Котлина можно беседовать и пытаться договориться, как и чего исправить.

    Все демонстрируемые пазлеры запускаются с Kotlin 1.1.1 — с последней стабильной версией. Исходные коды пазлеров находятся на GitHub — потом их можно посмотреть: https://github.com/angryziber/kotlin-puzzlers/tree/mobius.
    У кого появятся идеи новых пазлеров, присылайте pull-реквесты. Ждем.

    Пазлер 1


    Котлин хорош тем, что он поддерживает nullability, точнее, он null safe — можно так сказать.

    package p1_nullean
    
    val s: String? = null
    if (s?.isEmpty()) println("true")
    

    У него есть различия между nullable и не nullable типами. Это значит, если мы хотим присвоить куда-то null, это должен быть nullable-тип (с вопросиком). Вероятно, эту идею предложил C#, но он ее не доделал — там только примитивы могут быть nullable. А в Котлине это уже сделано нормально для всех типов. В принципе, язык рассчитан на то, чтобы никогда вы не получали страшных NullPointerException в рантайме.

    В данном примере Котлин перенял из Groovy отличный null-safe оператор s?, который позволяет на нуле вызвать какой-то метод и не схлопотать сразу в рантайме какие-то эксепшены.

    Давайте посмотрим, какой из возможных вариантов мы сейчас получим:

    • nothing
    • true
    • NullPointerException
    • Will not compile

    Запускаем. Смотрим.



    Не скомпилировалось.

    Почему?

    Котлин — type safe язык, поэтому результат выражения s?.isEmpty() — null, поэтому он не кастится в false.

    Исправить легко (надо написать так, чтобы Котлин вел себя так же, как Groovy):

    if (s?.isEmpty() ?: false) println("true")
    

    Это не очень красиво, но сейчас так. В котлиновский трекер уже засабмичено предложение все-таки интерпретировать null как false, но там есть свои нюансы. Еще неизвестно, получим мы эту фичу когда-нибудь или нет. Есть разные мнения, но с точки зрения удобства использования языка это небольшой косяк.

    Пазлер 2


    Пазлер очень похожий: у нас та же переменная nullable string и мы на нем пытаемся вызвать метод.

    package p2_nulleanExtended
    val x: String? = null
    print(x.isNullOrEmpty())
    

    Какой будет результат?

    • true
    • false
    • NullPointerException
    • не скомпилируется

    Запускаем…
    Ответ: true.



    Почему? Это extension-функция из стандартной библиотеки Kotlin, которая «повешена» на nullable CharSequence. Поэтому в подобном кейсе она обрабатывается нормально.



    Действительно, в Котлине можно некоторые функции запускать на null. Компилятор про это знает и позволяет это делать.

    Если бы мы поставили знак вопроса, IDEA бы нам сказала, что он тут не нужен.

    print(x?.isNullOrEmpty())
    

    Хорошо, что функция названа по-человечески (о результате можно догадаться по названию).

    Если запустить во втором варианте (со знаком вопроса), результат будет null, потому что выражение в скобках теперь приводится в null, и тогда эта функция не вызывается, несмотря на то, что она поддерживает null. Это, кстати, еще один пазлер.

    Пазлер 3


    package p3_platformNulls
    
    class Kotlin {
        fun hello(name: String) = print("Hello $name")
    }
    
    fun main(args: Array<String>) {
        val prop = System.getProperty("key")
        Kotlin().hello(prop)
    }
    

    В Котлине есть такая интересная фича — третий стейт nullability. Посмотрим, что будет, если мы вызовем:

    val prop = System.getProperty("key")
    

    и передадим это в метод hello у класса Kotlin, который должен его распечатать:

    Kotlin().hello(prop)
    

    Что получится на выходе?

    • hello
    • hello null
    • не скомпилируется
    • ни один из приведенных вариантов

    Вообще type inference — отличная тема.

    Запускаем. Получаем IllegalStateExeption.



    Почему?

    Значение prop будет null, тип — String!.. Он идет в hello, и в рантайме будет проверка, что должен быть не null, а он null.

    На самом деле в начальной версии Котлина действительно сделали, что когда из Java приходит String, он всегда по умолчанию nullable.

    val prop: String? = System.getProperty("key")
    


    Это привело к тому, что стало очень неудобно писать код, когда идет интероп с Java. И решили сделать виртуальный тип String! (с восклицательным знаком). Это как раз третий вариант nullability — называется «я не знаю».

    val prop: String! = System.getProperty("key")
    

    Однако такой код не компилируется, поскольку тип String! нельзя объявить самостоятельно. Он может прийти только из Java.

    Поэтому такие штуки лучше заранее объявлять как nullable или не nullable (как правило, вы из API знаете, может там null когда-нибудь прийти или нет).

    А вот такой код скомпилируется, но может упасть в рантайме:

    val prop: String = System.getProperty("key")
    

    IDEA всегда знает, где какой тип. Можно нажать на переменной Ctrl+q и выяснить.
    Но закончим с nullability, перейдем к другой теме.

    Пазлер 4


    У нас есть 2 функции, которые должны печатать. Мы их объявляем и запускаем — должно быть все просто:

    package p4_kotlinVsScala
    
    fun main1() = print("Hello")
    
    fun main2() = {
        print("Hello2")
    }
    
    main1()
    main2()
    

    Что будет на выходе?

    • Hello
    • Hello2
    • HelloHello2
    • не скомпилируется

    Запускаем… Получаем Hello.



    Почему?

    Main1 вернет юнит, но при этом вызовет print(«Hello»). А main2 всего лишь вернет лямбду, которая не будет выполняться.

    Исправить можно так:

    main2()()
    

    Второй, на мой взгляд, лучший вариант исправления — убрать знак равно у main2, поскольку он всех только смущает:

    fun main2() {
    print("Hello 2")
    }
    

    Почему я назвал этот пример Котлин vs Scala? Те, кто писал на Scala, знают, что там этот код — абсолютно валидное объявление функции, которая что-то возвращает:

    fun main2() = { }
    

    Бедные Scala-девелоперы, которые будут писать на Котлине. Они, наверное, постоянно будут возвращать лямбды без запуска.

    Пазлер 5


    У нас есть list из цифр, мы его перебираем методом forEach. ForEach, как и в Groovy, если параметр лямбда не объявлен, знает it. И мы проверяем, что он не больше 2, и печатаем.

    package p5_sneakyReturn
    
    fun main(args: Array<String>) {
        listOf(1, 2, 3).forEach {
            if (it > 2) return
            print(it)
        }
        print("ok")
    }
    

    Какой будет итог?

    • 123ok
    • 12ok
    • 12
    • бесконечный цикл

    Запускаем…



    12
    Что за ерунда?

    В Котлине return возвращает из функции. А чтобы выйти из конкретной лямбды, внутри этой функции нужно после return указать название лямбды:

    if (it > 2) return@forEach
    

    Так можно выйти из лямбды. Если вы пишите на Java и вам очень нужно выйти из лямбды, это вариант исправления кода.

    На самом деле return в Котлине работает так, как он должен работать. Если бы мы до этого не писали бы на C# и Java, наверное, и не ошиблись бы, потому что return возвращается из функции main. Все логично. И нет никаких странных фич с лямбдами, из которых тоже почему-то нужно выйти.

    Почему это так работает?



    Функция forEach объявлена как inline функция. В Kotlin компилятор не вызывает эту функцию в скомпилированном коде, а берет код этой функции и вставляет на то место, где был call. В результате здесь получается обычный for-цикл и, естественно, тогда return выходит из функции main.

    Как понять, что это Inline функция? Во-первых, в IDEA есть Ctrl+p. А во-вторых, если вызвать return, а функция окажется не inline, то компилятор скажет: «Извини, нельзя это делать». То есть компилятор не позволит нам сделать какую-то ерунду.

    Есть еще один вариант, как можно исправить этот код, чтобы он возвращал «12ok». Нужно это объявить как функцию, а не лямбду.

    fun main(args: Array<String>) {
        listOf(1, 2, 3).forEach(fun() {
            if (it > 2) return
            print(it)
        })
        print("ok")
    }
    

    Единственное отличие в Котлине анонимной функции и лямбды в том, что первая ведет себя именно как функция, а значит — return будет возвращать из ближайшего «веселья» (fun). Поэтому с таким исправлением оно будет работать как надо.

    Чтобы было еще интереснее, я подготовил несколько примеров. В Котлине бывают разные кейворды:

    • fun
    • inline fun
    • inline fun с лямбдой noinline
    • inline fun с лямбдой crossinline

    Некоторые из них позволяют использовать return, а некоторые — нет.

    package p5_sneakyReturn
    
    fun hello(block: () -> Unit) = block()
    
    inline fun helloInline(block: () -> Unit) = block()
    
    inline fun helloNoInline(noinline block: () -> Unit) = hello(block)
    
    inline fun helloCrossInline(crossinline block: () -> Unit) = runnable { block() }.run()
    
    fun main(args: Array<String>) {
        hello {
            println("hello")
            //return - impossible
        }
    
        hello(fun() {
            println("hello")
            return
        })
    
        helloInline {
            println("hello")
            return
        }
    
        helloNoInline {
            println("hello")
            //return - impossible
        }
    
        helloCrossInline {
            println("hello")
            //return - impossible
        }
    
    

    Домашним заданием оставляем пожелание разобраться, что такое Crossinline. Думаю, вам это будет интересно.

    Когда я только начал писать на Котлине, я тоже подумал, что это нечто сложное. Но когда ты понимаешь, что такое inline-функция (почти все extension-функции для коллекции — Inline для performance), все становится очень логичным.

    Пазлер 6


    Нам нужно получить John или Jaan.

    У нас есть простой класс Person. В Котлине очень удобно: можно при декларации класса сразу продекларировать конструктор. Мы получаем переменную конструктора name, забиваем ее в property. В Котлине нет field — есть только property, что очень круто, так как не нужно писать геттер, сеттеры и всякую ерунду (или геты и сеты, как в C#). Отличный красивый синтаксис.

    В итоге мы создаем Person с именем John и смотрим, превратится ли он у нас в эстонскую локализацию Jaan:

    package p6_getMeJohn
    
    class Person(name: String) {
        var name = name
            get() = if (name == "John") "Jaan" else name
    }
    
    println(Person("John").name)
    

    • John
    • Jaan
    • не скомпилируется
    • ни один из вариантов

    Запускаем…



    Это stack overflow.
    Почему?
    Мы берем name, делаем ему if-else и вызываем его же по get. Чтобы исправить, нужно обратиться к полю, а не к property. Можно использовать кейворд field:

    class Person(name: String) {
        var name = name
            get() = if (field == "John") "Jaan" else field
    }
    

    По кейворду field в Котлине можно обратиться к полю, но единственное место, где это можно сделать, — внутри геттера / сеттера. Все остальные обращения идут только через property — напрямую к field не обращаются.

    Говорят, что по перформансу все это круто, потому что Java Hotspot компилятор это хорошо оптимизирует, в отличие от виртуальных машин .NET, и все работает очень быстро.

    Пазлер 7


    Снова смотрим на офигенную фичу языка — type inference — нас не волнует, какого типа whatAmI, мы его можем все равно использовать. Но компилятор знает, что это такое. Посмотрим, знаем ли мы.

    package p7_whatAmI
    
    val whatAmI = {}()
    println(whatAmI)
    

    Какой вариант будет в итоге?

    • kotlin.jvm.functions.Function0
    • () -> kotlin.Unit
    • kotlin.Unit
    • ничего

    Запускаем… Получаем kotlin.Unit.



    Почему?

    Здесь объявляется лямбда, потом происходит вызов лямбды. Так как лямбда ничего не возвращает (точнее, возвращает kotlin.Unit), именно это и выводится. А самое лучшее определение unit — это void.

    Откуда вообще пришел Unit? По-моему, даже в математике (или в computer science) есть такое понятие как теория типов. И там описано, что Unit — это один элемент, который означает «ничего». Поэтому некоторые более академические языки программирования используют термин Unit. Котлин был задизайнен как прагматичный язык, но, тем не менее, его разработчики решили выбрать не прагматичный void, а придумали сделать Unit.

    Чтобы вам было еще интереснее, в Котлине есть еще один тип: kotlin.Nothing.
    Чем они отличаются? Пусть ответ на этот вопрос будет вам домашним заданием.

    Пазлер 8


    Мы посмотрели whatAmI, а теперь у нас будет iAmThis.

    Здесь все немного усложняется: у нас есть класс IAm, он — data class (это офигенная фича в Kotlin, которая за нас автоматически генерирует equal, hashCode, toString и весь этот boiler plate, который мы все так ненавидим писать на Java). В Scala это case class — там название для этого хуже, хотя на самом деле все используют его именно как data class.

    У класса IAm есть конструктор, в котором объявляем поле foo. Foo одновременно является property, поэтому его можно использовать с функцией hello().

    Мы передаем туда String «bar», вызываем функцию hello и смотрим, что она нам возвращает.

    package p8_iAmThis
    
    data class IAm(var foo: String) {
        fun hello() = foo.apply {
            return this
        }
    }
    
    println(IAm("bar").hello())
    

    Что получим на выходе?

    • IAm
    • IAm(foo=bar)
    • bar
    • не скомпилируется

    Запускаем… Получаем bar



    Почему?

    Apply — хитрая extension-функция. Она принимает лямбду и позволяет внутри нее с объектом, на котором она вызвана, выполнять какие-то действия по this. Соответственно, this — это bar. И Hello — это bar.

    В этом Kotlin похож на JavaScript. Как в JavaScript, в Kotlin можно достичь того состояния, когда вы уже не знаете, что такое this.

    Вообще там есть много полезных функций: also, let, with.



    В принципе, они все отличаются достаточно мало.

    К примеру, apply — это extension-функция на абсолютно любой тип (не nullable). Она принимает лямбду, а лямбда эта очень хитрая, потому что она апплаится к внутреннему T, а не к внешнему объекту (внутри этой лямбды свой Т). Т.е. функция вызывает эту лямбду со своим this и возвращает this (это иногда тоже полезно).

    Есть и другие функции. Код можно исправить следующим образом:

    package p8_iAmThis
    
    data class IAm(var foo: String) {
        fun hello() = foo.let {
            return it
        }
    }
    
    println(IAm("bar").hello())
    

    Тогда это, может быть, станет менее непонятно.

    Для некоторых кейсов Apply — очень полезная функция. Но если вы очень быстро смотрите на код (и при этом используется первый вариант записи), можно запутаться.

    В первом варианте можно сократить код так (функция apply и сама возвращает this, поэтому ничего не меняется):

    data class IAm(var foo: String) {
        fun hello() = foo.apply {
        }
    }
    

    Пазлер 9


    Посмотрим на уже известную нам функцию let.

    Этот пазлер прислал Kevin Most из Канады. У него есть простая функция, которая печатает знак аргумента (Int).

    package p9_weirdChaining
    // by Kevin Most @kevinmost
    
    fun printNumberSign(num; Int) {
        if (num < 0) {
            "negative"
        }  else if (num > 0) {
            "positive"
        } else {
            "zero"
        }.let { println(it) }
    }
    printNumberSign(-2)
    printNumberSign(0)
    printNumberSign(2)
    

    Что такой код будет печатать?

    • negative; zero; positive
    • negative; zero
    • negative; positive
    • zero; positive

    Запускаем… На выходе — zero; positive.



    В чем же дело?

    If — это на самом деле выражение. То есть получается два выражения, и let применяется только ко второму.

    Я много писал на Kotlin, но этот пазлер сам не решил. Это какая-то адская тема. На предыдущей конференции JPoint мы даже думали, что это баг в компиляторе. Но я спросил у Андрея Бреслава, и выяснилось, что это просто нюанс парсера.

    Как исправить? Легко — достаточно поставить скобки:

    fun printNumberSign(num; Int) {
        (if (num < 0) {
            "negative"
        }  else if (num > 0) {
            "positive"
        } else {
            "zero"
        }).let { println(it) }
    }
    

    Тогда let применяется к результату всего выражения. А в первом случае код срабатывал так:

    fun printNumberSign(num; Int) {
        if (num < 0) {
            "negative"
        }  else (if (num > 0) {
            "positive"
        } else {
            "zero"
        }).let { println(it) }
    }
    

    При этом верхний expression идет отдельно — к нему функция let не применяется.
    Оператора elseif в Котлине нет (если бы он был, тогда бы этого пазлера бы и не было).

    Как и во всех пазлерах, мораль такова: не пишите такой код. Если хотите сделать что-то сложное (как здесь), обязательно поставьте скобки или положите это в переменную и потом вызовите let.

    Пазлер 10


    Еще более интересный пазлер. Тут много кода.

    Этот пазлер засабмиттил Даниил Водопьян. Это пазлер на очень классную фичу в Kotlin — delegate properties. В Котлине мы можем объявить, например, что в классе есть несколько properties, и они имплементируются не как field, а как лукапы из map.

    У нас есть класс Population — население. А cities нам передает (var cities: Map<String, Int>) и мы делегируем их в этот map.

    Это фактически позволяет превратить Kotlin в JavaScript и делать более динамические структуры, не копировать данные туда-сюда. Такие классы сокращают очень много кода.

    Потом мы создаем инстанс класса Population и передаем ему для всех городов население.

    Теперь представим, что прошло много лет. Люди загадили Землю — улетели жить на Марс. Поэтому мы сбрасываем map с населением.

    Здесь есть функция with, которую мы смотрели до этого. Она берет population и ресолвит относительно него имеющиеся field-ы (в принципе, точно также, как и apply).

    package p10_mappedDelegates
    // by Daniil Vodopian @voddan
    
    class Population(var cities: Map<String, Int>) {
        val tallinn by cities
        val kronstadt by cities
        val st_petersburg by cities
    }
    
    val population = Population(mapOf(
        "st_petersburg" to 5_281_579,
        "tallinn" to 407_947,
        "kronstadt" to 43_005
    ))
    
    // Many years have passed, now all humans live on Mars 
    population.cities = emptyMap()
    
    with(population) {
        println("$tallinn; $kronstadt; $st_petersburg")
    }
    

    Все легко. Осталось только понять, что станет с нашей Землей, когда все улетят на Марс. Что такой код выдаст?

    • 0; 0; 0
    • 407947; 43005; 5281579
    • NullPointerException
    • NoSuchElementException

    Запускаем… Оказывается, люди никуда не исчезли (на Марсе жить очень сложно, поэтому мы, скорее всего, останемся на Земле).



    Почему?

    Неверно сказать, что population.cities = emptyMap() сделает пустую map у класса, но не у его экземпляра. Если мы изменим код так (сделаем MutableMap и обнулим Кронштадт — population.kronstadt = 0):

    class Population(var cities: MutableMap<String, Int>) {
        val tallinn by cities
        var kronstadt by cities
        val st_petersburg by cities
    }
    
    val population = Population(mutablemapOf(
        "st_petersburg" to 5_281_579,
        "tallinn" to 407_947,
        "kronstadt" to 43_005
    ))
    
    // Many years have passed, now all humans live on Mars 
    population.kronstadt = 0
    
    

    Код выведет: 407947; 0; 5281579

    Но обсуждаем мы все-таки первый вариант (c population.cities = emptyMap()).

    Когда мы исполняем delegate, ссылка на map запоминается внутри геттера (для каждого из них). И если мы меняем ссылку на cities, это уже не меняет ссылки внутри геттеров. Но мы можем даже в cities положить в map другое, и все будет работать, поскольку это все равно остается ссылка на тот же самый map. Но если мы меняем референс на другой map, то он перестает действовать.

    Пазлер 11


    У нас в Эстонии есть отличная поговорка: «У хорошего ребенка есть много имен».

    Посмотрим, как это здесь относится к нашим классам.

    В Котлине есть такой странный нюанс: классы по умолчанию final — их нельзя проэкстендить. Есть кейворд open, который все-таки позволяет их экстендить.

    В этом пазлере в классе C у нас есть open-метод (тоже, чтобы мы могли его заоверрайдить). Здесь мы берем x и y (у них есть дефолтные значения — это очень классная фича в языке).

    У нас есть класс D, который экстендит класс C и оверрайдит функцию sum, но в принципе ничего полезного не делает, кроме того, что вызывает супер-имплементацию.

    Дальше у нас есть переменная d — мы создаем инстанс класса D; у нас есть переменная c и туда мы присваиваем тот же самый инстанс (получаем 2 референса на один и тот же инстанс класса D). И мы вызываем один и тот же метод по сути на одном и том же объекте.

    package p11_goodChildHasManyNames
    
    open class C {
      open fun sum(x: Int = 1, y: Int = 2): Int = x + y
    }
    
    class D : C() {
      override fun sum(y: Int, x: Int): Int = super.sum(x, y)
    }
    
    val d: D = D()
    val c: C = d
    print(c.sum(x = 0))
    print(d.sum(x = 0))
    println()
    

    Что получим в итоге?

    • 22
    • 11
    • 21
    • не скомпилируется

    Запускаем… Правильный ответ — 21.



    Здесь еще есть некоторые warning-и, которые помогают понять, что происходит.

    В обоих случая вызывается переопределенная функция, потому что полиморфизм. В рантайме выбирается, какая функция вызывается, потому что в реальности и c, и d — это инстанс класса D. Но так как у JVM нет такой фичи, как именные параметры, их ресолвит компилятор от compile-time. Т.е. получается, что функция выбирается и вызывается в рантайме, а параметры выбираются в compile-time. Поэтому какие параметры он подставляет, зависит от типа переменной, а не объекта, получающегося в рантайме. Это косяк. Warning-и предупреждают, что не следует путать свои названия — когда вы оверрайдите функцию, ее надо назвать иначе.

    Хорошая новость в том, что примерно для половины представленных пазлеров в IDEA уже есть warning. Благодаря тому, что JetBrains сами занимаются еще и инструментами, они достаточно хорошо помогают избегать многих ошибок. Но не всех. Для некоторых из пазлеров warning сделать попросту невозможно.

    Однако язык развивается. В 2016 году, когда я только начал на нем писать, было гораздо меньше инспекций в IDEA и гораздо проще было эти пазлеры самому схлопотать. Сейчас ситуация совсем другая: вышла версия 1.1, было много патч-релизов, много инспекций добавлено в IDEA, и на Котлине писать правильно теперь очень легко.

    Вместо заключения хочу сказать: переходите на Kotlin.

    • Под Android до сих пор нет нормальной Java 8, а в Котлине вы получаете все фичи Java 8 и даже еще больше. Можно гораздо лучше себя выражать.
    • Котлин — язык без большого хайпа. Это тоже его плюс.
    • Его часто называют «Swift» для Android. Но со Swift есть небольшая проблема — когда выходит новая версия, приходится постоянно переписывать весь код. С Котлиным такой проблемы нет — нам обещают обратную совместимость, как и source-level, так и binary-level.
    • Kotlin компилируется гораздо быстрее, чем Scala. Он гораздо проще Scala.
    • Он гораздо быстрее в рантайме, чем Groovy. Если вы добавляете свое приложение на Android, то размер по-моему увеличивается всего на 600 Кб по сравнению с Java — и это очень мало по сравнению со Scala. Поэтому есть смысл на нем писать.
    • Когда я на него перешел, я начал быть продуктивным уже с первого дня.
    • Про Kotlin говорят, что это «более хороший Groovy», там есть хорошие фичи.
    • И ваш самый главный друг в IDEA — это Ctrl+Alt+Shift+K, который сконвертирует любой класс Java сразу в Kotlin (as is). При этом нет Ctrl+Alt+Shift+J, поэтому вы не можете уже вернуться — это дорога в один конец. Да вы и не захотите возвращаться.
    • Также переходит Gradle.

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



    Если любите нутрянку программирования так же, как и мы, и хотите основательнее погрузиться в Kotlin, рекомендуем обратить внимание вот на эти доклады, которые будут на грядущей конференции Mobius 2017 Moscow:
    JUG.ru Group 682,94
    Конференции для взрослых. Java, .NET, JS и др. 18+
    Поделиться публикацией
    Комментарии 24
    • 0
      В этом Kotlin похож на JavaScript. Как в JavaScript, в Kotlin можно достичь того состояния, когда вы уже не знаете, что такое this.

      *что-то вычеркивает в записной книжке
      • 0

        Тут иде может помочь. Есть вот такое предложение:
        https://youtrack.jetbrains.com/issue/KT-20533

        • +2
          Разница в том, что в JavaScript this определяется динамически, и без трассировки (хотя бы мысленной) не обойтись, а в Kotlin this можно вычислить статически, просто «на глаз» это не всегда очевидно. Иногда помогает расстановка явных скоупов this@Something
        • 0
          К сожалению, с практической точки зрения, эти «прикольные фичи», на самом деле – «долбанные ошибки дизайна».

          Они есть во всех языках, но в некоторых для этого нужно сделать что-то невероятное.
          А в Котлине, как показывает автор, на эти грабли можно будет наступить на каждом шагу :-(

          Язык очень интересный, но складывается впечатление, что взяли все возможные фичи со всех языков и попытались все их уместить в один язык. В результате имеем такие вот забавные пазлеры.
          • +1
            «Неочевидная вещь» это не то же самое, что «долбанная ошибка». И да, другим языкам тут лучше помолчать, потому что у них заскоков бывает ещё больше.
            • +1
              Вы урезали контекст, изначально было: «Долбанная ошибка дизайна».
              «Неочевидная вещь» – это проблема дизайна, т.е. – «Долбанная ошибка дизайна».

              Тут хорошо подойдет аналогия с UI/UX: все должно быть максимально просто и очевидно. Если это не так, значит есть проблема. Язык программирования – это товар, а Программист – потребитель/пользователь.

              Если Вы не согласны с этим утверждением, тогда ответьте на вопрос: почему каждый год выходят десятки новых языков и почему каждый раз, когда крупная компания выпускает свой язык, то подымается хайп?
              Kotlin, Scala, Groovy, TypeScript, Elm, CoffeScript, Rust, Go, Swift, etc.

              На сколько помню, целью Котлина было облегчение разработки и защита от глупых ошибок. А в результате одни глупые ошибки заменили на другие.
              • –1
                «Неочевидная вещь» – это проблема дизайна

                Умножение — вещь, неочевидная для первоклассника. Надо ли делать язык без умножений или учить первоклассников умножению?


                На сколько помню, целью Котлина было облегчение разработки и защита от глупых ошибок. А в результате одни глупые ошибки заменили на другие.

                Это суждение — следствие опыта разработки на котлине, или вы просто посмотрели пазлеры?

                • 0
                  1. Это не паззлер. Любой оператор?.. вернет nullable тип, это очевидно.
                    Автора предложения об автоматической конвертации null в false надо вернуть обратно в яваскрипт и не выпускать до просветления.
                  2. И это не пазлер. Для nullable типа могут быть методы расширения, это тоже очевидно.
                  3. И снова не пазлер. Очевидное решение по скрещиванию null-safe языка с null-unsafe JVM.
                  4. И опять не пазлер. Разница между методом и вызовом также очевидна.
                  5. Первый реальный пазлер. С лямбдами это вечный вопрос и в каждом языке он решается по-своему.
                  6. Не пазлер. Результат рекурсии немного предсказуем.
                  7. Снова не пазлер. Ибо тезис "самое лучшее определение unit — это void." неверен. void — это не прагматизм, а костыль, заботливо перенесенный из си.
                  8. Второй реальный пазлер и первый возможный претендент на ошибку дизайна. Правда сравнение с яваскриптом некорректно. В котлине значение this известно на этапе компиляции.
                  9. Третий реальный пазлер на типичной проблеме приоритета и ассоциативности операций. Ошибкой дизайна не является, код паззлера выглядит плохо и имеет заведомо лучшую альтернативу.
                  10. Не пазлер. Код сразу выглядит плохо, используя зависимость val от var. Конец снова предсказуем.
                  11. Не пазлер, компилятор бдит, жаль только warning а не error.

                  Итоги: 3 реальных пазлера из 11, 1 (одна) возможная ошибка дизайна. В остальных случаях результат или очевиден сразу, или плохой код и выглядит плохо (для человека, а нередко и для компилятора), что и требуется для защиты от глупых ошибок.

            • +4
              Настоящий паззлер — это Пазлер 11. Все остальное — это ожидаемое поведение.
              • +3

                я не спец по котлину, но 9ый и 10ый пазлеры для меня выглядят как бага языка

                • –1
                  Нууу…
                  Пазлер 9 — спорно. Можно рассматривать и так, и так. Возможно, JetBrains выбрали менее удобный вариант, но лично я предпочитаю использовать ключевое слово when, если веток больше чем 2.
                  Пазлер 10 — не баг, однозначно. Из доки следует, что делегат это объект с вот такими методами:
                  //на чтение
                  operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
                          return "$thisRef, thank you for delegating '${property.name}' to me!"
                  }
                  //на запись 
                  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
                          println("$value has been assigned to '${property.name} in $thisRef.'")
                  }
                  

                  У интерфейса kotlin.collections.Map нет таких методов, зато есть соответствующие экстеншн функции в файле MapAccessors.
                  То есть синтаксис объявления делегата по мапе не магия, которая от нас сокрыта, а вполне традиционный для котлина способ делегирования.
              • 0
                промахнулся, отвечая пользователю Moxa, см ответ выше
                • +8
                  Люди, которые хотят автоматическое приведение Nothing к False, сами не знают чего хотят.
                  • –3
                    Отличная статья, но вот этот программистский новояз реально режет глаза — «пушит», «тулинг», «кейворд», и т.д. Неужели нельзя найти подходящие русскоязычные термины? Или вы боитесь что будет как в примере с «фу, знач, печать»? Спешу вас успокоить, вы только выиграете, если будете периодически чистить свою речь.
                    • +5
                      И чтобы лишний раз не стрелять себе в ногу, в примере 9 лучше писать с when:
                      fun printNumber(num: Int) {
                          when {
                              num < 0 -> "negative"
                              num > 0 -> "positive"
                              else -> "zero"
                          }.let { println(it) }
                      }
                      

                      Кстати, если в исходном примере встать на первый if и применить intention «Replace 'if' with 'when'», то картина почему всё так было, становится яснее:
                          when {
                              num < 0 -> "negative"
                              else -> if (num > 0) {
                                  "positive"
                              } else {
                                  "zero"
                              }.let { println(it) }
                          }

                      • 0
                        if (s?.isEmpty() ?: false) println(«true»)
                        Студия уже давно предлагает такое исправлять на
                        if (s?.isEmpty() == true) println("true")
                        • 0
                          Тут будет сравнение null == true,
                          null – это не булевый тип, т.е. эта запись недоступна (как в котлине, так и в джаве).
                          • 0
                            Такое сравнение допустимо: если одно из слагаемых будет равно null, то результат будет равен false. А вот null == null, ожидаемо вернет true.
                            • +1
                              Будет, но это сравнение двух значений типа bool? — вполне рабочее.
                              • 0
                                Изначально обсуждался следующий пример:
                                String s = null;
                                if ( s?.isEmpty() == true ) {
                                   // do something
                                }

                                В этом случае s?.isEmpty() вернет null.
                                Но null и true сравнивать запрещено.

                                null нельзя привести в boolean. А если это сделать – это ослабит типизацию, что повлечет ряд других проблем.
                                • 0
                                  Изначально обсуждался следующий пример:
                                  String s = null;

                                  Это вообще не котлин.
                                  Правильно как в статье:


                                  val s: String? = null

                                  А вторая строка — наоборот совершенно правильная


                                  if ( s?.isEmpty() == true )

                                  1. s имеет тип String? (nullable String)
                                  2. s?.isEmpty() имеет тип Boolean? (nullable Boolean)
                                  3. Константа true принадлежит как типу Boolean, так и типу Boolean?
                                  4. Конкретный тип котлин выводит по контексту — слева от равенства Boolean?
                                  5. Соответственно справа будет тоже Boolean?
                                  6. Для этого типа можно сравнить true с null

                                  Ссылка для проверки онлайн:
                                  https://try.kotlinlang.org/#/UserProjects/2f15ok94p3k8lse1fj96t6apr0/2032nrf2rjpjvp11t1307onr7m

                                  • 0
                                    null нельзя привести в boolean

                                    Зато boolean можно привести в boolean?

                            • 0
                              С Котлиным такой проблемы нет

                              Котлин это не фамилия :)
                              А вообще для kotlin-программиста большинство этих паззлеров не то что известно — это очевидное поведение языка :) Самый страшный паззлер 11, но и вкупэ с Intellij Idea страх перед возможностью напороться на паззлеры сам собой рассеивается.
                              • +1
                                Котлин — язык без большого хайпа.

                                лол

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое